OC-KittenOS/code/libs/neoux.lua

409 lines
11 KiB
Lua

-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- neoux: Implements utilities on top of Everest & event:
-- Everest crash protection
-- Control reference
-- x/y/w/h: ints, position/size, 1,1 TL
-- selectable: boolean
-- key(window, update, char, code, down, keyFlags) (If this returns something truthy, defaults are inhibited)
-- touch(window, update, x, y, xI, yI, button)
-- drag(window, update, x, y, xI, yI, button)
-- drop(window, update, x, y, xI, yI, button)
-- scroll(window, update, x, y, xI, yI, amount)
-- clipboard(window, update, contents)
-- Global forces reference. Otherwise, nasty duplication happens.
newNeoux = function (event, neo)
-- id -> callback
local lclEvToW = {}
local everest = neo.requireAccess("x.neo.pub.window", "windowing")
event.listen("x.neo.pub.window", function (_, window, tp, ...)
if lclEvToW[window] then
lclEvToW[window](tp, ...)
end
end)
local neoux = {}
neoux.fileDialog = function (forWrite, callback, dfn)
local sync = false
local rtt = nil
if not callback then
sync = true
callback = function (rt)
sync = false
rtt = rt
end
end
local tag = neo.requireAccess("x.neo.pub.base", "filedialog").showFileDialogAsync(forWrite, dfn)
local f
f = function (_, fd, tg, re)
if fd == "filedialog" then
if tg == tag then
callback(re)
event.ignore(f)
end
end
end
event.listen("x.neo.pub.base", f)
while sync do
event.pull()
end
return rtt
end
-- Creates a wrapper around a window.
neoux.create = function (w, h, title, callback)
local window = {}
local windowCore = everest(w, h, title)
-- res is the window!
lclEvToW[windowCore.id] = function (...) callback(window, ...) end
-- API convenience: args compatible with .create
window.reset = function (nw, nh, _, cb)
callback = cb
w = nw or w
h = nh or h
windowCore.setSize(w, h)
end
window.getSize = function ()
return w, h
end
window.setSize = function (nw, nh)
w = nw
h = nh
windowCore.setSize(w, h)
end
window.getDepth = windowCore.getDepth
window.span = windowCore.span
window.recommendPalette = windowCore.recommendPalette
window.close = function ()
windowCore.close()
lclEvToW[windowCore.id] = nil
windowCore = nil
end
return window
end
-- Padding function
neoux.pad = function (...)
local fmt = require("fmttext")
return fmt.pad(...)
end
-- Text dialog formatting function.
-- Assumes you've run unicode.safeTextFormat if need be
neoux.fmtText = function (...)
local fmt = require("fmttext")
return fmt.fmtText(...)
end
-- UI FRAMEWORK --
neoux.tcwindow = function (w, h, controls, closing, bg, fg, selIndex, keyFlags)
local function rotateSelIndex()
local original = selIndex
while true do
selIndex = selIndex + 1
if not controls[selIndex] then
selIndex = 1
end
if controls[selIndex] then
if controls[selIndex].selectable then
return
end
end
if selIndex == original then
return
end
end
end
if not selIndex then
selIndex = #controls
if #controls == 0 then
selIndex = 1
end
rotateSelIndex()
end
keyFlags = keyFlags or {}
local function moveIndex(vertical, negative)
if not controls[selIndex] then return end
local currentMA, currentOA = controls[selIndex].y, controls[selIndex].x
local currentMAX = controls[selIndex].y + controls[selIndex].h - 1
if vertical then
currentMA, currentOA = controls[selIndex].x, controls[selIndex].y
currentMAX = controls[selIndex].x + controls[selIndex].w - 1
end
local bestOA = 9001
local bestSI = selIndex
if negative then
bestOA = -9000
end
for k, v in ipairs(controls) do
if (k ~= selIndex) and v.selectable then
local ma, oa = v.y, v.x
local max = v.y + v.h - 1
if vertical then
ma, oa = v.x, v.y
max = v.x + v.w - 1
end
if (ma >= currentMA and ma <= currentMAX) or (max >= currentMA and max <= currentMAX)
or (currentMA >= ma and currentMA <= max) or (currentMAX >= ma and currentMAX <= max) then
if negative then
if (oa < currentOA) and (oa > bestOA) then
bestOA = oa
bestSI = k
end
else
if (oa > currentOA) and (oa < bestOA) then
bestOA = oa
bestSI = k
end
end
end
end
end
selIndex = bestSI
end
local function doLine(window, a)
window.span(1, a, (" "):rep(w), bg, fg)
for k, v in ipairs(controls) do
if a >= v.y then
if a < (v.y + v.h) then
v.line(window, v.x, a, (a - v.y) + 1, bg, fg, selIndex == k)
end
end
end
end
local function doZone(window, control, cache)
for i = 1, control.h do
local l = i + control.y - 1
if (not cache) or (not cache[l]) then
doLine(window, l)
if cache then cache[l] = true end
end
end
end
local function moveIndexAU(window, vertical, negative)
local c1 = controls[selIndex]
moveIndex(vertical, negative)
local c2 = controls[selIndex]
local cache = {}
if c1 then doZone(window, c1, cache) end
if c2 then doZone(window, c2, cache) end
end
-- Attach .update for external interference
for k, v in ipairs(controls) do
v.update = function (window) doZone(window, v, {}) end
end
return function (window, ev, a, b, c, d, e)
-- X,Y,Xi,Yi,B
if ev == "touch" then
local found = nil
for k, v in ipairs(controls) do
if v.selectable then
if a >= v.x then
if a < (v.x + v.w) then
if b >= v.y then
if b < (v.y + v.h) then
found = k
break
end
end
end
end
end
end
if found then
local c1 = controls[selIndex]
selIndex = found
local c2 = controls[selIndex]
local cache = {}
if c1 then doZone(window, c1, cache) end
if c2 then
doZone(window, c2, cache)
if c2.touch then
c2.touch(window, function () doZone(window, c2) end, (a - c2.x) + 1, (b - c2.y) + 1, c, d, e)
end
end
end
-- X,Y,Xi,Yi,B (or D for scroll)
elseif ev == "drag" or ev == "drop" or ev == "scroll" then
if controls[selIndex] then
if controls[selIndex][ev] then
controls[selIndex][ev](window, function () doZone(window, controls[selIndex]) end, (a - controls[selIndex].x) + 1, (b - controls[selIndex].y) + 1, c, d, e)
end
end
elseif ev == "key" then
if controls[selIndex] and controls[selIndex].key then
if controls[selIndex].key(window, function () doZone(window, controls[selIndex]) end, a, b, c, keyFlags) then
return
end
end
if b == 29 then
keyFlags.ctrl = c
elseif b == 157 then
keyFlags.rctrl = c
elseif b == 42 then
keyFlags.shift = c
elseif b == 54 then
keyFlags.rshift = c
elseif not (keyFlags.ctrl or keyFlags.rctrl or keyFlags.shift or keyFlags.rshift) then
if b == 203 then
if c then
moveIndexAU(window, false, true)
end
elseif b == 205 then
if c then
moveIndexAU(window, false, false)
end
elseif b == 200 then
if c then
moveIndexAU(window, true, true)
end
elseif b == 208 then
if c then
moveIndexAU(window, true, false)
end
elseif a == 9 then
if c then
local c1 = controls[selIndex]
rotateSelIndex()
local c2 = controls[selIndex]
local cache = {}
if c1 then doZone(window, c1, cache) end
if c2 then doZone(window, c2, cache) end
end
end
end
elseif ev == "clipboard" then
if controls[selIndex] then
if controls[selIndex].clipboard then
controls[selIndex].clipboard(window, function () doZone(window, controls[selIndex]) end, a)
end
end
elseif ev == "line" then
doLine(window, a)
elseif ev == "close" then
closing(window)
end
end
end
neoux.tcrawview = function (x, y, lines)
return {
x = x,
y = y,
w = unicode.len(lines[1]),
h = #lines,
selectable = false,
line = function (window, x, y, lined, bg, fg, selected)
-- Can't be selected normally so ignore that flag
window.span(x, y, lines[lined], bg, fg)
end
}
end
neoux.tchdivider = function (x, y, w)
return neoux.tcrawview(x, y, {("-"):rep(w)})
end
neoux.tcvdivider = function (x, y, h)
local n = {}
for i = 1, h do
n[i] = "|"
end
return neoux.tcrawview(x, y, n)
end
neoux.tcbutton = function (x, y, text, callback)
text = "<" .. text .. ">"
return {
x = x,
y = y,
w = unicode.len(text),
h = 1,
selectable = true,
key = function (window, update, a, c, d, f)
if d then
if a == 13 or a == 32 then
callback(window)
return true
end
end
end,
touch = function (window, update, x, y, button)
callback(window)
end,
line = function (window, x, y, lind, bg, fg, selected)
local fg1 = fg
if selected then
fg = bg
bg = fg1
end
window.span(x, y, text, bg, fg)
end
}
end
-- Note: w should be at least 2 - this is similar to buttons.
neoux.tcfield = function (x, y, w, textprop)
-- compat. workaround for apps which nuke tcfields
local p = unicode.len(textprop()) + 1
return {
x = x,
y = y,
w = w,
h = 1,
selectable = true,
key = function (window, update, a, c, d, f)
if d then
local ot = textprop()
local le = require("lineedit")
p = le.clamp(ot, p)
if c == 63 then
neo.requireAccess("x.neo.pub.globals", "clipboard").setSetting("clipboard", ot)
elseif c == 64 then
local contents = neo.requireAccess("x.neo.pub.globals", "clipboard").getSetting("clipboard")
contents = contents:match("^[^\r\n]*")
textprop(contents)
update()
elseif a ~= 9 then
local lT, lC, lX = le.key(a ~= 0 and unicode.char(a), c, ot, p)
if lT or lC then
if lT then textprop(lT) end
p = lC or p
update()
return true
end
end
end
end,
clipboard = function (window, update, contents)
contents = contents:match("^[^\r\n]*")
textprop(contents)
update()
end,
line = function (window, x, y, lind, bg, fg, selected)
local fg1 = fg
if selected then
fg = bg
bg = fg1
end
local t, e, r = textprop(), require("lineedit")
p = e.clamp(t, p)
t, r = unicode.safeTextFormat(t, p)
window.span(x, y, "[" .. e.draw(w - 2, t, selected and r) .. "]", bg, fg)
end
}
end
neoux.startDialog = function (fmt, title, wait)
fmt = neoux.fmtText(unicode.safeTextFormat(fmt), 40)
neoux.create(40, #fmt, title, function (window, ev, a, b, c)
if ev == "line" then
window.span(1, a, fmt[a], 0xFFFFFF, 0)
end
if ev == "close" then
window.close()
wait = nil
end
end)
while wait do
event.pull()
end
end
return neoux
end
return newNeoux