note blocks and sticky test
This commit is contained in:
parent
1bc1cdb11f
commit
d829b4972f
11 changed files with 3389 additions and 118 deletions
299
level_editor/note_editor.lua
Normal file
299
level_editor/note_editor.lua
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
-- A small, self-contained multi-line text field for the level editor sidebar.
|
||||
-- LÖVE ships no GUI widgets, so this rolls the minimum needed to feel like a
|
||||
-- normal text box: a caret, a selection, mouse click/drag positioning, and the
|
||||
-- usual keyboard shortcuts (arrows, word-jump, home/end, select-all, clipboard).
|
||||
--
|
||||
-- Text is kept as an array of one-character strings (utf8-aware) with the caret
|
||||
-- and selection anchor expressed as caret indices (0..#chars, a position between
|
||||
-- characters). Everything else is derived from that.
|
||||
|
||||
local utf8 = require("utf8")
|
||||
|
||||
local pad = 6
|
||||
|
||||
local note = {
|
||||
active = false,
|
||||
chars = {}, -- array of single utf8 characters
|
||||
caret = 0, -- 0..#chars
|
||||
anchor = nil, -- selection start (caret index) or nil
|
||||
rect = { x = 0, y = 0, w = 0, h = 0 },
|
||||
dragging = false,
|
||||
}
|
||||
|
||||
-- ── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
local function toChars(str)
|
||||
local t = {}
|
||||
local i, len = 1, #str
|
||||
while i <= len do
|
||||
local nexti = utf8.offset(str, 2, i) or (len + 1)
|
||||
t[#t + 1] = str:sub(i, nexti - 1)
|
||||
i = nexti
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
local function clamp(v, lo, hi)
|
||||
if v < lo then return lo elseif v > hi then return hi else return v end
|
||||
end
|
||||
|
||||
local function isWordChar(c)
|
||||
return c ~= nil and c:match("[%w_]") ~= nil
|
||||
end
|
||||
|
||||
-- Line ranges as caret-index pairs: line covers caret indices s..e, and its text
|
||||
-- is chars[s+1 .. e]. A blank final line after a trailing newline is included.
|
||||
local function buildLines()
|
||||
local lines, start = {}, 0
|
||||
for k = 1, #note.chars do
|
||||
if note.chars[k] == "\n" then
|
||||
lines[#lines + 1] = { s = start, e = k - 1 }
|
||||
start = k
|
||||
end
|
||||
end
|
||||
lines[#lines + 1] = { s = start, e = #note.chars }
|
||||
return lines
|
||||
end
|
||||
|
||||
local function lineIndexOf(caret, lines)
|
||||
for i, line in ipairs(lines) do
|
||||
if caret >= line.s and caret <= line.e then return i end
|
||||
end
|
||||
return #lines
|
||||
end
|
||||
|
||||
-- Pixel width from a line's start up to `col` characters into it.
|
||||
local function colWidth(line, col)
|
||||
local s = table.concat(note.chars, "", line.s + 1, line.s + col)
|
||||
return font:getWidth(s)
|
||||
end
|
||||
|
||||
-- ── selection / mutation ─────────────────────────────────────────────────────
|
||||
|
||||
local function selectionRange()
|
||||
if not note.anchor or note.anchor == note.caret then return nil end
|
||||
return math.min(note.anchor, note.caret), math.max(note.anchor, note.caret)
|
||||
end
|
||||
|
||||
local function deleteRange(lo, hi)
|
||||
for k = hi, lo + 1, -1 do table.remove(note.chars, k) end
|
||||
note.caret = lo
|
||||
note.anchor = nil
|
||||
end
|
||||
|
||||
local function deleteSelection()
|
||||
local lo, hi = selectionRange()
|
||||
if not lo then return false end
|
||||
deleteRange(lo, hi)
|
||||
return true
|
||||
end
|
||||
|
||||
local function insertText(str)
|
||||
deleteSelection()
|
||||
local ins = toChars(str)
|
||||
for offset, c in ipairs(ins) do
|
||||
table.insert(note.chars, note.caret + offset, c)
|
||||
end
|
||||
note.caret = note.caret + #ins
|
||||
note.anchor = nil
|
||||
end
|
||||
|
||||
-- Move the caret, extending or clearing the selection depending on shift.
|
||||
local function moveCaret(pos, keepSelection)
|
||||
if keepSelection then
|
||||
if not note.anchor then note.anchor = note.caret end
|
||||
else
|
||||
note.anchor = nil
|
||||
end
|
||||
note.caret = clamp(pos, 0, #note.chars)
|
||||
end
|
||||
|
||||
local function wordLeft(i)
|
||||
local j = i
|
||||
while j > 0 and not isWordChar(note.chars[j]) do j = j - 1 end
|
||||
while j > 0 and isWordChar(note.chars[j]) do j = j - 1 end
|
||||
return j
|
||||
end
|
||||
|
||||
local function wordRight(i)
|
||||
local n = #note.chars
|
||||
local j = i
|
||||
while j < n and not isWordChar(note.chars[j + 1]) do j = j + 1 end
|
||||
while j < n and isWordChar(note.chars[j + 1]) do j = j + 1 end
|
||||
return j
|
||||
end
|
||||
|
||||
-- Map a point (sidebar-virtual coords) to the nearest caret index.
|
||||
local function caretFromPoint(px, py)
|
||||
local lines = buildLines()
|
||||
local lineH = font:getHeight()
|
||||
local row = clamp(math.floor((py - (note.rect.y + pad)) / lineH) + 1, 1, #lines)
|
||||
local line = lines[row]
|
||||
local target = px - (note.rect.x + pad)
|
||||
local best, bestDist = 0, math.huge
|
||||
for col = 0, line.e - line.s do
|
||||
local dist = math.abs(colWidth(line, col) - target)
|
||||
if dist < bestDist then best, bestDist = col, dist end
|
||||
end
|
||||
return line.s + best
|
||||
end
|
||||
|
||||
-- ── public API (globals, matching the editor's module style) ─────────────────
|
||||
|
||||
function noteEditorActive()
|
||||
return note.active and editorView == "room"
|
||||
end
|
||||
|
||||
function noteEditorFocus()
|
||||
note.active = true
|
||||
end
|
||||
|
||||
function noteEditorBlur()
|
||||
note.active = false
|
||||
note.dragging = false
|
||||
end
|
||||
|
||||
function noteEditorText()
|
||||
return table.concat(note.chars)
|
||||
end
|
||||
|
||||
function noteEditorSetText(str)
|
||||
note.chars = toChars(str or "")
|
||||
note.caret = #note.chars
|
||||
note.anchor = nil
|
||||
end
|
||||
|
||||
function noteEditorSetRect(x, y, w, h)
|
||||
note.rect.x, note.rect.y, note.rect.w, note.rect.h = x, y, w, h
|
||||
end
|
||||
|
||||
-- Click landed somewhere in the sidebar (virtual coords). Returns true if it hit
|
||||
-- the field (so the caller stops looking for another control).
|
||||
function noteEditorMousepressed(x, y)
|
||||
local r = note.rect
|
||||
if x < r.x or x > r.x + r.w or y < r.y or y > r.y + r.h then return false end
|
||||
note.active = true
|
||||
note.caret = caretFromPoint(x, y)
|
||||
note.anchor = note.caret
|
||||
note.dragging = true
|
||||
return true
|
||||
end
|
||||
|
||||
function noteEditorMousemoved(x, y)
|
||||
if not note.dragging then return false end
|
||||
note.caret = caretFromPoint(x, y)
|
||||
return true
|
||||
end
|
||||
|
||||
function noteEditorMousereleased()
|
||||
note.dragging = false
|
||||
if note.anchor == note.caret then note.anchor = nil end
|
||||
end
|
||||
|
||||
function noteEditorTextinput(t)
|
||||
insertText(t)
|
||||
end
|
||||
|
||||
function noteEditorKeypressed(key)
|
||||
local ctrl = love.keyboard.isDown("lctrl", "rctrl", "lgui", "rgui")
|
||||
local shift = love.keyboard.isDown("lshift", "rshift")
|
||||
|
||||
if key == "escape" then
|
||||
noteEditorBlur()
|
||||
elseif key == "return" or key == "kpenter" then
|
||||
insertText("\n")
|
||||
elseif key == "backspace" then
|
||||
if not deleteSelection() then
|
||||
local to = ctrl and wordLeft(note.caret) or note.caret - 1
|
||||
if to >= 0 and note.caret > 0 then deleteRange(math.max(0, to), note.caret) end
|
||||
end
|
||||
elseif key == "delete" then
|
||||
if not deleteSelection() then
|
||||
local to = ctrl and wordRight(note.caret) or note.caret + 1
|
||||
if note.caret < #note.chars then deleteRange(note.caret, math.min(#note.chars, to)) end
|
||||
end
|
||||
elseif key == "left" then
|
||||
moveCaret(ctrl and wordLeft(note.caret) or note.caret - 1, shift)
|
||||
elseif key == "right" then
|
||||
moveCaret(ctrl and wordRight(note.caret) or note.caret + 1, shift)
|
||||
elseif key == "up" or key == "down" then
|
||||
local lines = buildLines()
|
||||
local row = lineIndexOf(note.caret, lines)
|
||||
local col = note.caret - lines[row].s
|
||||
local target = clamp(row + (key == "up" and -1 or 1), 1, #lines)
|
||||
local line = lines[target]
|
||||
moveCaret(line.s + math.min(col, line.e - line.s), shift)
|
||||
elseif key == "home" then
|
||||
if ctrl then
|
||||
moveCaret(0, shift)
|
||||
else
|
||||
local lines = buildLines()
|
||||
moveCaret(lines[lineIndexOf(note.caret, lines)].s, shift)
|
||||
end
|
||||
elseif key == "end" then
|
||||
if ctrl then
|
||||
moveCaret(#note.chars, shift)
|
||||
else
|
||||
local lines = buildLines()
|
||||
moveCaret(lines[lineIndexOf(note.caret, lines)].e, shift)
|
||||
end
|
||||
elseif ctrl and key == "a" then
|
||||
note.anchor = 0
|
||||
note.caret = #note.chars
|
||||
elseif ctrl and (key == "c" or key == "x") then
|
||||
local lo, hi = selectionRange()
|
||||
if lo then
|
||||
love.system.setClipboardText(table.concat(note.chars, "", lo + 1, hi))
|
||||
if key == "x" then deleteRange(lo, hi) end
|
||||
end
|
||||
elseif ctrl and key == "v" then
|
||||
insertText(love.system.getClipboardText() or "")
|
||||
end
|
||||
end
|
||||
|
||||
-- Rendered inside the room-view sidebar (virtual space), clipped to its rect.
|
||||
function noteEditorDraw()
|
||||
local r = note.rect
|
||||
local lines = buildLines()
|
||||
local lineH = font:getHeight()
|
||||
local ox, oy = r.x + pad, r.y + pad
|
||||
|
||||
love.graphics.setColor(0, 0, 0, 0.85)
|
||||
love.graphics.rectangle("fill", r.x, r.y, r.w, r.h)
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
love.graphics.setLineWidth(noteEditorActive() and 2 or 1)
|
||||
love.graphics.rectangle("line", r.x, r.y, r.w, r.h)
|
||||
|
||||
Layout.clip({ x = r.x, y = r.y, w = r.w, h = r.h })
|
||||
|
||||
-- selection highlight
|
||||
local lo, hi = selectionRange()
|
||||
if lo then
|
||||
love.graphics.setColor(0.25, 0.45, 0.9, 0.6)
|
||||
for i, line in ipairs(lines) do
|
||||
local segStart, segEnd = math.max(lo, line.s), math.min(hi, line.e)
|
||||
if segEnd > segStart then
|
||||
local x1 = ox + colWidth(line, segStart - line.s)
|
||||
local x2 = ox + colWidth(line, segEnd - line.s)
|
||||
love.graphics.rectangle("fill", x1, oy + (i - 1) * lineH, x2 - x1, lineH)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
for i, line in ipairs(lines) do
|
||||
local text = table.concat(note.chars, "", line.s + 1, line.e)
|
||||
love.graphics.print(text, ox, oy + (i - 1) * lineH)
|
||||
end
|
||||
|
||||
-- caret (blinking) — only while focused
|
||||
if noteEditorActive() and (love.timer.getTime() % 1) < 0.5 then
|
||||
local row = lineIndexOf(note.caret, lines)
|
||||
local cx = ox + colWidth(lines[row], note.caret - lines[row].s)
|
||||
local cy = oy + (row - 1) * lineH
|
||||
love.graphics.setLineWidth(1)
|
||||
love.graphics.line(cx, cy, cx, cy + lineH)
|
||||
end
|
||||
|
||||
Layout.unclip()
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue