first commit
This commit is contained in:
commit
d533cad870
12 changed files with 2726 additions and 0 deletions
1504
src/lib/abstractInventoryLib.lua
Normal file
1504
src/lib/abstractInventoryLib.lua
Normal file
File diff suppressed because it is too large
Load diff
466
src/lib/primeui.lua
Normal file
466
src/lib/primeui.lua
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
-- PrimeUI by JackMacWindows
|
||||
-- Public domain/CC0
|
||||
|
||||
local expect = require "cc.expect".expect
|
||||
|
||||
-- Initialization code
|
||||
local PrimeUI = {}
|
||||
do
|
||||
local coros = {}
|
||||
local restoreCursor
|
||||
|
||||
--- Adds a task to run in the main loop.
|
||||
---@param func function The function to run, usually an `os.pullEvent` loop
|
||||
function PrimeUI.addTask(func)
|
||||
expect(1, func, "function")
|
||||
local t = {coro = coroutine.create(func)}
|
||||
coros[#coros+1] = t
|
||||
_, t.filter = coroutine.resume(t.coro)
|
||||
end
|
||||
|
||||
--- Sends the provided arguments to the run loop, where they will be returned.
|
||||
---@param ... any The parameters to send
|
||||
function PrimeUI.resolve(...)
|
||||
coroutine.yield(coros, ...)
|
||||
end
|
||||
|
||||
--- Clears the screen and resets all components. Do not use any previously
|
||||
--- created components after calling this function.
|
||||
function PrimeUI.clear()
|
||||
-- Reset the screen.
|
||||
term.setCursorPos(1, 1)
|
||||
term.setCursorBlink(false)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.setTextColor(colors.white)
|
||||
term.clear()
|
||||
-- Reset the task list and cursor restore function.
|
||||
coros = {}
|
||||
restoreCursor = nil
|
||||
end
|
||||
|
||||
--- Sets or clears the window that holds where the cursor should be.
|
||||
---@param win window|nil The window to set as the active window
|
||||
function PrimeUI.setCursorWindow(win)
|
||||
expect(1, win, "table", "nil")
|
||||
restoreCursor = win and win.restoreCursor
|
||||
end
|
||||
|
||||
--- Gets the absolute position of a coordinate relative to a window.
|
||||
---@param win window The window to check
|
||||
---@param x number The relative X position of the point
|
||||
---@param y number The relative Y position of the point
|
||||
---@return number x The absolute X position of the window
|
||||
---@return number y The absolute Y position of the window
|
||||
function PrimeUI.getWindowPos(win, x, y)
|
||||
if win == term then return x, y end
|
||||
while win ~= term.native() and win ~= term.current() do
|
||||
if not win.getPosition then return x, y end
|
||||
local wx, wy = win.getPosition()
|
||||
x, y = x + wx - 1, y + wy - 1
|
||||
_, win = debug.getupvalue(select(2, debug.getupvalue(win.isColor, 1)), 1) -- gets the parent window through an upvalue
|
||||
end
|
||||
return x, y
|
||||
end
|
||||
|
||||
--- Runs the main loop, returning information on an action.
|
||||
---@return any ... The result of the coroutine that exited
|
||||
function PrimeUI.run()
|
||||
while true do
|
||||
-- Restore the cursor and wait for the next event.
|
||||
if restoreCursor then restoreCursor() end
|
||||
local ev = table.pack(os.pullEvent())
|
||||
-- Run all coroutines.
|
||||
for _, v in ipairs(coros) do
|
||||
if v.filter == nil or v.filter == ev[1] then
|
||||
-- Resume the coroutine, passing the current event.
|
||||
local res = table.pack(coroutine.resume(v.coro, table.unpack(ev, 1, ev.n)))
|
||||
-- If the call failed, bail out. Coroutines should never exit.
|
||||
if not res[1] then error(res[2], 2) end
|
||||
-- If the coroutine resolved, return its values.
|
||||
if res[2] == coros then return table.unpack(res, 3, res.n) end
|
||||
-- Set the next event filter.
|
||||
v.filter = res[2]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Creates a text input box.
|
||||
---@param win window The window to draw on
|
||||
---@param x number The X position of the left side of the box
|
||||
---@param y number The Y position of the box
|
||||
---@param width number The width/length of the box
|
||||
---@param action function|string A function or `run` event to call when a key is pressed
|
||||
---@param fgColor color|nil The color of the text (defaults to white)
|
||||
---@param bgColor color|nil The color of the background (defaults to black)
|
||||
---@param replacement string|nil A character to replace typed characters with
|
||||
---@param history string[]|nil A list of previous entries to provide
|
||||
---@param completion function|nil A function to call to provide completion
|
||||
function PrimeUI.inputBox(win, x, y, width, action, fgColor, bgColor, replacement, history, completion, default)
|
||||
expect(1, win, "table")
|
||||
expect(2, x, "number")
|
||||
expect(3, y, "number")
|
||||
expect(4, width, "number")
|
||||
expect(5, action, "function", "string")
|
||||
fgColor = expect(6, fgColor, "number", "nil") or colors.white
|
||||
bgColor = expect(7, bgColor, "number", "nil") or colors.black
|
||||
expect(8, replacement, "string", "nil")
|
||||
expect(9, history, "table", "nil")
|
||||
expect(10, completion, "function", "nil")
|
||||
expect(11, default, "string", "nil")
|
||||
|
||||
local box = window.create(win, x, y, width, 1)
|
||||
box.setTextColor(fgColor)
|
||||
box.setBackgroundColor(bgColor)
|
||||
box.clear()
|
||||
|
||||
PrimeUI.addTask(function()
|
||||
local text = default or ""
|
||||
local cursor = #text + 1
|
||||
local histIndex = nil
|
||||
|
||||
local function runAction()
|
||||
if type(action) == "string" then
|
||||
PrimeUI.resolve("inputBox", action, text)
|
||||
else
|
||||
action(text)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function redraw()
|
||||
box.clear()
|
||||
box.setCursorPos(1, 1)
|
||||
if replacement then
|
||||
box.write(string.rep(replacement, #text))
|
||||
else
|
||||
box.write(text)
|
||||
end
|
||||
box.setCursorPos(cursor, 1)
|
||||
|
||||
runAction()
|
||||
end
|
||||
|
||||
redraw()
|
||||
|
||||
while true do
|
||||
local ev, p1 = os.pullEvent()
|
||||
if ev == "char" then
|
||||
text = text:sub(1, cursor - 1) .. p1 .. text:sub(cursor)
|
||||
cursor = cursor + 1
|
||||
redraw()
|
||||
elseif ev == "key" then
|
||||
if p1 == keys.enter then
|
||||
runAction()
|
||||
elseif p1 == keys.left then
|
||||
cursor = math.max(1, cursor - 1)
|
||||
box.setCursorPos(cursor, 1)
|
||||
elseif p1 == keys.right then
|
||||
cursor = math.min(#text + 1, cursor + 1)
|
||||
box.setCursorPos(cursor, 1)
|
||||
elseif p1 == keys.backspace then
|
||||
if cursor > 1 then
|
||||
text = text:sub(1, cursor - 2) .. text:sub(cursor)
|
||||
cursor = cursor - 1
|
||||
redraw()
|
||||
end
|
||||
elseif p1 == keys.delete then
|
||||
text = text:sub(1, cursor - 1) .. text:sub(cursor + 1)
|
||||
redraw()
|
||||
elseif p1 == keys.up and history then
|
||||
if not histIndex then histIndex = #history + 1 end
|
||||
histIndex = math.max(1, histIndex - 1)
|
||||
text = history[histIndex] or ""
|
||||
cursor = #text + 1
|
||||
redraw()
|
||||
elseif p1 == keys.down and history then
|
||||
if histIndex then
|
||||
histIndex = math.min(#history + 1, histIndex + 1)
|
||||
text = history[histIndex] or ""
|
||||
cursor = #text + 1
|
||||
redraw()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Draws a line of text at a position.
|
||||
---@param win window The window to draw on
|
||||
---@param x number The X position of the left side of the text
|
||||
---@param y number The Y position of the text
|
||||
---@param text string The text to draw
|
||||
---@param fgColor color|nil The color of the text (defaults to white)
|
||||
---@param bgColor color|nil The color of the background (defaults to black)
|
||||
function PrimeUI.label(win, x, y, text, fgColor, bgColor)
|
||||
expect(1, win, "table")
|
||||
expect(2, x, "number")
|
||||
expect(3, y, "number")
|
||||
expect(4, text, "string")
|
||||
fgColor = expect(5, fgColor, "number", "nil") or colors.white
|
||||
bgColor = expect(6, bgColor, "number", "nil") or colors.black
|
||||
win.setCursorPos(x, y)
|
||||
win.setTextColor(fgColor)
|
||||
win.setBackgroundColor(bgColor)
|
||||
win.write(text)
|
||||
end
|
||||
|
||||
--- Creates a scrollable window, which allows drawing large content in a small area.
|
||||
---@param win window The parent window of the scroll box
|
||||
---@param x number The X position of the box
|
||||
---@param y number The Y position of the box
|
||||
---@param width number The width of the box
|
||||
---@param height number The height of the outer box
|
||||
---@param innerHeight number The height of the inner scroll area
|
||||
---@param allowArrowKeys boolean|nil Whether to allow arrow keys to scroll the box (defaults to true)
|
||||
---@param showScrollIndicators boolean|nil Whether to show arrow indicators on the right side when scrolling is available, which reduces the inner width by 1 (defaults to false)
|
||||
---@param fgColor number|nil The color of scroll indicators (defaults to white)
|
||||
---@param bgColor color|nil The color of the background (defaults to black)
|
||||
---@return window inner The inner window to draw inside
|
||||
---@return fun(pos:number) scroll A function to manually set the scroll position of the window
|
||||
function PrimeUI.scrollBox(win, x, y, width, height, innerHeight, allowArrowKeys, showScrollIndicators, fgColor, bgColor)
|
||||
expect(1, win, "table")
|
||||
expect(2, x, "number")
|
||||
expect(3, y, "number")
|
||||
expect(4, width, "number")
|
||||
expect(5, height, "number")
|
||||
expect(6, innerHeight, "number")
|
||||
expect(7, allowArrowKeys, "boolean", "nil")
|
||||
expect(8, showScrollIndicators, "boolean", "nil")
|
||||
fgColor = expect(9, fgColor, "number", "nil") or colors.white
|
||||
bgColor = expect(10, bgColor, "number", "nil") or colors.black
|
||||
if allowArrowKeys == nil then allowArrowKeys = true end
|
||||
-- Create the outer container box.
|
||||
local outer = window.create(win == term and term.current() or win, x, y, width, height)
|
||||
outer.setBackgroundColor(bgColor)
|
||||
outer.clear()
|
||||
-- Create the inner scrolling box.
|
||||
local inner = window.create(outer, 1, 1, width - (showScrollIndicators and 1 or 0), innerHeight)
|
||||
inner.setBackgroundColor(bgColor)
|
||||
inner.clear()
|
||||
-- Draw scroll indicators if desired.
|
||||
if showScrollIndicators then
|
||||
outer.setBackgroundColor(bgColor)
|
||||
outer.setTextColor(fgColor)
|
||||
outer.setCursorPos(width, height)
|
||||
outer.write(innerHeight > height and "\31" or " ")
|
||||
end
|
||||
-- Get the absolute position of the window.
|
||||
x, y = PrimeUI.getWindowPos(win, x, y)
|
||||
-- Add the scroll handler.
|
||||
local scrollPos = 1
|
||||
PrimeUI.addTask(function()
|
||||
while true do
|
||||
-- Wait for next event.
|
||||
local ev = table.pack(os.pullEvent())
|
||||
-- Update inner height in case it changed.
|
||||
innerHeight = select(2, inner.getSize())
|
||||
-- Check for scroll events and set direction.
|
||||
local dir
|
||||
if ev[1] == "key" and allowArrowKeys then
|
||||
if ev[2] == keys.up then dir = -1
|
||||
elseif ev[2] == keys.down then dir = 1 end
|
||||
elseif ev[1] == "mouse_scroll" and ev[3] >= x and ev[3] < x + width and ev[4] >= y and ev[4] < y + height then
|
||||
dir = ev[2]
|
||||
end
|
||||
-- If there's a scroll event, move the window vertically.
|
||||
if dir and (scrollPos + dir >= 1 and scrollPos + dir <= innerHeight - height) then
|
||||
scrollPos = scrollPos + dir
|
||||
inner.reposition(1, 2 - scrollPos)
|
||||
end
|
||||
-- Redraw scroll indicators if desired.
|
||||
if showScrollIndicators then
|
||||
outer.setBackgroundColor(bgColor)
|
||||
outer.setTextColor(fgColor)
|
||||
outer.setCursorPos(width, 1)
|
||||
outer.write(scrollPos > 1 and "\30" or " ")
|
||||
outer.setCursorPos(width, height)
|
||||
outer.write(scrollPos < innerHeight - height and "\31" or " ")
|
||||
end
|
||||
end
|
||||
end)
|
||||
-- Make a function to allow external scrolling.
|
||||
local function scroll(pos)
|
||||
expect(1, pos, "number")
|
||||
pos = math.floor(pos)
|
||||
expect.range(pos, 1, innerHeight - height)
|
||||
-- Scroll the window.
|
||||
scrollPos = pos
|
||||
inner.reposition(1, 2 - scrollPos)
|
||||
-- Redraw scroll indicators if desired.
|
||||
if showScrollIndicators then
|
||||
outer.setBackgroundColor(bgColor)
|
||||
outer.setTextColor(fgColor)
|
||||
outer.setCursorPos(width, 1)
|
||||
outer.write(scrollPos > 1 and "\30" or " ")
|
||||
outer.setCursorPos(width, height)
|
||||
outer.write(scrollPos < innerHeight - height and "\31" or " ")
|
||||
end
|
||||
end
|
||||
return inner, scroll
|
||||
end
|
||||
|
||||
|
||||
--- Creates a list of entries that can each be selected.
|
||||
---@param win window The window to draw on
|
||||
---@param x number The X coordinate of the inside of the box
|
||||
---@param y number The Y coordinate of the inside of the box
|
||||
---@param width number The width of the inner box
|
||||
---@param height number The height of the inner box
|
||||
---@param entries function A function that returns a list of entries to show, where the value is whether the item is pre-selected (or `"R"` for required/forced selected)
|
||||
---@param action function|string A function or `run` event that's called when a selection is made
|
||||
---@param selectChangeAction function|string|nil A function or `run` event that's called when the current selection is changed
|
||||
---@param fgColor color|nil The color of the text (defaults to white)
|
||||
---@param bgColor color|nil The color of the background (defaults to black)
|
||||
function PrimeUI.selectionBox(win, x, y, width, height, entries, action, selectChangeAction, fgColor, bgColor)
|
||||
expect(1, win, "table")
|
||||
expect(2, x, "number")
|
||||
expect(3, y, "number")
|
||||
expect(4, width, "number")
|
||||
expect(5, height, "number")
|
||||
expect(6, entries, "function")
|
||||
expect(7, action, "function", "string")
|
||||
expect(8, selectChangeAction, "function", "string", "nil")
|
||||
fgColor = expect(9, fgColor, "number", "nil") or colors.white
|
||||
bgColor = expect(10, bgColor, "number", "nil") or colors.black
|
||||
|
||||
local entrywin = window.create(win, x, y, width, height)
|
||||
local selection, scroll = 1, 1
|
||||
-- Create a function to redraw the entries on screen.
|
||||
local function drawEntries()
|
||||
-- Clear and set invisible for performance.
|
||||
entrywin.setVisible(false)
|
||||
entrywin.setBackgroundColor(bgColor)
|
||||
entrywin.clear()
|
||||
-- Draw each entry in the scrolled region.
|
||||
for i = scroll, scroll + height - 1 do
|
||||
-- Get the entry; stop if there's no more.
|
||||
local e = entries()[i]
|
||||
if not e then break end
|
||||
-- Set the colors: invert if selected.
|
||||
entrywin.setCursorPos(2, i - scroll + 1)
|
||||
if i == selection then
|
||||
entrywin.setBackgroundColor(fgColor)
|
||||
entrywin.setTextColor(bgColor)
|
||||
else
|
||||
entrywin.setBackgroundColor(bgColor)
|
||||
entrywin.setTextColor(fgColor)
|
||||
end
|
||||
-- Draw the selection.
|
||||
entrywin.clearLine()
|
||||
entrywin.write(#e > width - 1 and e:sub(1, width - 4) .. "..." or e)
|
||||
end
|
||||
-- Draw scroll arrows.
|
||||
entrywin.setBackgroundColor(bgColor)
|
||||
entrywin.setTextColor(fgColor)
|
||||
entrywin.setCursorPos(width, 1)
|
||||
entrywin.write("\30")
|
||||
entrywin.setCursorPos(width, height)
|
||||
entrywin.write("\31")
|
||||
-- Send updates to the screen.
|
||||
entrywin.setVisible(true)
|
||||
end
|
||||
-- Draw first screen.
|
||||
drawEntries()
|
||||
-- Add a task for selection keys.
|
||||
PrimeUI.addTask(function()
|
||||
while true do
|
||||
local event, key, cx, cy = os.pullEvent()
|
||||
if event == "key" then
|
||||
if key == keys.down and selection < #entries() then
|
||||
-- Move selection down.
|
||||
selection = selection + 1
|
||||
if selection > scroll + height - 1 then scroll = scroll + 1 end
|
||||
-- Send action if necessary.
|
||||
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
|
||||
elseif selectChangeAction then selectChangeAction(selection) end
|
||||
-- Redraw screen.
|
||||
drawEntries()
|
||||
elseif key == keys.up and selection > 1 then
|
||||
-- Move selection up.
|
||||
selection = selection - 1
|
||||
if selection < scroll then scroll = scroll - 1 end
|
||||
-- Send action if necessary.
|
||||
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
|
||||
elseif selectChangeAction then selectChangeAction(selection) end
|
||||
-- Redraw screen.
|
||||
drawEntries()
|
||||
elseif key == keys.enter then
|
||||
-- Select the entry: send the action.
|
||||
if type(action) == "string" then PrimeUI.resolve("selectionBox", action, entries()[selection])
|
||||
else action(entries()[selection]) end
|
||||
end
|
||||
elseif event == "mouse_click" and key == 1 then
|
||||
-- Handle clicking the scroll arrows.
|
||||
local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1)
|
||||
if cx == wx + width - 1 then
|
||||
if cy == wy and selection > 1 then
|
||||
-- Move selection up.
|
||||
selection = selection - 1
|
||||
if selection < scroll then scroll = scroll - 1 end
|
||||
-- Send action if necessary.
|
||||
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
|
||||
elseif selectChangeAction then selectChangeAction(selection) end
|
||||
-- Redraw screen.
|
||||
drawEntries()
|
||||
elseif cy == wy + height - 1 and selection < #entries() then
|
||||
-- Move selection down.
|
||||
selection = selection + 1
|
||||
if selection > scroll + height - 1 then scroll = scroll + 1 end
|
||||
-- Send action if necessary.
|
||||
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
|
||||
elseif selectChangeAction then selectChangeAction(selection) end
|
||||
-- Redraw screen.
|
||||
drawEntries()
|
||||
end
|
||||
elseif cx >= wx and cx < wx + width - 1 and cy >= wy and cy < wy + height then
|
||||
local sel = scroll + (cy - wy)
|
||||
if sel == selection then
|
||||
-- Select the entry: send the action.
|
||||
if type(action) == "string" then PrimeUI.resolve("selectionBox", action, entries()[selection])
|
||||
else action(entries()[selection]) end
|
||||
else
|
||||
selection = sel
|
||||
-- Send action if necessary.
|
||||
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
|
||||
elseif selectChangeAction then selectChangeAction(selection) end
|
||||
-- Redraw screen.
|
||||
drawEntries()
|
||||
end
|
||||
end
|
||||
elseif event == "mouse_scroll" then
|
||||
-- Handle mouse scrolling.
|
||||
local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1)
|
||||
if cx >= wx and cx < wx + width and cy >= wy and cy < wy + height then
|
||||
if key < 0 and selection > 1 then
|
||||
-- Move selection up.
|
||||
selection = selection - 1
|
||||
if selection < scroll then scroll = scroll - 1 end
|
||||
-- Send action if necessary.
|
||||
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
|
||||
elseif selectChangeAction then selectChangeAction(selection) end
|
||||
-- Redraw screen.
|
||||
drawEntries()
|
||||
elseif key > 0 and selection < #entries() then
|
||||
-- Move selection down.
|
||||
selection = selection + 1
|
||||
if selection > scroll + height - 1 then scroll = scroll + 1 end
|
||||
-- Send action if necessary.
|
||||
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
|
||||
elseif selectChangeAction then selectChangeAction(selection) end
|
||||
-- Redraw screen.
|
||||
drawEntries()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
return drawEntries
|
||||
end
|
||||
|
||||
|
||||
return {
|
||||
PrimeUI = PrimeUI
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue