-- Launcher touch-controls editor (#327): drag each on-screen button to a -- new spot, resize them, toggle the overlay off entirely, Reset to -- defaults, Done to persist into options.lua. Opened from the game -- panel's "Touch Controls" button; main.lua suspends the launcher the same -- way it does for the save editor. -- -- Everything edited here belongs to the orientation currently on screen -- (#633): portrait and landscape keep separate positions and sizes, so a -- player lays out each rotation once. Rotate the device (or resize the -- desktop window past square) and the editor switches buckets live. -- -- Draws in full window LOVE units -- the same space TouchControls uses -- after Renderer:endFrame -- so what you drag here is what you get in play. -- -- Switch / gamepad: PadCursor draws the virtual pointer the launcher just -- dropped (same soft-lock class as the save editor). A clicks / drags, B -- closes (Done), shoulders nudge button size. local SaveData = require("src.core.SaveData") local TouchControls = require("src.core.TouchControls") local PadCursor = require("src.ui.PadCursor") local GamepadMap = require("src.core.GamepadMap") local Editor = {} local PAL = { bgTop = { 8, 14, 36 }, bgBot = { 4, 8, 22 }, white = { 245, 248, 255 }, label = { 160, 175, 210 }, green = { 80, 220, 140 }, red = { 240, 90, 110 }, card = { 18, 28, 58 }, stroke = { 120, 150, 220 }, } local function col(c, a) love.graphics.setColor(c[1] / 255, c[2] / 255, c[3] / 255, a or 1) end local function inside(r, x, y) return r and x >= r.x and x <= r.x + r.w and y >= r.y and y <= r.y + r.h end local function roundRect(mode, x, y, w, h, r) love.graphics.rectangle(mode, x, y, w, h, r, r) end function Editor.load(opts) opts = opts or {} Editor.onClose = opts.onClose Editor.version = opts.version Editor.hostPoll = opts.hostPoll == true Editor.drag = nil Editor.rects = {} Editor._hostMouse = false Editor._hostTouches = nil Editor.fonts = { title = love.graphics.newFont(28), body = love.graphics.newFont(16), btn = love.graphics.newFont(18), } local optsTbl = SaveData.loadOptions() local applied = optsTbl if opts.version == "gold" then local gold = type(optsTbl.gold) == "table" and optsTbl.gold or {} applied = { touchControls = gold.touchControls, haptics = gold.haptics or optsTbl.haptics, } end TouchControls:init() TouchControls:ensureImages() TouchControls:applyOptions(applied) TouchControls:setPreview(true) Editor.enabled = TouchControls.enabled ~= false PadCursor.reset() end function Editor.unload() TouchControls:setPreview(false) TouchControls:reset() PadCursor.reset() Editor.drag = nil Editor.onClose = nil Editor.version = nil Editor._hostMouse = false Editor._hostTouches = nil end local function persist() local opts = SaveData.loadOptions() local cfg = TouchControls:config() local block = { enabled = cfg.enabled, layouts = cfg.layouts, } if Editor.version == "gold" then opts.gold = type(opts.gold) == "table" and opts.gold or {} opts.gold.touchControls = block else opts.touchControls = block end SaveData.saveOptions(opts) end local function close() persist() local cb = Editor.onClose Editor.unload() if cb then cb() end end local function resetLayout() TouchControls:clearPositions() end local function toggleEnabled() Editor.enabled = not Editor.enabled TouchControls.enabled = Editor.enabled if not Editor.enabled then TouchControls:reset() end end function Editor.update(dt) PadCursor.update(dt or 0) if Editor.hostPoll then Editor.pollHostPointers() end if not Editor.drag then return end local x, y if Editor.drag.touchId == "pad" then x, y = PadCursor.pointer() elseif love.touch and love.touch.getPosition and Editor.drag.touchId then local ok, tx, ty = pcall(love.touch.getPosition, Editor.drag.touchId) if ok and tx then x, y = tx, ty end end if not x and love.mouse then x, y = love.mouse.getPosition() end if x then TouchControls:setControlCenter( Editor.drag.name, x - Editor.drag.offX, y - Editor.drag.offY) end end function Editor.draw() local SafeArea = require("src.core.SafeArea") local fullW, fullH = love.graphics.getDimensions() local ox, oy, ww, wh = SafeArea.rect() local s = math.max(0.75, math.min(1.4, wh / 768)) -- resolve the orientation bucket before any chrome reads scale (#633) local bucket = TouchControls:currentBucket() Editor.rects = {} -- radial-ish navy field (two stacked fills; matches launcher atmosphere) col(PAL.bgBot) love.graphics.rectangle("fill", 0, 0, fullW, fullH) col(PAL.bgTop, 0.85) love.graphics.circle("fill", ox + ww * 0.5, oy + wh * 0.15, math.max(ww, wh) * 0.55) local pad = 18 * s local barH = 56 * s local btnH = 40 * s local btnW = 100 * s -- top bar (inside the safe area so it clears the notch / status bar) col(PAL.card, 0.92) love.graphics.rectangle("fill", 0, 0, fullW, oy + barH + pad) col(PAL.stroke, 0.35) love.graphics.setLineWidth(1) love.graphics.line(0, oy + barH + pad, fullW, oy + barH + pad) love.graphics.setFont(Editor.fonts.title) col(PAL.white) love.graphics.print("Touch Controls", ox + pad, oy + pad + 4 * s) -- Done / Reset local done = { x = ox + ww - pad - btnW, y = oy + pad + (barH - btnH) / 2, w = btnW, h = btnH } local reset = { x = done.x - 10 * s - btnW, y = done.y, w = btnW, h = btnH } Editor.rects.done, Editor.rects.reset = done, reset local function chromeBtn(r, label, fill) col(fill, 0.9) roundRect("fill", r.x, r.y, r.w, r.h, 8 * s) col(PAL.white, 0.2) roundRect("line", r.x, r.y, r.w, r.h, 8 * s) love.graphics.setFont(Editor.fonts.btn) col(PAL.white) local tw = Editor.fonts.btn:getWidth(label) love.graphics.print(label, r.x + (r.w - tw) / 2, r.y + (r.h - Editor.fonts.btn:getHeight()) / 2) end chromeBtn(reset, "Reset", { 60, 70, 110 }) chromeBtn(done, "Done", PAL.green) -- enable toggle card local cardY = oy + barH + pad + 14 * s local cardH = 64 * s local cardX, cardW = ox + pad, ww - 2 * pad col(PAL.card, 0.88) roundRect("fill", cardX, cardY, cardW, cardH, 12 * s) col(PAL.stroke, 0.4) roundRect("line", cardX, cardY, cardW, cardH, 12 * s) love.graphics.setFont(Editor.fonts.body) col(PAL.label) love.graphics.print("On-screen controls", cardX + 16 * s, cardY + 12 * s) love.graphics.setFont(Editor.fonts.btn) local on = Editor.enabled col(on and PAL.green or PAL.red) love.graphics.print(on and "ON" or "OFF", cardX + 16 * s, cardY + 34 * s) local toggleW = 110 * s local toggle = { x = cardX + cardW - 16 * s - toggleW, y = cardY + (cardH - btnH) / 2, w = toggleW, h = btnH, } Editor.rects.toggle = toggle chromeBtn(toggle, on and "Disable" or "Enable", on and PAL.red or PAL.green) -- size card (#633): -/+ resize every control in the orientation on -- screen; the heading names it so it is plain the other one is untouched local sizeY = cardY + cardH + 10 * s col(PAL.card, 0.88) roundRect("fill", cardX, sizeY, cardW, cardH, 12 * s) col(PAL.stroke, 0.4) roundRect("line", cardX, sizeY, cardW, cardH, 12 * s) love.graphics.setFont(Editor.fonts.body) col(PAL.label) local orient = TouchControls.orientation == "landscape" and "Landscape" or "Portrait" love.graphics.print("Button size (" .. orient .. ")", cardX + 16 * s, sizeY + 12 * s) love.graphics.setFont(Editor.fonts.btn) col(PAL.white) love.graphics.print( string.format("%d%%", math.floor((bucket.scale or 1) * 100 + 0.5)), cardX + 16 * s, sizeY + 34 * s) local stepW = 52 * s local plus = { x = cardX + cardW - 16 * s - stepW, y = sizeY + (cardH - btnH) / 2, w = stepW, h = btnH } local minus = { x = plus.x - 10 * s - stepW, y = plus.y, w = stepW, h = btnH } Editor.rects.sizeUp, Editor.rects.sizeDown = plus, minus chromeBtn(minus, "-", { 60, 70, 110 }) chromeBtn(plus, "+", { 60, 70, 110 }) -- hint love.graphics.setFont(Editor.fonts.body) col(PAL.label, 0.9) local hint = on and "Drag each button to reposition, -/+ to resize. Portrait and landscape are saved separately when you tap Done." or "Controls are hidden in-game. Enable them to show and edit the layout." love.graphics.printf(hint, ox + pad, sizeY + cardH + 12 * s, ww - 2 * pad, "left") -- the overlay itself (preview mode; dimmed when disabled) TouchControls:draw() -- highlight the control under drag if Editor.drag then local L = TouchControls:layout() local zone = L[Editor.drag.name] if zone then love.graphics.setLineWidth(3 * s) col(PAL.green, 0.85) love.graphics.circle("line", zone.cx, zone.cy, zone.w * 0.62) end end -- pad / Joy-Con virtual cursor (after chrome so it sits on top) PadCursor.draw() end local function beginDrag(id, x, y) -- chrome takes priority over controls if inside(Editor.rects.done, x, y) then close(); return end if inside(Editor.rects.reset, x, y) then resetLayout(); return end if inside(Editor.rects.toggle, x, y) then toggleEnabled(); return end if inside(Editor.rects.sizeDown, x, y) then TouchControls:nudgeScale(-TouchControls.SCALE_STEP); return end if inside(Editor.rects.sizeUp, x, y) then TouchControls:nudgeScale(TouchControls.SCALE_STEP); return end local name = TouchControls:hitTest(x, y) if not name then return end local zone = TouchControls:layout()[name] Editor.drag = { name = name, touchId = id, offX = x - zone.cx, offY = y - zone.cy, } end local function moveDrag(id, x, y) local d = Editor.drag if not d then return end if d.touchId ~= nil and id ~= nil and d.touchId ~= id then return end TouchControls:setControlCenter(d.name, x - d.offX, y - d.offY) end local function endDrag(id) local d = Editor.drag if not d then return end if d.touchId ~= nil and id ~= nil and d.touchId ~= id then return end Editor.drag = nil end function Editor.pollHostPointers() local down = love.mouse and love.mouse.isDown and love.mouse.isDown(1) if down then local x, y = love.mouse.getPosition() if not Editor._hostMouse then Editor._hostMouse = true beginDrag("mouse", x, y) else moveDrag("mouse", x, y) end elseif Editor._hostMouse then Editor._hostMouse = false endDrag("mouse") end if not (love.touch and love.touch.getTouches and love.touch.getPosition) then return end Editor._hostTouches = Editor._hostTouches or {} local seen = {} for _, id in ipairs(love.touch.getTouches()) do seen[id] = true local ok, tx, ty = pcall(love.touch.getPosition, id) if ok and tx then if not Editor._hostTouches[id] then Editor._hostTouches[id] = true beginDrag(id, tx, ty) else moveDrag(id, tx, ty) end end end for id in pairs(Editor._hostTouches) do if not seen[id] then Editor._hostTouches[id] = nil endDrag(id) end end end function Editor.mousepressed(x, y, button) if button ~= 1 then return end -- Finger / mouse tap yields the Joy-Con pointer so the click lands where -- the event said (same NX soft-miss fix as the save editor). PadCursor.yieldToPointer() beginDrag("mouse", x, y) end function Editor.mousemoved(x, y) if love.mouse.isDown(1) then moveDrag("mouse", x, y) end end function Editor.mousereleased(x, y, button) if button ~= 1 then return end endDrag("mouse") end function Editor.touchpressed(id, x, y) PadCursor.yieldToPointer() beginDrag(id, x, y) end function Editor.touchmoved(id, x, y) moveDrag(id, x, y) end function Editor.touchreleased(id, x, y) endDrag(id) end local function handlePadAction(action) if not action then return end if action == "a" then local mx, my = PadCursor.pointer() beginDrag("pad", mx, my) elseif action == "b" then close() elseif action == "tab_prev" then TouchControls:nudgeScale(-TouchControls.SCALE_STEP) elseif action == "tab_next" then TouchControls:nudgeScale(TouchControls.SCALE_STEP) end end function Editor.gamepadpressed(joystick, button) handlePadAction(PadCursor.gamepadpressed(joystick, button)) end function Editor.gamepadreleased(joystick, button) PadCursor.gamepadreleased(joystick, button) -- A release ends a pad drag (hold A + stick to reposition a control). local action = GamepadMap.mapGamepadButton(button) if action == "a" then endDrag("pad") end end function Editor.gamepadaxis(joystick, axis, value) PadCursor.gamepadaxis(joystick, axis, value) end function Editor.joystickpressed(joystick, button) handlePadAction(PadCursor.joystickpressed(joystick, button)) end function Editor.joystickreleased(joystick, button) PadCursor.joystickreleased(joystick, button) if GamepadMap.ignoreRawForJoystick(joystick) then return end local padButton = GamepadMap.mapRawToGamepadButton(button) if padButton then local action = GamepadMap.mapGamepadButton(padButton) if action == "a" then endDrag("pad") end end end function Editor.joystickaxis(joystick, axis, value) PadCursor.joystickaxis(joystick, axis, value) end function Editor.joystickhat(joystick, hat, direction) PadCursor.joystickhat(joystick, hat, direction) end function Editor.keypressed(key) if key == "escape" or key == "return" or key == "space" then close() elseif key == "r" then resetLayout() -- desktop shortcuts for the size -/+ (POKEPORT_TOUCH=1 testing, #633) elseif key == "-" or key == "kp-" then TouchControls:nudgeScale(-TouchControls.SCALE_STEP) elseif key == "=" or key == "+" or key == "kp+" then TouchControls:nudgeScale(TouchControls.SCALE_STEP) end end function Editor.new(game) local state = { game = game, isOpaque = true } Editor.hostPoll = true Editor.load({ version = "gold", hostPoll = true, onClose = function() Editor.hostPoll = false if game.options then game.options.touchControls = TouchControls:config() end if game.stack and game.stack:top() == state then game.stack:pop() end if game.applyOptions then game:applyOptions() end end, }) function state:wantsFillScale() return true end function state:drawsWidescreen() return true end function state:update(dt) Editor.update(dt) local input = self.game and self.game.input if input and input:wasPressed("b") then close() end end function state:draw() end function state:drawWidescreen(_w, _h) Editor.draw() end return state end return Editor