chroma_solstice/level_editor/editor.lua
Your Name 5b7ea9ec20 Rebuild level editor sidebar with a fixed virtual-space layout
Replace the fraction-of-window positioning (which overlapped and didn't
scale) with a small layout system: the sidebar is authored in one fixed
virtual coordinate space and rendered at native panel resolution, so
element sizes stay constant relative to the panel at any window size and
fonts scale with it.

- level_editor/layout.lua: sections, scrolling list (with clip + hit-test
  by point), scrollbar. Clip rects are scaled to real canvas pixels.
- Room view split into non-overlapping divs: search/back header, a
  scrollable asset list (replaces the old row-paging + forward buttons),
  a status strip, and a compact bottom control cluster.
- Mouse wheel scrolls the list; clicks map from window coords into the
  virtual space via the same transform used to render.
- Sidebar canvas is panel-sized and blitted 1:1 (nearest filter, never
  averaged) to stay crisp; editorDraw runs under a scale transform.
- Center text-only fields (level name) in their rect.

Also carried in main.lua (share the file, can't split cleanly here):
narrator require path fixed via love.filesystem.setRequirePath; sfxr
static uses waveform=NOISE again after the field rename; low-pass +
lower volume on the static loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 12:24:54 -04:00

849 lines
26 KiB
Lua

require "level_editor/object_attributes"
require "level_editor/gui"
require "level_editor/layout"
editorView = "room" --can be "off", "room", "world", "asset"
selectedColor = "red"
local bacSprite = love.graphics.newImage("art/_ui/bac.png")
local folderSprite = love.graphics.newImage("art/_ui/folder.png")
local cancelSprite = love.graphics.newImage("art/_ui/cancel.png")
local confirmSprite = love.graphics.newImage("art/_ui/confirm.png")
local clearSavesSprite = love.graphics.newImage("art/_ui/clear_saves.png")
local selectedFolder = nil
local selectedAsset = nil
local selectedRoom = nil
local gameAssets = {}
local inputText = ""
local assetProperties = {}
local hierarchy = {}
local activeField = nil --this is for whenwe are doing text input things
local uiButtons = {}
local folderButtons = {}
local assetListButtons = {} -- flat list of the selected folder's assets (scrolls)
local listScroll = { scroll = 0 } -- shared vertical scroll for the room list
local editingAssetButtons = {}
local labels = {}
local editorRoomName = nil
local roomNameField = nil
local editorStatus = nil
local clearSaveConfirmationExpires = 0
local clearSaveConfirmationScope = nil
local worldSeedConfirmationExpires = 0
local worldMode = "place"
local worldRoomButtons = {}
local worldControls = {}
local worldPage = 1
local worldPageSize = 5
local assetSearchText = ""
local assetSearchButtons = {}
local worldSearchText = ""
local worldSearchBox = { x = 10, y = 34, w = 0, h = 28 }
-- ── Room-view sidebar layout ────────────────────────────────────────────────
-- Declared up here (not next to editorDraw) so editorMouseHandler, defined
-- earlier in the file, can still capture these as upvalues.
-- Which flat list the room view is currently showing (search > folder > roots).
local function currentRoomList()
if assetSearchText ~= "" then return assetSearchButtons
elseif not selectedFolder then return folderButtons
else return assetListButtons end
end
-- Carve the sidebar into fixed, non-overlapping "divs": a search header pinned
-- to the top, a control cluster pinned to the bottom, a status strip above it,
-- and a flexible list filling whatever is left. Recomputed every frame so it
-- tracks window resizes.
local function roomSections()
local pad = 10
local w = math.max(0, sb.w - pad * 2)
local controlsH = 104
local statusH = 44
local search = { x = pad, y = pad, w = w, h = 34 }
local controls = { x = pad, y = sb.h - pad - controlsH, w = w, h = controlsH }
local status = { x = pad, y = controls.y - statusH, w = w, h = statusH }
local listY = search.y + search.h + 8
local list = { x = pad, y = listY, w = w, h = math.max(0, status.y - 8 - listY) }
return { search = search, listRect = list, status = status, controls = controls }
end
-- Position the bottom control cluster inside its section: level-name field, a
-- row of colour buttons, then a row of action buttons. Runs for both drawing
-- and hit-testing so the two never disagree.
local function layoutRoomControls(c, s)
local vgap = 4 -- vertical space between rows
local hgap = 8 -- horizontal space between buttons in a row
local y = c.y
s.labelY = y
y = y + 15
local field = uiButtons.roomName
if field then field.x = c.x; field.y = y; field.w = c.w; field.h = 22 end
y = y + 22 + vgap
-- Resize a sprite button to a target height in place, so all control sizing
-- lives here instead of being scattered across the creation calls.
local function sizeBtn(name, target)
local b = uiButtons[name]
if b and b.sprite then
b.scale = target / b.sprite:getHeight()
b.w = b.sprite:getWidth() * b.scale
b.h = b.sprite:getHeight() * b.scale
end
return b
end
-- Pack the row centered with a fixed gap, rather than spreading each button
-- across a full-width slot (which left big gaps between small icons).
local function placeRow(names, target)
local btns, total = {}, 0
for _, name in ipairs(names) do
local b = sizeBtn(name, target)
if b then table.insert(btns, b); total = total + b.w end
end
total = total + hgap * math.max(0, #btns - 1)
local x = c.x + (c.w - total) / 2
for _, b in ipairs(btns) do
b.x = x
b.y = y + (target - b.h) / 2
x = x + b.w + hgap
end
y = y + target + vgap
end
placeRow({ "white", "red", "green", "blue" }, 22)
placeRow({ "save", "clearSaves", "world" }, 28)
end
-- Build the full room-view layout (sections + a scroll list + a back/search
-- header) without drawing. Shared by editorDraw and editorMouseHandler.
local function buildRoomLayout()
local s = roomSections()
local showBack = assetSearchText ~= "" or selectedFolder
local sx, sw = s.search.x, s.search.w
if showBack then
s.back = { x = s.search.x, y = s.search.y, w = s.search.h, h = s.search.h }
sx = s.back.x + s.back.w + 6
sw = s.search.w - s.back.w - 6
end
s.searchBox = { x = sx, y = s.search.y, w = sw, h = s.search.h }
s.items = currentRoomList()
s.list = Layout.list(listScroll, s.listRect, 40, #s.items, 2)
layoutRoomControls(s.controls, s)
return s
end
-- Mouse wheel scrolls the room list; clamped by Layout.list next frame.
function editorScrollRoomList(dy)
listScroll.scroll = math.max(0, (listScroll.scroll or 0) - dy * 40)
end
-- ────────────────────────────────────────────────────────────────────────────
local function fuzzyMatches(value, query)
query = (query or ""):lower():gsub("%s+", "")
if query == "" then return true end
local index = 1
for character in value:lower():gmatch(".") do
if character == query:sub(index, index) then
index = index + 1
if index > #query then return true end
end
end
return false
end
local function beginSearch(field)
activeField = field
inputText = field == "assetSearch" and assetSearchText or worldSearchText
response = inputText
end
local function rebuildAssetSearchButtons()
assetSearchButtons = {}
listScroll.scroll = 0
if assetSearchText == "" then return end
local names = {}
for asset in pairs(gameAssets) do
if fuzzyMatches(asset, assetSearchText) then table.insert(names, asset) end
end
table.sort(names)
for index, asset in ipairs(names) do
if index > 8 then break end
local sprites = {}
for color, location in pairs(gameAssets[asset].sprites) do sprites[color] = love.graphics.newImage(location) end
table.insert(assetSearchButtons, createUIElement{
sprites = sprites,
name = "asset",
text = asset,
x = 30,
y = 45 + (index - 1) * 62,
onClic = { left = function() selectAsset(asset) end }
})
end
end
local function buttonContains(button, x, y)
return x > button.x and x < button.x + button.w and y > button.y and y < button.y + button.h
end
local function rebuildWorldRoomButtons()
worldRoomButtons = {}
local names = {}
for _, name in ipairs(gameWorld:listRoomNames()) do
if fuzzyMatches(name, worldSearchText) then table.insert(names, name) end
end
local pages = math.max(1, math.ceil(#names / worldPageSize))
worldPage = constrain(worldPage, 1, pages)
local first = (worldPage - 1) * worldPageSize + 1
for index = first, math.min(first + worldPageSize - 1, #names) do
local roomName = names[index]
local row = index - first
table.insert(worldRoomButtons, { name = roomName, x = 10, y = 70 + row * 68, w = sb.w - 20, h = 60 })
end
worldControls = {
{ text = worldMode == "place" and "MODE: PLACE ROOMS" or "MODE: OPEN LEVELS", x = 10, y = sb.h - 194, w = sb.w - 20, h = 34,
onClic = function() worldMode = worldMode == "place" and "jump" or "place"; rebuildWorldRoomButtons() end },
{ text = "RESEED OLD EXITS", x = 10, y = sb.h - 152, w = sb.w - 20, h = 34,
onClic = function()
local now = love.timer.getTime()
if now > worldSeedConfirmationExpires then
worldSeedConfirmationExpires = now + 3
editorStatus = "Click RESEED OLD EXITS again within 3 seconds to replace the current map."
return
end
worldSeedConfirmationExpires = 0
local result = seedWorldFromLegacyExits(gameWorld)
editorStatus = "Seeded " .. result.placed .. " rooms; " .. result.links .. " legacy links (" .. result.conflicts .. " conflicts kept separate)."
rebuildWorldRoomButtons()
end },
{ text = "< PAGE " .. worldPage .. "/" .. pages .. " >", x = 10, y = sb.h - 110, w = sb.w - 20, h = 34,
onClic = function(buttonX)
if buttonX < sb.w / 2 then worldPage = math.max(1, worldPage - 1) else worldPage = math.min(pages, worldPage + 1) end
rebuildWorldRoomButtons()
end },
}
end
local function drawWorldSidebar()
love.graphics.setColor(gColor.white:set())
love.graphics.printf("ROOMS — " .. (worldMode == "place" and "select to place; re-click to clear" or "click a map room to open"), 8, 6, sb.w - 16, "center")
worldSearchBox.w = sb.w - 20
love.graphics.rectangle("line", worldSearchBox.x, worldSearchBox.y, worldSearchBox.w, worldSearchBox.h)
love.graphics.printf("SEARCH: " .. worldSearchText, worldSearchBox.x + 6, worldSearchBox.y + 7, worldSearchBox.w - 12, "left")
for _, button in ipairs(worldRoomButtons) do
love.graphics.setColor(button.name == selectedRoom and 1 or 100 / 255, button.name == selectedRoom and 1 or 100 / 255, button.name == selectedRoom and 1 or 100 / 255, 1)
love.graphics.rectangle("line", button.x, button.y, button.w, button.h)
gameWorld:drawRoomPreview(button.name, button.x + 4, button.y + 4, button.h - 8)
love.graphics.setColor(gColor.white:set())
love.graphics.printf(button.name, button.x + button.h + 4, button.y + 21, button.w - button.h - 8, "left")
end
for _, control in ipairs(worldControls) do
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("line", control.x, control.y, control.w, control.h)
love.graphics.printf(control.text, control.x, control.y + 9, control.w, "center")
end
-- Pin the assets/world toggle to a fixed spot here; the room view owns its
-- position otherwise, so give the world view a stable one of its own.
uiButtons.world.x = sb.w / 2 - uiButtons.world.w / 2
uiButtons.world.y = sb.h - uiButtons.world.h - 12
uiButtons.world:draw()
end
local function validRoomName(name)
name = (name or ""):match("^%s*(.-)%s*$")
if name == "" then return nil, "Enter a level name." end
if not name:match("^[%w][%w_-]*$") then
return nil, "Use letters, numbers, _ or - for level names."
end
return name
end
local function beginRoomNameEdit()
activeField = "roomName"
inputText = ""
response = ""
roomNameField:editText("")
editorStatus = "Type a name, press Enter, then Save."
end
local function saveRoomFromEditor()
local requestedName = editorRoomName or currentRoom.name
if activeField == "roomName" then requestedName = inputText end
local name, err = validRoomName(requestedName)
if not name then
editorStatus = err
return
end
local oldName = currentRoom.name
if name ~= oldName and readFromSource("rooms/" .. name .. ".sav") then
editorStatus = "A source level named '" .. name .. "' already exists."
return
end
local savePlayerPositions = love.keyboard.isDown("lshift", "rshift")
if not currentRoom:saveToSource(name, savePlayerPositions, oldName) then
editorStatus = "Could not save '" .. name .. "'."
return
end
-- A changed name is a rename, not an accidental duplicate. Do this only
-- after the new source file is safely written.
if oldName and oldName ~= name then
removeFromSource("rooms/" .. oldName .. ".sav")
gameWorld:renameRoom(oldName, name)
end
gameWorld:invalidatePreview(name)
if editorView == "world" then rebuildWorldRoomButtons() end
currentRoom.name = name
editorRoomName = name
roomNameField:editText(name)
activeField = nil
editorStatus = "Saved source level '" .. name .. "'" .. (savePlayerPositions and " with player positions." or ".")
end
local function clearRuntimeSaves()
local now = love.timer.getTime()
local clearAll = love.keyboard.isDown("lshift", "rshift")
local roomName = currentRoom.name
local scope = clearAll and "all" or roomName
if now > clearSaveConfirmationExpires or clearSaveConfirmationScope ~= scope then
clearSaveConfirmationExpires = now + 3
clearSaveConfirmationScope = scope
if clearAll then
editorStatus = "Shift-click the clear-save icon again within 3 seconds to erase runtime saves for every room."
else
editorStatus = "Click the clear-save icon again within 3 seconds to erase the runtime save for '" .. (roomName or "this room") .. "'."
end
return
end
local removed = clearRuntimeSaveData(clearAll and nil or roomName)
clearSaveConfirmationExpires = 0
clearSaveConfirmationScope = nil
if roomName then
currentRoom = Room:new{ name = roomName }
setEditorRoomName(currentRoom.name)
editorStatus = "Cleared " .. removed .. " runtime save item(s) and reloaded '" .. roomName .. ".'"
else
editorStatus = "Cleared " .. removed .. " runtime save item(s). No named level to reload."
end
end
function _initLevelEditor()
gameAssets = getAllAssets()
editorRoomName = ""
--create folder buttons
local counter = 1
for asset, attributes in pairs(gameAssets) do
if not hierarchy[gameAssets[asset].directory] then
hierarchy[gameAssets[asset].directory] = {}
end
table.insert(hierarchy[gameAssets[asset].directory], asset)
end
local folders = {}
for folder in pairs(hierarchy) do table.insert(folders, folder) end
table.sort(folders)
for _, folder in ipairs(folders) do
local button = createUIElement{
name = "folder",
text = folder,
x = 30,
y = (sb.h - 50) * (counter / 15),
sprite = folderSprite,
onClic = {
left = function(self)
print("my asse")
selectFolder(folder)
end,
right = function()
end
},
}
counter = counter + 1
table.insert(folderButtons, button)
end
_addUIButton(createUIElement{
name = "save",
sprite = love.graphics.newImage("art/_ui/save.png"),
onClic = {
left = function()
if editorView == "room" then
saveRoomFromEditor()
end
end,
right = function()
end
},
x = sb.w / 4,
y = sb.h * (9 / 10)
}, "save")
_addUIButton(createUIElement{
name = "world",
sprite = love.graphics.newImage("art/_ui/world.png"),
onClic = {
left = function()
if editorView == "world" then assetsView() else worldView() end
end,
right = function()
end
},
x = sb.w * (3 / 4),
y = sb.h * (9 / 10)
}, "world")
_addUIButton(createUIElement{
name = "clearSaves",
sprite = clearSavesSprite,
onClic = {
left = clearRuntimeSaves,
},
x = sb.w / 2,
y = sb.h * (9 / 10),
scale = editorScale / 3
}, "clearSaves")
for _, color in ipairs({"white", "red", "green", "blue"}) do
_addUIButton(createUIElement{
name = color,
sprite = love.graphics.newImage("art/_ui/" .. color .. ".png"),
onClic = {
left = function(self)
if editorView == "room" then
setSelectedColor(color)
end
end,
right = function()
end
},
x = sb.w * (_ / 5),
y = sb.h * (8 / 10)
}, color)
end
roomNameField = createUIElement{
name = "roomName",
text = editorRoomName,
x = 25,
y = sb.h * (7 / 10),
onClic = {
left = beginRoomNameEdit,
}
}
_addUIButton(roomNameField, "roomName")
end
function setEditorRoomName(name)
editorRoomName = name or ""
if roomNameField then roomNameField:editText(editorRoomName) end
end
function setSelectedColor(color)
selectedColor = color
end
function selectFolder(folder)
selectedFolder = folder
listScroll.scroll = 0
assetListButtons = {}
table.sort(hierarchy[selectedFolder])
for _, asset in ipairs(hierarchy[selectedFolder]) do
local sprites = {}
for color, location in pairs(gameAssets[asset].sprites) do
sprites[color] = love.graphics.newImage(location)
end
-- x/y are placeholders; the room-view layout repositions every item
-- each frame from the list section (see buildRoomLayout).
table.insert(assetListButtons, createUIElement{
sprites = sprites,
name = "asset",
text = asset,
x = 30,
y = 0,
onClic = {
left = function() selectAsset(asset) end,
right = function() editAsset(asset) end,
}
})
end
end
function selectAsset(asset)
print("selecting", asset)
selectedAsset = asset
print(selectedAsset)
end
function editAsset(asset)
inputText = ""
editorView = "asset"
selectedAsset = asset
assetProperties = gameAssets[selectedAsset]
local cancelButton = createUIElement{
name = "cancel",
sprite = cancelSprite,
x = sb.w / 3,
y = sb.h - 75,
onClic = {
left = function(self)
assetsView()
end,
right = function(self) return end
}
}
local confirmButton = createUIElement{
name = "confirm",
sprite = confirmSprite,
x = sb.w * 2 / 3,
y = sb.h - 75,
onClic = {
left = function(self)
saveAsset()
end,
right = function(self) return end
}
}
editingAssetButtons[cancelButton] = cancelButton
editingAssetButtons[confirmButton] = confirmButton
local assetSprites = {}
for color, path in pairs(assetProperties.sprites) do
assetSprites[color] = love.graphics.newImage(path)
end
local assetButton = createUIElement{
name = asset,
sprites = assetSprites,
x = sb.w / 2 - 18,
y = 80,
}
local classFieldLabel = {
draw = function() love.graphics.printf("CLASS", 0, 125, sb.w, "center") end
}
labels[classFieldLabel] = classFieldLabel
local classButton = createUIElement{
name = "class",
text = assetProperties.class or "",
x = 25,
y = 150,
onClic = {
left = function()
activeField = "class"
end
}
}
local preloadLabel = {
draw = function() love.graphics.printf("preload", 0, 225, sb.w, "center") end
}
labels[preloadLabel] = preloadLabel
local preloadButton = createUIElement{
name = "preload",
text = tostring(assetProperties.preload),
x = 25,
y = 250,
onClic = {
left = function()
assetProperties.preload = not assetProperties.preload
end
}
}
editingAssetButtons[assetButton] = assetButton
editingAssetButtons.class = classButton
editingAssetButtons.preload = preloadButton
end
function saveAsset()
gameAssets[selectedAsset] = assetProperties
-- the asset's class lives in its .meta next to the art (source of truth,
-- read back by getAllAssets); write it there
local metaPath = "art/" .. assetProperties.directory .. "/" .. selectedAsset .. ".meta"
writeToSource(metaPath, TSerial.pack({ class = assetProperties.class }, nil, true))
assetsView()
end
function _addUIButton(button, name)
local index = name or button
uiButtons[index] = button
end
function folderView()
selectedFolder = nil
assetListButtons = {}
listScroll.scroll = 0
labels = {}
end
function assetsView()
editingAssetButtons = {}
editorView = "room"
labels = {}
end
function worldView()
editorView = "world"
activeField = nil
selectedFolder = nil
worldMode = "place"
worldPage = 1
rebuildWorldRoomButtons()
end
local function newWorldRoomName(cellX, cellY)
local base = string.format("room_%02d_%02d", cellX, cellY)
local name, suffix = base, 2
while gameWorld:roomLocation(name) or readFromSource("rooms/" .. name .. ".sav") do
name = base .. "_" .. suffix
suffix = suffix + 1
end
return name
end
local function openWorldRoom(cellX, cellY)
local name = gameWorld:roomAt(cellX, cellY)
if not name then
name = newWorldRoomName(cellX, cellY)
currentRoom = Room:new{ name = name }
-- Creating from an empty cell is a real level creation, not merely a
-- transient map reference, so it immediately joins the room picker.
currentRoom:saveToSource(name)
gameWorld:placeRoom(name, cellX, cellY)
gameWorld:invalidatePreview(name)
editorStatus = "Created and opened '" .. name .. ".'"
else
currentRoom = Room:new{ name = name }
editorStatus = "Editing '" .. name .. ".'"
end
gameWorld:setLastRoom(currentRoom.name)
setEditorRoomName(currentRoom.name)
assetsView()
end
function editorMouseHandler(x, y, button)
if x > width then
-- map from real window coords into the sidebar's fixed virtual space
-- (same transform used to scale the sidebar canvas up in love.draw)
x = (x - width) * sb.w / sideBarBox.w
y = y * sb.h / sideBarBox.h
if editorView == "room" then
local s = buildRoomLayout()
-- back button: drop the search, else leave the folder
if s.back and Layout.hit(s.back, x, y) then
if assetSearchText ~= "" then
assetSearchText = ""
inputText = ""
activeField = nil
rebuildAssetSearchButtons()
elseif selectedFolder then
folderView()
end
return
end
if Layout.hit(s.searchBox, x, y) then
beginSearch("assetSearch")
return
end
-- scrolling list item
local i = s.list.at(x, y)
if i and s.items[i] then
s.items[i]:onClic(button)
return
end
-- control cluster (only the buttons that live there)
for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "save", "clearSaves", "world" }) do
local b = uiButtons[name]
if b and b:clicIsInBox(x, y) then
b:onClic(button)
return
end
end
elseif editorView == "asset" then
for _, uiElement in pairs(editingAssetButtons) do
if uiElement:clicIsInBox(x, y) then
uiElement:onClic(button)
end
end
elseif editorView == "world" then
if buttonContains(worldSearchBox, x, y) then
beginSearch("worldSearch")
return
end
for _, roomButton in ipairs(worldRoomButtons) do
if buttonContains(roomButton, x, y) then
if selectedRoom == roomButton.name then
selectedRoom = nil
editorStatus = "Room brush cleared. Click the map to open or create a level."
else
selectedRoom = roomButton.name
editorStatus = "Selected '" .. selectedRoom .. "' for placement. Click it again to clear."
end
return
end
end
for _, control in ipairs(worldControls) do
if buttonContains(control, x, y) then
control.onClic(x)
return
end
end
if uiButtons.world:clicIsInBox(x, y) then uiButtons.world:onClic(button) end
end
else
-- click landed on the grid (game area): place the selected asset
if editorView == "room" and button == 1 and selectedAsset then
currentRoom:attemptPlace(selectedAsset, gameAssets[selectedAsset], x, y, selectedColor)
elseif editorView == "world" then
local cellX, cellY = gameWorld:getCellFromMousePos(x, y)
if not gameWorld:isInBounds(cellX, cellY) then return end
if button == 2 then
local removed, name = gameWorld:removeRoom(cellX, cellY)
if removed then editorStatus = "Removed '" .. name .. "' from the world." end
elseif worldMode == "jump" or not selectedRoom then
openWorldRoom(cellX, cellY)
elseif selectedRoom then
gameWorld:placeRoom(selectedRoom, cellX, cellY)
editorStatus = "Placed '" .. selectedRoom .. "' at " .. cellX .. ", " .. cellY .. "."
end
end
end
end
function editorTextHandler(text)
if editorView == "asset" and activeField then
inputText = text
editingAssetButtons[activeField]:editText(text)
elseif editorView == "room" and activeField == "roomName" then
inputText = text
roomNameField:editText(text)
elseif editorView == "room" and activeField == "assetSearch" then
inputText = text
assetSearchText = text
rebuildAssetSearchButtons()
elseif editorView == "world" and activeField == "worldSearch" then
inputText = text
worldSearchText = text
worldPage = 1
rebuildWorldRoomButtons()
end
end
function editorCompleteResponse()
if editorView == "asset" and activeField then
assetProperties[activeField] = inputText
elseif editorView == "room" and activeField == "roomName" then
editorRoomName = inputText
elseif editorView == "room" and activeField == "assetSearch" then
assetSearchText = inputText
elseif editorView == "world" and activeField == "worldSearch" then
worldSearchText = inputText
end
activeField = nil
end
function sortAllAssets()
setAll()
end
function createUIElement(t)
return GUI:new(t)
end
function editorDraw()
if editorView == "room" then
local s = buildRoomLayout()
love.graphics.setColor(gColor.white:set())
-- header: optional back button + search box
if s.back then
love.graphics.rectangle("line", s.back.x, s.back.y, s.back.w, s.back.h)
local sc = (s.back.h - 8) / bacSprite:getHeight()
love.graphics.draw(bacSprite, s.back.x + 4, s.back.y + 4, 0, sc, sc)
end
love.graphics.rectangle("line", s.searchBox.x, s.searchBox.y, s.searchBox.w, s.searchBox.h)
love.graphics.printf("SEARCH: " .. assetSearchText, s.searchBox.x + 6, s.searchBox.y + 9, s.searchBox.w - 12, "left")
-- scrolling list, clipped to its section so nothing bleeds into others
Layout.clip(s.listRect)
for i, item in ipairs(s.items) do
if s.list.visible(i) then
item.x, item.y = s.list.pos(i)
item.w = s.listRect.w
item:draw()
end
end
Layout.unclip()
Layout.scrollbar(s.list)
-- control cluster
love.graphics.setColor(gColor.white:set())
love.graphics.printf("LEVEL NAME", s.controls.x, s.labelY, s.controls.w, "center")
for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "save", "clearSaves", "world" }) do
if uiButtons[name] then uiButtons[name]:draw() end
end
-- status strip between the list and the controls
if editorStatus then
Layout.clip(s.status)
love.graphics.setColor(gColor.white:set())
love.graphics.printf(editorStatus, s.status.x, s.status.y + 4, s.status.w, "center")
Layout.unclip()
end
elseif editorView == "asset" then
for _, button in pairs(editingAssetButtons) do
button:draw()
end
elseif editorView == "world" then
drawWorldSidebar()
end
for _, label in pairs(labels) do
label:draw()
end
-- The room view draws its status inside its own strip (see the room branch);
-- the other views keep the simple centered line.
if editorStatus and editorView ~= "room" then
love.graphics.setColor(gColor.white:set())
love.graphics.printf(editorStatus, 10, sb.h * (6 / 10), sb.w - 20, "center")
end
end