mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-22 13:36:14 +02:00
+14
-1
@@ -209,9 +209,21 @@ New/Load workspace controls are deliberately not duplicated inside that editor.
|
||||
|
||||
The editor keeps its canvas unobstructed and puts the contextual actions in a
|
||||
compact lower tray: add/control binding, button and bezel artwork, pages,
|
||||
screen placement, freeform/10:9 screen shape and deletion. Touches select, drag and resize the
|
||||
screen placement, freeform/10:9 screen shape and deletion. **Screen** opens
|
||||
cutout, bezel-hole detect, **Detect this screen**, and the canvas presets.
|
||||
**Detect this screen** (also on the tray) sets the mock device to the live
|
||||
window size so a phone skin is authored at that phone's form factor rather
|
||||
than a generic 1080x1920 16:9. **Zoom −** shrinks the mock device inside the
|
||||
workspace so the screen hole can be dragged larger than the bezel while the
|
||||
handles stay grabable; **Fit** restores contain. The mouse wheel over the
|
||||
canvas, and `-` / `=` / `0` on a keyboard, do the same. Touches select, drag and resize the
|
||||
same controls that a mouse edits on desktop.
|
||||
|
||||
My Skins and the editor chrome sit inside the platform safe area (notch,
|
||||
status bar, home indicator), the same inset the launcher uses. The mock
|
||||
device still represents the full window, because a skin covers the whole
|
||||
screen at play time.
|
||||
|
||||
The launcher’s **Turn skins off** button clears the selected skin and disables
|
||||
skin use. With no skin enabled, mobile falls back to the built-in pad; that pad
|
||||
is not itself a skin card.
|
||||
@@ -222,6 +234,7 @@ phone proportions on a desktop monitor.
|
||||
| Preset | Size |
|
||||
| --- | --- |
|
||||
| Phone portrait / landscape | 1080x1920, 1920x1080 |
|
||||
| This screen | the live window, so a phone is authored at its own height |
|
||||
| Tablet portrait / landscape | 1536x2048, 2048x1536 |
|
||||
| Steam Deck | 1280x800 |
|
||||
| Desktop 1080p | 1920x1080 |
|
||||
|
||||
+326
-53
@@ -5,12 +5,14 @@ local TouchSkin = require("src.core.TouchSkin")
|
||||
local TouchControls = require("src.core.TouchControls")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local FilePicker = require("src.core.FilePicker")
|
||||
local SafeArea = require("src.core.SafeArea")
|
||||
|
||||
local Studio = {}
|
||||
|
||||
Studio.CANVASES = {
|
||||
{ id = "phone_portrait", label = "Phone portrait", w = 1080, h = 1920 },
|
||||
{ id = "phone_landscape", label = "Phone landscape", w = 1920, h = 1080 },
|
||||
{ id = "this_device", label = "This screen", live = true },
|
||||
{ id = "tablet_portrait", label = "Tablet portrait", w = 1536, h = 2048 },
|
||||
{ id = "tablet_landscape", label = "Tablet landscape", w = 2048, h = 1536 },
|
||||
{ id = "steamdeck", label = "Steam Deck", w = 1280, h = 800 },
|
||||
@@ -20,6 +22,17 @@ Studio.CANVASES = {
|
||||
lockViewport = { x = 48 / 256, y = 40 / 224, w = 160 / 256, h = 144 / 224 } },
|
||||
}
|
||||
|
||||
-- Editor view zoom: 1 is contain-fit. Smaller values shrink the mock
|
||||
-- device inside the workspace so the screen hole can be dragged past the
|
||||
-- bezel and you can still grab the handles.
|
||||
Studio.ZOOM_LEVELS = { 1, 0.75, 0.5, 0.35 }
|
||||
Studio.viewZoom = 1
|
||||
|
||||
-- Hydrated live canvas; canvas() is called many times a frame so this is
|
||||
-- mutated in place instead of allocating a new table.
|
||||
local liveCanvas = { id = "this_device", label = "This screen", live = true,
|
||||
w = 1080, h = 1920 }
|
||||
|
||||
-- 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
|
||||
@@ -100,8 +113,92 @@ end
|
||||
|
||||
local function round(v) return math.floor(v + 0.5) end
|
||||
|
||||
function Studio.deviceSize()
|
||||
if love and love.graphics and love.graphics.getDimensions then
|
||||
local w, h = love.graphics.getDimensions()
|
||||
w, h = tonumber(w), tonumber(h)
|
||||
if w and h and w > 1 and h > 1 then return w, h end
|
||||
end
|
||||
return 1080, 1920
|
||||
end
|
||||
|
||||
function Studio.canvasIndexById(id)
|
||||
for i, c in ipairs(Studio.CANVASES) do
|
||||
if c.id == id then return i end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function Studio.canvas()
|
||||
return Studio.CANVASES[Studio.canvasIndex] or Studio.CANVASES[1]
|
||||
local spec = Studio.CANVASES[Studio.canvasIndex] or Studio.CANVASES[1]
|
||||
if spec.live then
|
||||
local w, h = Studio.deviceSize()
|
||||
liveCanvas.id = spec.id
|
||||
liveCanvas.label = spec.label
|
||||
liveCanvas.w, liveCanvas.h = w, h
|
||||
liveCanvas.live = true
|
||||
liveCanvas.lockViewport = spec.lockViewport
|
||||
return liveCanvas
|
||||
end
|
||||
return spec
|
||||
end
|
||||
|
||||
function Studio.zoomIndex()
|
||||
local z = Studio.viewZoom or 1
|
||||
local best, dist = 1, math.huge
|
||||
for i, v in ipairs(Studio.ZOOM_LEVELS) do
|
||||
local d = math.abs(v - z)
|
||||
if d < dist then best, dist = i, d end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
function Studio.zoomOut()
|
||||
local i = Studio.zoomIndex()
|
||||
if i < #Studio.ZOOM_LEVELS then
|
||||
Studio.viewZoom = Studio.ZOOM_LEVELS[i + 1]
|
||||
end
|
||||
return Studio.viewZoom
|
||||
end
|
||||
|
||||
function Studio.zoomIn()
|
||||
local i = Studio.zoomIndex()
|
||||
if i > 1 then
|
||||
Studio.viewZoom = Studio.ZOOM_LEVELS[i - 1]
|
||||
end
|
||||
return Studio.viewZoom
|
||||
end
|
||||
|
||||
function Studio.zoomFit()
|
||||
Studio.viewZoom = Studio.ZOOM_LEVELS[1]
|
||||
return Studio.viewZoom
|
||||
end
|
||||
|
||||
function Studio.detectDeviceCanvas()
|
||||
local idx = Studio.canvasIndexById("this_device")
|
||||
if not idx then return nil end
|
||||
Studio.setCanvas(idx)
|
||||
local canvas = Studio.canvas()
|
||||
Studio.setStatus(("This screen: %dx%d"):format(canvas.w, canvas.h))
|
||||
return canvas
|
||||
end
|
||||
|
||||
-- Usable chrome rect. Background still fills the window so the notch
|
||||
-- band is the same colour as the rest of the studio; every button and
|
||||
-- the mock device live inside the safe area, matching the launcher.
|
||||
function Studio.safeFrame()
|
||||
local W, H = 0, 0
|
||||
if love and love.graphics and love.graphics.getDimensions then
|
||||
W, H = love.graphics.getDimensions()
|
||||
end
|
||||
W, H = math.max(1, tonumber(W) or 1), math.max(1, tonumber(H) or 1)
|
||||
local ox, oy, sw, sh = SafeArea.rect()
|
||||
ox = math.max(0, tonumber(ox) or 0)
|
||||
oy = math.max(0, tonumber(oy) or 0)
|
||||
sw = math.max(1, tonumber(sw) or W)
|
||||
sh = math.max(1, tonumber(sh) or H)
|
||||
Studio._frame = { W = W, H = H, x = ox, y = oy, w = sw, h = sh }
|
||||
return W, H, ox, oy, sw, sh
|
||||
end
|
||||
|
||||
function Studio.page()
|
||||
@@ -256,11 +353,19 @@ end
|
||||
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"
|
||||
local w, h = canvas.w, canvas.h
|
||||
if canvas.live and (not w or not h) then
|
||||
w, h = Studio.deviceSize()
|
||||
end
|
||||
if not w or not h then return nil end
|
||||
return w > h and "landscape" or "portrait"
|
||||
end
|
||||
|
||||
local function pickCanvasIndex(want)
|
||||
local cur = Studio.canvas()
|
||||
-- A live "this screen" canvas already is the window; keep it so Match
|
||||
-- canvas cannot yank a phone author back onto a generic 16:9 preset.
|
||||
if cur and cur.live then return Studio.canvasIndex end
|
||||
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)
|
||||
@@ -269,7 +374,10 @@ local function pickCanvasIndex(want)
|
||||
end
|
||||
end
|
||||
for i, c in ipairs(Studio.CANVASES) do
|
||||
if canvasOrientation(c) == want and not c.lockViewport then return i end
|
||||
if not c.live and not c.lockViewport
|
||||
and canvasOrientation(c) == want then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
@@ -368,6 +476,14 @@ function Studio.load(opts)
|
||||
Studio.drag = nil
|
||||
Studio.pendingPlay = false
|
||||
Studio.canvasIndex = 1
|
||||
Studio.viewZoom = 1
|
||||
-- On a phone the mock device should be this screen's form factor, not a
|
||||
-- generic 1080x1920 16:9, so the hole and the pad land where they will
|
||||
-- at play time.
|
||||
if Studio.isMobile() then
|
||||
local live = Studio.canvasIndexById("this_device")
|
||||
if live then Studio.canvasIndex = live end
|
||||
end
|
||||
-- A free-form screen is the default. 10:9 is an optional convenience,
|
||||
-- never a restriction on imported or custom skins.
|
||||
Studio.aspectLock = false
|
||||
@@ -1065,6 +1181,11 @@ function Studio.openPageMenu()
|
||||
return true
|
||||
end
|
||||
|
||||
function Studio.openScreenMenu()
|
||||
Studio.openModal("screen")
|
||||
return true
|
||||
end
|
||||
|
||||
function Studio.renamePage(name)
|
||||
local page = Studio.page()
|
||||
if not page then return false end
|
||||
@@ -1222,8 +1343,13 @@ end
|
||||
function Studio.canvasRect(x, y, w, h)
|
||||
local canvas = Studio.canvas()
|
||||
local aspect = canvas.w / canvas.h
|
||||
if not aspect or aspect <= 0 then aspect = 1 end
|
||||
local cw, ch = w, w / aspect
|
||||
if ch > h then ch, cw = h, h * aspect end
|
||||
local z = Studio.viewZoom or 1
|
||||
if z < 0.999 then
|
||||
cw, ch = cw * z, ch * z
|
||||
end
|
||||
return x + (w - cw) * 0.5, y + (h - ch) * 0.5, cw, ch
|
||||
end
|
||||
|
||||
@@ -1430,10 +1556,12 @@ local function drawGameTest(r, page)
|
||||
end
|
||||
|
||||
local function drawCanvas(x, y, w, h)
|
||||
Studio.canvasWorkspace = { x = x, y = y, w = w, h = h }
|
||||
local page = Studio.page()
|
||||
local r = { }
|
||||
r.x, r.y, r.w, r.h = Studio.canvasRect(x, y, w, h)
|
||||
Studio.lastCanvas = r
|
||||
Theme.fill(x, y, w, h, { 10, 12, 18 }, 1)
|
||||
if not page then return r end
|
||||
|
||||
Theme.fill(r.x, r.y, r.w, r.h, { 0, 0, 0 }, 1)
|
||||
@@ -1745,12 +1873,18 @@ function Studio.commitField(id, text)
|
||||
end
|
||||
|
||||
local function modalFrame(W, H, title, wFrac, hFrac)
|
||||
Theme.fill(0, 0, W, H, PAL.bg, 0.85)
|
||||
local ox, oy, sw, sh, winW, winH = 0, 0, W, H, W, H
|
||||
local f = Studio._frame
|
||||
if f then
|
||||
ox, oy, sw, sh = f.x, f.y, f.w, f.h
|
||||
winW, winH = f.W, f.H
|
||||
end
|
||||
Theme.fill(0, 0, winW, winH, PAL.bg, 0.85)
|
||||
local pad = 14 * Kit.scale
|
||||
local mw = math.min(W - pad * 2, math.max(320 * Kit.scale, (wFrac or 0.6) * W))
|
||||
local mh = math.min(H - pad * 2, math.max(220 * Kit.scale, (hFrac or 0.72) * H))
|
||||
local mx = math.floor((W - mw) * 0.5)
|
||||
local my = math.floor((H - mh) * 0.5)
|
||||
local mw = math.min(sw - pad * 2, math.max(320 * Kit.scale, (wFrac or 0.6) * sw))
|
||||
local mh = math.min(sh - pad * 2, math.max(220 * Kit.scale, (hFrac or 0.72) * sh))
|
||||
local mx = math.floor(ox + (sw - mw) * 0.5)
|
||||
local my = math.floor(oy + (sh - mh) * 0.5)
|
||||
Kit.card(mx, my, mw, mh)
|
||||
Kit.textBold("title", title, mx + pad, my + pad * 0.7, PAL.heading)
|
||||
return mx, my, mw, mh, pad
|
||||
@@ -2022,6 +2156,79 @@ local function drawExportModal(W, H)
|
||||
mx + pad, cy, mw - pad * 2, PAL.faint, 2)
|
||||
end
|
||||
|
||||
local function drawScreenModal(W, H)
|
||||
local modal = Studio.modal
|
||||
local mx, my, mw, mh, pad = modalFrame(W, H, "Screen & canvas", 0.62, 0.82)
|
||||
local top = my + pad + Kit.textHeight("title") + pad * 0.5
|
||||
local by, bh = modalFooter(mx, my, mw, mh, pad)
|
||||
local gap = 6 * Kit.scale
|
||||
local rowH = math.max(Kit.tapMin(), 34 * Kit.scale)
|
||||
local innerW = mw - pad * 2
|
||||
local half = (innerW - gap) * 0.5
|
||||
local page = Studio.page()
|
||||
local locked = Studio.canvas().lockViewport
|
||||
|
||||
local vpOn = page and (page.viewport or page.screenFit == "remainder")
|
||||
if Kit.button(mx + pad, top, half, rowH,
|
||||
vpOn and "Cutout: ON" or "Cutout: OFF",
|
||||
{ id = "screen-cutout", active = vpOn == true,
|
||||
enabled = not locked }) then
|
||||
Studio.toggleViewport()
|
||||
end
|
||||
if Kit.button(mx + pad + half + gap, top, half, rowH,
|
||||
Studio.aspectLock and "Shape: 10:9" or "Shape: Free",
|
||||
{ id = "screen-shape", active = Studio.aspectLock }) then
|
||||
Studio.aspectLock = not Studio.aspectLock
|
||||
end
|
||||
top = top + rowH + gap
|
||||
if Kit.button(mx + pad, top, innerW, rowH, "Detect screen from bezel",
|
||||
{ id = "screen-bezel",
|
||||
enabled = page ~= nil and page.imagePath ~= nil and not locked }) then
|
||||
Studio.detectViewport()
|
||||
end
|
||||
top = top + rowH + gap
|
||||
local live = Studio.canvas()
|
||||
local detectLabel = ("Detect this screen (%dx%d)"):format(live.w, live.h)
|
||||
if live.id ~= "this_device" then
|
||||
local w, h = Studio.deviceSize()
|
||||
detectLabel = ("Detect this screen (%dx%d)"):format(w, h)
|
||||
end
|
||||
if Kit.button(mx + pad, top, innerW, rowH, detectLabel,
|
||||
{ id = "screen-device", kind = "accent" }) then
|
||||
Studio.detectDeviceCanvas()
|
||||
end
|
||||
top = top + rowH + gap * 1.5
|
||||
Kit.caption(mx + pad, top, "CANVAS")
|
||||
top = top + Kit.textHeight("small") + gap
|
||||
|
||||
local viewH = by - top - pad
|
||||
local canvases = Studio.CANVASES
|
||||
local contentH = #canvases * (rowH + gap)
|
||||
local at = modalScroll(modal, mx + pad, top, innerW, viewH, contentH)
|
||||
local maxScroll = Kit.scrollExtent(contentH, viewH)
|
||||
local baseY = Kit.scrollBegin(mx + pad, top, innerW, viewH, at, maxScroll)
|
||||
for i, spec in ipairs(canvases) do
|
||||
local py = baseY + (i - 1) * (rowH + gap)
|
||||
local selected = i == Studio.canvasIndex
|
||||
local clicked = Kit.row(mx + pad, py, innerW, rowH, selected,
|
||||
"canvas-" .. spec.id)
|
||||
local label = spec.label
|
||||
local detail
|
||||
if spec.live then
|
||||
local w, h = Studio.deviceSize()
|
||||
detail = ("%dx%d · this window"):format(w, h)
|
||||
else
|
||||
detail = ("%dx%d"):format(spec.w, spec.h)
|
||||
end
|
||||
Kit.text("mono", Kit.ellipsize("mono", label, innerW - pad),
|
||||
mx + pad * 2, py + 4 * Kit.scale, selected and PAL.green or PAL.heading)
|
||||
Kit.text("small", Kit.ellipsize("small", detail, innerW - pad),
|
||||
mx + pad * 2, py + 4 * Kit.scale + Kit.textHeight("mono"), PAL.muted)
|
||||
if clicked then Studio.setCanvas(i) end
|
||||
end
|
||||
Kit.scrollEnd(mx + pad, top, innerW, viewH, at, maxScroll)
|
||||
end
|
||||
|
||||
local function drawConfirm(W, H)
|
||||
local c = Studio.confirm
|
||||
local mx, my, mw, mh, pad = modalFrame(W, H, "Unsaved changes", 0.42, 0.3)
|
||||
@@ -2057,21 +2264,24 @@ function Studio.drawOverlay(W, H)
|
||||
elseif modal.kind == "open" then drawOpenModal(W, H)
|
||||
elseif modal.kind == "page" then drawPageModal(W, H)
|
||||
elseif modal.kind == "export" then drawExportModal(W, H)
|
||||
elseif modal.kind == "screen" then drawScreenModal(W, H)
|
||||
else Studio.closeModal() end
|
||||
return true
|
||||
end
|
||||
|
||||
local function drawLegacyStudio()
|
||||
local W, H = love.graphics.getDimensions()
|
||||
local winW, winH, ox, oy, W, H = Studio.safeFrame()
|
||||
Kit.layout(W, H)
|
||||
local mx, my = love.mouse.getPosition()
|
||||
if Studio.pointerX ~= nil then mx, my = Studio.pointerX, Studio.pointerY end
|
||||
Kit.beginFrame(mx, my, Studio.clicked, Studio.wheel)
|
||||
Kit.beginFrame(mx - ox, my - oy, Studio.clicked, Studio.wheel)
|
||||
Studio.clicked, Studio.wheel = false, 0
|
||||
|
||||
Theme.fill(0, 0, W, H, PAL.bg, 1)
|
||||
Theme.fill(0, 0, winW, winH, PAL.bg, 1)
|
||||
Studio.expireStatus()
|
||||
Kit.blockClicks = Studio.modalUp()
|
||||
love.graphics.push()
|
||||
love.graphics.translate(ox, oy)
|
||||
|
||||
local pad = 14 * Kit.scale
|
||||
local mobile = Studio.isMobile()
|
||||
@@ -2142,6 +2352,7 @@ local function drawLegacyStudio()
|
||||
end
|
||||
if closed then
|
||||
Kit.blockClicks = false
|
||||
love.graphics.pop()
|
||||
Kit.endFrame()
|
||||
return
|
||||
end
|
||||
@@ -2183,6 +2394,16 @@ local function drawLegacyStudio()
|
||||
Studio.statusErr and PAL.red or PAL.detail)
|
||||
|
||||
Kit.blockClicks = false
|
||||
love.graphics.pop()
|
||||
if r then r.x, r.y = r.x + ox, r.y + oy end
|
||||
if Studio.lastCanvas then
|
||||
Studio.lastCanvas.x = Studio.lastCanvas.x + ox
|
||||
Studio.lastCanvas.y = Studio.lastCanvas.y + oy
|
||||
end
|
||||
if Studio.canvasWorkspace then
|
||||
Studio.canvasWorkspace.x = Studio.canvasWorkspace.x + ox
|
||||
Studio.canvasWorkspace.y = Studio.canvasWorkspace.y + oy
|
||||
end
|
||||
Studio.drawOverlay(W, H)
|
||||
|
||||
Kit.endFrame()
|
||||
@@ -2225,34 +2446,37 @@ local function drawSkinArtwork(image, x, y, w, h)
|
||||
PAL.steel, 0.8, h * 0.12)
|
||||
end
|
||||
|
||||
local function drawLibrary(W, H, pad)
|
||||
local function drawLibrary()
|
||||
local f = Studio._frame
|
||||
local ox, oy, W, H = f.x, f.y, f.w, f.h
|
||||
local pad = 14 * Kit.scale
|
||||
local btnH = math.max(Kit.tapMin(), 32 * Kit.scale)
|
||||
local titleY = pad * 0.55
|
||||
Kit.textBold("title", "My Skins", pad, titleY, PAL.heading)
|
||||
local titleY = oy + pad * 0.55
|
||||
Kit.textBold("title", "My Skins", ox + pad, titleY, PAL.heading)
|
||||
local closeW = math.min(112 * Kit.scale, W * 0.24)
|
||||
if Kit.button(W - pad - closeW, pad * 0.5, closeW, btnH, "Close",
|
||||
if Kit.button(ox + W - pad - closeW, oy + pad * 0.5, closeW, btnH, "Close",
|
||||
{ id = "library-close" }) then
|
||||
if Studio.onClose then Studio.onClose() end
|
||||
return
|
||||
end
|
||||
|
||||
local y = pad * 0.5 + btnH + pad
|
||||
local y = oy + pad * 0.5 + btnH + pad
|
||||
local gap = 8 * Kit.scale
|
||||
local half = (W - pad * 2 - gap) * 0.5
|
||||
if Kit.button(pad, y, half, btnH * 1.12, "+ New skin",
|
||||
if Kit.button(ox + pad, y, half, btnH * 1.12, "+ New skin",
|
||||
{ id = "library-new", kind = "accent" }) then
|
||||
Studio.newSkin()
|
||||
Studio.enterEditor()
|
||||
return
|
||||
end
|
||||
if Kit.button(pad + half + gap, y, half, btnH * 1.12, "Import skin",
|
||||
if Kit.button(ox + pad + half + gap, y, half, btnH * 1.12, "Import skin",
|
||||
{ id = "library-import" }) then
|
||||
Studio.importSkinFile()
|
||||
return
|
||||
end
|
||||
y = y + btnH * 1.12 + pad
|
||||
|
||||
Kit.caption(pad, y, "YOUR SKINS")
|
||||
Kit.caption(ox + pad, y, "YOUR SKINS")
|
||||
y = y + Kit.textHeight("small") + gap
|
||||
|
||||
local entries = Studio.available or {}
|
||||
@@ -2264,7 +2488,7 @@ local function drawLibrary(W, H, pad)
|
||||
local tc = type(saved.touchControls) == "table" and saved.touchControls or {}
|
||||
local activeId = tc.enabled == false and nil or tc.skin
|
||||
local total = #entries
|
||||
local maxRows = math.max(1, math.floor((H - y - pad - btnH - gap)
|
||||
local maxRows = math.max(1, math.floor((oy + H - y - pad - btnH - gap)
|
||||
/ (cardH + cardGap)))
|
||||
local perPage = maxRows * cols
|
||||
local pages = math.max(1, math.ceil(total / perPage))
|
||||
@@ -2274,7 +2498,7 @@ local function drawLibrary(W, H, pad)
|
||||
|
||||
for slot = first, last do
|
||||
local index = slot - first
|
||||
local cx = pad + (index % cols) * (cardW + cardGap)
|
||||
local cx = ox + pad + (index % cols) * (cardW + cardGap)
|
||||
local cy = y + math.floor(index / cols) * (cardH + cardGap)
|
||||
do
|
||||
local entry = entries[slot]
|
||||
@@ -2319,67 +2543,71 @@ local function drawLibrary(W, H, pad)
|
||||
end
|
||||
|
||||
if pages > 1 then
|
||||
local pagerY = H - pad - btnH
|
||||
local pagerY = oy + H - pad - btnH
|
||||
local prevW, nextW = (W - pad * 2 - gap) * 0.5, (W - pad * 2 - gap) * 0.5
|
||||
if Kit.button(pad, pagerY, prevW, btnH, "Previous",
|
||||
if Kit.button(ox + pad, pagerY, prevW, btnH, "Previous",
|
||||
{ id = "library-prev", enabled = Studio.libraryPage > 1 }) then
|
||||
Studio.libraryPage = Studio.libraryPage - 1
|
||||
end
|
||||
if Kit.button(pad + prevW + gap, pagerY, nextW, btnH, "Next",
|
||||
if Kit.button(ox + pad + prevW + gap, pagerY, nextW, btnH, "Next",
|
||||
{ id = "library-next", enabled = Studio.libraryPage < pages }) then
|
||||
Studio.libraryPage = Studio.libraryPage + 1
|
||||
end
|
||||
end
|
||||
|
||||
if #entries == 0 then
|
||||
Kit.text("small", "Create a blank skin or import one to get started.", pad,
|
||||
y + cardH + gap, PAL.muted)
|
||||
Kit.text("small", "Create a blank skin or import one to get started.",
|
||||
ox + pad, y + cardH + gap, PAL.muted)
|
||||
end
|
||||
end
|
||||
|
||||
local function drawEditor(W, H, pad)
|
||||
local function drawEditor()
|
||||
local f = Studio._frame
|
||||
local ox, oy, W, H = f.x, f.y, f.w, f.h
|
||||
local pad = 14 * Kit.scale
|
||||
local btnH = math.max(Kit.tapMin(), 32 * Kit.scale)
|
||||
local gap = 7 * Kit.scale
|
||||
local backW = math.min(120 * Kit.scale, W * 0.23)
|
||||
if Kit.button(pad, pad * 0.5, backW, btnH, "My Skins",
|
||||
if Kit.button(ox + pad, oy + pad * 0.5, backW, btnH, "My Skins",
|
||||
{ id = "editor-back" }) then
|
||||
Studio.backToLibrary()
|
||||
end
|
||||
local closeW = math.min(90 * Kit.scale, W * 0.17)
|
||||
local saveW = math.min(90 * Kit.scale, W * 0.17)
|
||||
local testW = math.min(100 * Kit.scale, W * 0.18)
|
||||
local right = W - pad
|
||||
if Kit.button(right - closeW, pad * 0.5, closeW, btnH, "Close",
|
||||
local right = ox + W - pad
|
||||
if Kit.button(right - closeW, oy + pad * 0.5, closeW, btnH, "Close",
|
||||
{ id = "editor-close" }) then
|
||||
Studio.guard("Close the studio and lose the unsaved changes?", function()
|
||||
if Studio.onClose then Studio.onClose() end
|
||||
end)
|
||||
end
|
||||
right = right - closeW - gap
|
||||
if Kit.button(right - saveW, pad * 0.5, saveW, btnH, "Save",
|
||||
if Kit.button(right - saveW, oy + pad * 0.5, saveW, btnH, "Save",
|
||||
{ id = "editor-save", kind = "accent" }) then Studio.save() end
|
||||
right = right - saveW - gap
|
||||
if Kit.button(right - testW, pad * 0.5, testW, btnH,
|
||||
if Kit.button(right - testW, oy + pad * 0.5, testW, btnH,
|
||||
Studio.testing and "Test: ON" or "Test",
|
||||
{ id = "editor-test", active = Studio.testing }) then
|
||||
Studio.testing = not Studio.testing
|
||||
TouchControls:setPreview(not Studio.testing)
|
||||
TouchControls:reset()
|
||||
end
|
||||
local titleX = pad + backW + gap
|
||||
local titleX = ox + pad + backW + gap
|
||||
local titleW = math.max(0, right - testW - gap - titleX)
|
||||
Kit.textBold("button", Kit.ellipsize("button",
|
||||
(Studio.skin and Studio.skin.name) or "Untitled skin", titleW),
|
||||
titleX, pad * 0.5 + (btnH - Kit.textHeight("button")) * 0.5, PAL.heading)
|
||||
titleX, oy + pad * 0.5 + (btnH - Kit.textHeight("button")) * 0.5, PAL.heading)
|
||||
|
||||
local trayH = btnH * 2 + gap * 3 + 30 * Kit.scale
|
||||
local bodyY = pad * 0.5 + btnH + pad
|
||||
local trayY = H - pad - trayH
|
||||
local zH = math.max(Kit.tapMin(), 28 * Kit.scale)
|
||||
local trayH = btnH * 2 + zH + gap * 4 + Kit.textHeight("small") + gap
|
||||
local bodyY = oy + pad * 0.5 + btnH + pad
|
||||
local trayY = oy + H - pad - trayH
|
||||
local canvasH = math.max(120 * Kit.scale, trayY - bodyY - pad)
|
||||
drawCanvas(pad, bodyY, W - pad * 2, canvasH)
|
||||
drawCanvas(ox + pad, bodyY, W - pad * 2, canvasH)
|
||||
|
||||
Kit.card(pad, trayY, W - pad * 2, trayH)
|
||||
local innerX, innerW = pad + gap, W - pad * 2 - gap * 2
|
||||
Kit.card(ox + pad, trayY, W - pad * 2, trayH)
|
||||
local innerX, innerW = ox + pad + gap, W - pad * 2 - gap * 2
|
||||
local actionW = (innerW - gap * 3) / 4
|
||||
local ctl = Studio.selectedControl()
|
||||
if Kit.button(innerX, trayY + gap, actionW, btnH, "+ Control",
|
||||
@@ -2401,19 +2629,41 @@ local function drawEditor(W, H, pad)
|
||||
Studio.openImagePicker("bezel")
|
||||
end
|
||||
|
||||
if Kit.button(innerX, trayY + gap * 2 + btnH, actionW, btnH, "Pages",
|
||||
local row2 = trayY + gap * 2 + btnH
|
||||
if Kit.button(innerX, row2, actionW, btnH, "Pages",
|
||||
{ id = "tray-pages" }) then Studio.openPageMenu() end
|
||||
if Kit.button(innerX + (actionW + gap), trayY + gap * 2 + btnH, actionW, btnH,
|
||||
"Screen", { id = "tray-screen" }) then Studio.toggleViewport() end
|
||||
if Kit.button(innerX + (actionW + gap) * 2, trayY + gap * 2 + btnH, actionW, btnH,
|
||||
if Kit.button(innerX + (actionW + gap), row2, actionW, btnH,
|
||||
"Screen", { id = "tray-screen" }) then Studio.openScreenMenu() end
|
||||
if Kit.button(innerX + (actionW + gap) * 2, row2, actionW, btnH,
|
||||
Studio.aspectLock and "Shape: 10:9" or "Shape: Free",
|
||||
{ id = "tray-shape", active = Studio.aspectLock }) then
|
||||
Studio.aspectLock = not Studio.aspectLock
|
||||
end
|
||||
if Kit.button(innerX + (actionW + gap) * 3, trayY + gap * 2 + btnH, actionW, btnH,
|
||||
if Kit.button(innerX + (actionW + gap) * 3, row2, actionW, btnH,
|
||||
"Delete", { id = "tray-delete", kind = "danger", enabled = ctl ~= nil }) then
|
||||
Studio.deleteControl()
|
||||
end
|
||||
|
||||
local row3 = row2 + btnH + gap
|
||||
local zoomAt = Studio.zoomIndex()
|
||||
if Kit.button(innerX, row3, actionW, zH, "Detect screen",
|
||||
{ id = "tray-detect" }) then
|
||||
Studio.detectDeviceCanvas()
|
||||
end
|
||||
if Kit.button(innerX + (actionW + gap), row3, actionW, zH, "Zoom −",
|
||||
{ id = "zoom-out", enabled = zoomAt < #Studio.ZOOM_LEVELS }) then
|
||||
Studio.zoomOut()
|
||||
end
|
||||
local pct = math.floor((Studio.viewZoom or 1) * 100 + 0.5) .. "%"
|
||||
if Kit.button(innerX + (actionW + gap) * 2, row3, actionW, zH, "Fit " .. pct,
|
||||
{ id = "zoom-fit", active = zoomAt == 1 }) then
|
||||
Studio.zoomFit()
|
||||
end
|
||||
if Kit.button(innerX + (actionW + gap) * 3, row3, actionW, zH, "Zoom +",
|
||||
{ id = "zoom-in", enabled = zoomAt > 1 }) then
|
||||
Studio.zoomIn()
|
||||
end
|
||||
|
||||
local hint = ctl and ("Selected: " .. TouchSkin.describeBind(ctl.spec)
|
||||
.. " — drag to move; use blue handles to resize.")
|
||||
or "Tap a control to select it. Drag to move; use blue handles to resize."
|
||||
@@ -2423,8 +2673,9 @@ local function drawEditor(W, H, pad)
|
||||
end
|
||||
|
||||
function Studio.draw()
|
||||
local W, H = love.graphics.getDimensions()
|
||||
Kit.layout(W, H)
|
||||
local W, H = Studio.safeFrame()
|
||||
local f = Studio._frame
|
||||
Kit.layout(f.w, f.h)
|
||||
local mx, my = love.mouse.getPosition()
|
||||
if Studio.pointerX ~= nil then mx, my = Studio.pointerX, Studio.pointerY end
|
||||
Kit.beginFrame(mx, my, Studio.clicked, Studio.wheel)
|
||||
@@ -2433,12 +2684,11 @@ function Studio.draw()
|
||||
Studio.expireStatus()
|
||||
Kit.blockClicks = Studio.modalUp()
|
||||
|
||||
local pad = 14 * Kit.scale
|
||||
if Studio.mode == "library" then drawLibrary(W, H, pad)
|
||||
else drawEditor(W, H, pad) end
|
||||
if Studio.mode == "library" then drawLibrary()
|
||||
else drawEditor() end
|
||||
|
||||
Kit.blockClicks = false
|
||||
Studio.drawOverlay(W, H)
|
||||
Studio.drawOverlay(f.w, f.h)
|
||||
Kit.endFrame()
|
||||
end
|
||||
|
||||
@@ -2467,8 +2717,12 @@ function Studio.mousepressed(x, y, button)
|
||||
return
|
||||
end
|
||||
local slop = HANDLE * 2 * Kit.scale
|
||||
if x < r.x - slop or x > r.x + r.w + slop
|
||||
or y < r.y - slop or y > r.y + r.h + slop then
|
||||
local work = Studio.canvasWorkspace
|
||||
local insideDevice = x >= r.x - slop and x <= r.x + r.w + slop
|
||||
and y >= r.y - slop and y <= r.y + r.h + slop
|
||||
local insideWork = work and x >= work.x and x <= work.x + work.w
|
||||
and y >= work.y and y <= work.y + work.h
|
||||
if not insideDevice and not insideWork then
|
||||
return
|
||||
end
|
||||
Studio.beginCanvasDrag(x, y, r)
|
||||
@@ -2517,6 +2771,19 @@ function Studio.touchreleased(id, x, y)
|
||||
end
|
||||
|
||||
function Studio.wheelmoved(_, dy)
|
||||
if Studio.mode == "editor" and not Studio.modalUp() and dy and dy ~= 0 then
|
||||
local work = Studio.canvasWorkspace
|
||||
local mx, my = Studio.pointerX, Studio.pointerY
|
||||
if (not mx or not my) and love and love.mouse and love.mouse.getPosition then
|
||||
mx, my = love.mouse.getPosition()
|
||||
end
|
||||
if work and mx and my
|
||||
and mx >= work.x and mx <= work.x + work.w
|
||||
and my >= work.y and my <= work.y + work.h then
|
||||
if dy > 0 then Studio.zoomIn() else Studio.zoomOut() end
|
||||
return
|
||||
end
|
||||
end
|
||||
Studio.wheel = dy
|
||||
end
|
||||
|
||||
@@ -2594,6 +2861,12 @@ function Studio.keypressed(key)
|
||||
Studio.testing = not Studio.testing
|
||||
TouchControls:setPreview(not Studio.testing)
|
||||
TouchControls:reset()
|
||||
elseif key == "-" or key == "kp-" then
|
||||
Studio.zoomOut()
|
||||
elseif key == "=" or key == "+" or key == "kp+" then
|
||||
Studio.zoomIn()
|
||||
elseif key == "0" or key == "kp0" then
|
||||
Studio.zoomFit()
|
||||
elseif key == "s" then
|
||||
Studio.save()
|
||||
elseif key == "tab" then
|
||||
|
||||
@@ -58,6 +58,19 @@ return function(game)
|
||||
U.log("selected:", ctl and ctl.spec, "x", ctl and ctl.x, "y", ctl and ctl.y)
|
||||
shot("studio_selected.png")
|
||||
|
||||
Studio.zoomOut()
|
||||
U.wait(2)
|
||||
shot("studio_zoom_out.png")
|
||||
Studio.zoomFit()
|
||||
|
||||
Studio.openScreenMenu()
|
||||
U.wait(2)
|
||||
shot("studio_screen.png")
|
||||
Studio.closeModal()
|
||||
Studio.detectDeviceCanvas()
|
||||
U.wait(2)
|
||||
shot("studio_this_screen.png")
|
||||
|
||||
-- drag the selected control and confirm the model moved
|
||||
local r = Studio.lastCanvas
|
||||
if r and ctl then
|
||||
|
||||
@@ -23,6 +23,7 @@ local function session()
|
||||
Studio.pageIndex = 1
|
||||
Studio.selected = nil
|
||||
Studio.canvasIndex = 1
|
||||
Studio.viewZoom = 1
|
||||
Studio.aspectLock = true
|
||||
Studio.drag = nil
|
||||
Studio.dirty = false
|
||||
@@ -73,11 +74,16 @@ eq(Studio.selected, nil, "and clears the selection")
|
||||
|
||||
local ids = {}
|
||||
for _, c in ipairs(Studio.CANVASES) do
|
||||
if c.live then
|
||||
check(c.id == "this_device", "the live preset is this screen")
|
||||
else
|
||||
check(c.w > 0 and c.h > 0, c.id .. " has real pixel dimensions")
|
||||
end
|
||||
check(not ids[c.id], c.id .. " appears once")
|
||||
ids[c.id] = true
|
||||
end
|
||||
check(ids.phone_portrait, "a phone portrait preset exists")
|
||||
check(ids.this_device, "a this-screen live preset exists")
|
||||
check(ids.desktop_1080, "a desktop preset exists")
|
||||
check(ids.ultrawide, "an ultrawide preset exists")
|
||||
check(ids.sgb_border, "a Super Game Boy border preset exists")
|
||||
@@ -105,9 +111,8 @@ check(Studio.page().viewport ~= nil, "the SGB cutout cannot be toggled off")
|
||||
Studio.setCanvas(#Studio.CANVASES + 1)
|
||||
eq(Studio.canvasIndex, 1, "canvas selection wraps")
|
||||
|
||||
-- ------------------------------------------------------------ canvas fit
|
||||
|
||||
Studio.setCanvas(1)
|
||||
Studio.viewZoom = 1
|
||||
local cx, cy, cw, ch = Studio.canvasRect(0, 0, 800, 600)
|
||||
near(cw / ch, Studio.canvas().w / Studio.canvas().h, 1e-6,
|
||||
"the mock device keeps its aspect")
|
||||
@@ -115,6 +120,35 @@ check(cw <= 800 + 1e-6 and ch <= 600 + 1e-6, "and fits inside the workspace")
|
||||
near(cx + cw / 2, 400, 1e-6, "centred horizontally")
|
||||
near(cy + ch / 2, 300, 1e-6, "centred vertically")
|
||||
|
||||
eq(Studio.zoomOut(), 0.75, "zoom out steps to 75%")
|
||||
local zx, zy, zw, zh = Studio.canvasRect(0, 0, 800, 600)
|
||||
near(zw, cw * 0.75, 1e-6, "zoom out shrinks the mock device")
|
||||
near(zh, ch * 0.75, 1e-6, "on both axes")
|
||||
near(zx + zw / 2, 400, 1e-6, "and stays centred")
|
||||
near(zw / zh, cw / ch, 1e-6, "keeping the same aspect")
|
||||
check(zw < cw, "so there is margin around the device for a larger screen hole")
|
||||
eq(Studio.zoomIn(), 1, "zoom in restores fit")
|
||||
eq(Studio.zoomOut(), 0.75)
|
||||
eq(Studio.zoomOut(), 0.5)
|
||||
eq(Studio.zoomOut(), 0.35)
|
||||
eq(Studio.zoomOut(), 0.35, "zoom out stops at the smallest level")
|
||||
eq(Studio.zoomFit(), 1, "fit snaps back to contain")
|
||||
|
||||
local oldDims = love.graphics.getDimensions
|
||||
love.graphics.getDimensions = function() return 1170, 2532 end
|
||||
session()
|
||||
local live = Studio.detectDeviceCanvas()
|
||||
eq(live.id, "this_device", "detect this screen selects the live canvas")
|
||||
eq(live.w, 1170, "at the window width")
|
||||
eq(live.h, 2532, "and the window height")
|
||||
eq(Studio.canvas().w / Studio.canvas().h, 1170 / 2532,
|
||||
"so the mock device is this screen's form factor")
|
||||
Studio.matchOrient = true
|
||||
Studio.syncCanvasToPage()
|
||||
eq(Studio.canvas().id, "this_device",
|
||||
"Match canvas leaves the live screen selected")
|
||||
love.graphics.getDimensions = oldDims
|
||||
|
||||
-- ----------------------------------------------------------- touch bridge
|
||||
|
||||
session()
|
||||
|
||||
@@ -18,6 +18,7 @@ local function session()
|
||||
Studio.pageIndex = 1
|
||||
Studio.selected = nil
|
||||
Studio.canvasIndex = 1
|
||||
Studio.viewZoom = 1
|
||||
Studio.aspectLock = true
|
||||
Studio.drag = nil
|
||||
Studio.dirty = false
|
||||
@@ -325,11 +326,52 @@ love.graphics.getDimensions = love.graphics.getDimensions
|
||||
or function() return 1280, 720 end
|
||||
session()
|
||||
Studio.addControl()
|
||||
for _, kind in ipairs({ "bind", "image", "open", "page", "export" }) do
|
||||
for _, kind in ipairs({ "bind", "image", "open", "page", "export", "screen" }) do
|
||||
Studio.openModal(kind)
|
||||
check(pcall(Studio.draw), "the studio draws with the " .. kind .. " modal up")
|
||||
end
|
||||
Studio.closeModal()
|
||||
check(Studio.openScreenMenu(), "Screen opens the canvas/screen modal")
|
||||
eq(Studio.modal.kind, "screen", "as the screen modal")
|
||||
Studio.closeModal()
|
||||
|
||||
-- chrome stays inside the platform safe area (notch / home indicator)
|
||||
local Kit = require("src.ui.kit.Kit")
|
||||
local oldDims = love.graphics.getDimensions
|
||||
local oldSafe = love.window.getSafeArea
|
||||
love.graphics.getDimensions = function() return 400, 800 end
|
||||
love.window.getSafeArea = function() return 12, 48, 376, 704 end
|
||||
session()
|
||||
Studio.mode = "library"
|
||||
Studio.available = {}
|
||||
Kit.audit = {}
|
||||
check(pcall(Studio.draw), "My Skins draws inside an inset safe area")
|
||||
local function insideSafe(rects, ox, oy, sw, sh, where)
|
||||
for _, r in ipairs(rects or {}) do
|
||||
check(r.x >= ox - 0.5,
|
||||
where .. " control '" .. r.label .. "' is right of the left inset")
|
||||
check(r.y >= oy - 0.5,
|
||||
where .. " control '" .. r.label .. "' is below the notch")
|
||||
check(r.x + r.w <= ox + sw + 0.5,
|
||||
where .. " control '" .. r.label .. "' stays left of the right inset")
|
||||
check(r.y + r.h <= oy + sh + 0.5,
|
||||
where .. " control '" .. r.label .. "' stays above the home indicator")
|
||||
end
|
||||
end
|
||||
insideSafe(Kit.audit, 12, 48, 376, 704, "library")
|
||||
Studio.mode = "editor"
|
||||
Studio.addControl()
|
||||
Kit.audit = {}
|
||||
check(pcall(Studio.draw), "the editor draws inside an inset safe area")
|
||||
insideSafe(Kit.audit, 12, 48, 376, 704, "editor")
|
||||
check(Studio.lastCanvas ~= nil, "the editor still has a mock device")
|
||||
check(Studio.lastCanvas.y >= 48 - 0.5, "the mock device is below the notch")
|
||||
check(Studio.lastCanvas.y + Studio.lastCanvas.h <= 48 + 704 + 0.5,
|
||||
"and above the home indicator")
|
||||
Kit.audit = nil
|
||||
love.graphics.getDimensions = oldDims
|
||||
love.window.getSafeArea = oldSafe
|
||||
Studio.closeModal()
|
||||
Studio.ask("sure?", function() end)
|
||||
check(pcall(Studio.draw), "and with the confirm prompt up")
|
||||
Studio.confirmNo()
|
||||
|
||||
Reference in New Issue
Block a user