feat: libtui + tuidemo (WIP)
This commit is contained in:
parent
c47b6e0ae4
commit
e7666715b0
634
apis/libtui.lua
Normal file
634
apis/libtui.lua
Normal file
@ -0,0 +1,634 @@
|
|||||||
|
local _VERSION = '0.1.0';
|
||||||
|
|
||||||
|
local NODE_TEXT = 'text';
|
||||||
|
local NODE_BUTTON = 'button';
|
||||||
|
local NODE_BOX = 'box';
|
||||||
|
local NODE_FRAGMENT = 'fragment';
|
||||||
|
|
||||||
|
local DEFAULT_COLOR = colors.white;
|
||||||
|
local DEFAULT_BG_COLOR = colors.black;
|
||||||
|
local DISABLED_COLOR = colors.gray;
|
||||||
|
|
||||||
|
local function isArray(value)
|
||||||
|
if type(value) ~= 'table' then
|
||||||
|
return false;
|
||||||
|
end
|
||||||
|
|
||||||
|
if value.kind then
|
||||||
|
return false;
|
||||||
|
end
|
||||||
|
|
||||||
|
local count = 0;
|
||||||
|
for key, _ in pairs(value) do
|
||||||
|
if type(key) ~= 'number' then
|
||||||
|
return false;
|
||||||
|
end
|
||||||
|
if key > count then
|
||||||
|
count = key;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return count > 0;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function firstColor(value, fallback)
|
||||||
|
if type(value) == 'table' then
|
||||||
|
return value[1] or fallback;
|
||||||
|
end
|
||||||
|
|
||||||
|
return value or fallback;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function firstFunction(value)
|
||||||
|
if type(value) == 'function' then
|
||||||
|
return value;
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(value) == 'table' and type(value[1]) == 'function' then
|
||||||
|
return value[1];
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function shallowCopy(value)
|
||||||
|
local result = {};
|
||||||
|
|
||||||
|
if type(value) ~= 'table' then
|
||||||
|
return result;
|
||||||
|
end
|
||||||
|
|
||||||
|
for key, item in pairs(value) do
|
||||||
|
result[key] = item;
|
||||||
|
end
|
||||||
|
|
||||||
|
return result;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function makeNode(kind, props)
|
||||||
|
props = props or {};
|
||||||
|
return {
|
||||||
|
kind = kind,
|
||||||
|
props = props,
|
||||||
|
};
|
||||||
|
end
|
||||||
|
|
||||||
|
local function makeText(value, props)
|
||||||
|
if type(value) == 'table' and props == nil then
|
||||||
|
props = shallowCopy(value);
|
||||||
|
else
|
||||||
|
props = shallowCopy(props);
|
||||||
|
props.text = value;
|
||||||
|
end
|
||||||
|
|
||||||
|
return makeNode(NODE_TEXT, props);
|
||||||
|
end
|
||||||
|
|
||||||
|
local function makeButton(value, props)
|
||||||
|
if type(value) == 'table' and props == nil then
|
||||||
|
props = shallowCopy(value);
|
||||||
|
else
|
||||||
|
props = shallowCopy(props);
|
||||||
|
props.text = value;
|
||||||
|
end
|
||||||
|
|
||||||
|
return makeNode(NODE_BUTTON, props);
|
||||||
|
end
|
||||||
|
|
||||||
|
local function makeBox(props)
|
||||||
|
return makeNode(NODE_BOX, shallowCopy(props));
|
||||||
|
end
|
||||||
|
|
||||||
|
local function makeList(props)
|
||||||
|
props = shallowCopy(props);
|
||||||
|
props.direction = 'column';
|
||||||
|
return makeNode(NODE_BOX, props);
|
||||||
|
end
|
||||||
|
|
||||||
|
local function makeFragment(children)
|
||||||
|
return makeNode(NODE_FRAGMENT, { children = children or {} });
|
||||||
|
end
|
||||||
|
|
||||||
|
local function lineText(value, width)
|
||||||
|
value = tostring(value or '');
|
||||||
|
|
||||||
|
if width <= 0 then
|
||||||
|
return '';
|
||||||
|
end
|
||||||
|
|
||||||
|
if string.len(value) > width then
|
||||||
|
return string.sub(value, 1, width);
|
||||||
|
end
|
||||||
|
|
||||||
|
return value .. string.rep(' ', width - string.len(value));
|
||||||
|
end
|
||||||
|
|
||||||
|
local function shrinkRect(rect, amount)
|
||||||
|
amount = amount or 0;
|
||||||
|
return {
|
||||||
|
x = rect.x + amount,
|
||||||
|
y = rect.y + amount,
|
||||||
|
w = rect.w - amount * 2,
|
||||||
|
h = rect.h - amount * 2,
|
||||||
|
};
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isInside(rect, x, y)
|
||||||
|
return x >= rect.x and x < rect.x + rect.w and y >= rect.y and y < rect.y + rect.h;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fillRect(rect, bgColor)
|
||||||
|
if rect.w <= 0 or rect.h <= 0 then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setBackgroundColor(bgColor);
|
||||||
|
for y = rect.y, rect.y + rect.h - 1 do
|
||||||
|
term.setCursorPos(rect.x, y);
|
||||||
|
term.write(string.rep(' ', rect.w));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function writeAt(x, y, text, width)
|
||||||
|
if width <= 0 then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setCursorPos(x, y);
|
||||||
|
term.write(lineText(text, width));
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawBorder(rect, props)
|
||||||
|
if rect.w <= 1 or rect.h <= 1 then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
local color = firstColor(props.color, DEFAULT_COLOR);
|
||||||
|
local bgColor = firstColor(props.bgColor, DEFAULT_BG_COLOR);
|
||||||
|
local top = '+' .. string.rep('-', rect.w - 2) .. '+';
|
||||||
|
local bottom = top;
|
||||||
|
local title = props.title;
|
||||||
|
|
||||||
|
if title then
|
||||||
|
local titleText = ' ' .. tostring(title) .. ' ';
|
||||||
|
if string.len(titleText) < rect.w - 1 then
|
||||||
|
top = '+' .. titleText .. string.rep('-', rect.w - 2 - string.len(titleText)) .. '+';
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setTextColor(color);
|
||||||
|
term.setBackgroundColor(bgColor);
|
||||||
|
writeAt(rect.x, rect.y, top, rect.w);
|
||||||
|
writeAt(rect.x, rect.y + rect.h - 1, bottom, rect.w);
|
||||||
|
|
||||||
|
for y = rect.y + 1, rect.y + rect.h - 2 do
|
||||||
|
term.setCursorPos(rect.x, y);
|
||||||
|
term.write('|');
|
||||||
|
term.setCursorPos(rect.x + rect.w - 1, y);
|
||||||
|
term.write('|');
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function normalizeChildren(children)
|
||||||
|
local result = {};
|
||||||
|
|
||||||
|
if children == nil then
|
||||||
|
return result;
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(children) ~= 'table' or children.kind then
|
||||||
|
return { children };
|
||||||
|
end
|
||||||
|
|
||||||
|
if isArray(children) then
|
||||||
|
for _, child in ipairs(children) do
|
||||||
|
table.insert(result, child);
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(result, children);
|
||||||
|
end
|
||||||
|
|
||||||
|
return result;
|
||||||
|
end
|
||||||
|
|
||||||
|
local resolveNode;
|
||||||
|
|
||||||
|
local function resolveChildren(children)
|
||||||
|
local result = {};
|
||||||
|
|
||||||
|
for _, child in ipairs(normalizeChildren(children)) do
|
||||||
|
local node = resolveNode(child);
|
||||||
|
if node then
|
||||||
|
table.insert(result, node);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return result;
|
||||||
|
end
|
||||||
|
|
||||||
|
function resolveNode(input)
|
||||||
|
if input == nil then
|
||||||
|
return nil;
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(input) == 'function' then
|
||||||
|
return resolveNode(input());
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(input) == 'string' or type(input) == 'number' then
|
||||||
|
return makeText(tostring(input));
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(input) ~= 'table' then
|
||||||
|
return makeText(tostring(input));
|
||||||
|
end
|
||||||
|
|
||||||
|
if input.kind then
|
||||||
|
local props = shallowCopy(input.props);
|
||||||
|
props.children = resolveChildren(props.children);
|
||||||
|
return makeNode(input.kind, props);
|
||||||
|
end
|
||||||
|
|
||||||
|
if isArray(input) then
|
||||||
|
return makeFragment(resolveChildren(input));
|
||||||
|
end
|
||||||
|
|
||||||
|
return makeFragment(resolveChildren(input.children));
|
||||||
|
end
|
||||||
|
|
||||||
|
local function buttonLabel(props)
|
||||||
|
return '[ ' .. tostring(props.text or props.label or '') .. ' ]';
|
||||||
|
end
|
||||||
|
|
||||||
|
local naturalSize;
|
||||||
|
|
||||||
|
local function childrenNaturalSize(children, direction, gap)
|
||||||
|
local width = 0;
|
||||||
|
local height = 0;
|
||||||
|
|
||||||
|
for index, child in ipairs(children) do
|
||||||
|
local childWidth, childHeight = naturalSize(child);
|
||||||
|
|
||||||
|
if direction == 'row' then
|
||||||
|
width = width + childWidth;
|
||||||
|
height = math.max(height, childHeight);
|
||||||
|
if index > 1 then
|
||||||
|
width = width + gap;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
width = math.max(width, childWidth);
|
||||||
|
height = height + childHeight;
|
||||||
|
if index > 1 then
|
||||||
|
height = height + gap;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return width, height;
|
||||||
|
end
|
||||||
|
|
||||||
|
function naturalSize(node)
|
||||||
|
if not node then
|
||||||
|
return 0, 0;
|
||||||
|
end
|
||||||
|
|
||||||
|
local props = node.props or {};
|
||||||
|
|
||||||
|
if node.kind == NODE_TEXT then
|
||||||
|
return props.width or string.len(tostring(props.text or '')), props.height or 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
if node.kind == NODE_BUTTON then
|
||||||
|
return props.width or string.len(buttonLabel(props)), props.height or 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
local direction = props.direction or 'column';
|
||||||
|
local gap = props.gap or 0;
|
||||||
|
local padding = props.padding or 0;
|
||||||
|
local border = props.border and 2 or 0;
|
||||||
|
local childWidth, childHeight = childrenNaturalSize(props.children or {}, direction, gap);
|
||||||
|
|
||||||
|
if node.kind == NODE_FRAGMENT then
|
||||||
|
return childWidth, childHeight;
|
||||||
|
end
|
||||||
|
|
||||||
|
return props.width or childWidth + padding * 2 + border, props.height or childHeight + padding * 2 + border;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function applyNodeColors(props)
|
||||||
|
term.setTextColor(firstColor(props.color, DEFAULT_COLOR));
|
||||||
|
term.setBackgroundColor(firstColor(props.bgColor, DEFAULT_BG_COLOR));
|
||||||
|
end
|
||||||
|
|
||||||
|
local function childAxisSize(child, direction)
|
||||||
|
local props = child.props or {};
|
||||||
|
local naturalWidth, naturalHeight = naturalSize(child);
|
||||||
|
|
||||||
|
if direction == 'row' then
|
||||||
|
return props.width or naturalWidth;
|
||||||
|
end
|
||||||
|
|
||||||
|
return props.height or naturalHeight;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function layoutChildren(children, rect, direction, gap)
|
||||||
|
local fixedSize = 0;
|
||||||
|
local flexSize = 0;
|
||||||
|
local axisSize = direction == 'row' and rect.w or rect.h;
|
||||||
|
local layouts = {};
|
||||||
|
local remaining;
|
||||||
|
|
||||||
|
for _, child in ipairs(children) do
|
||||||
|
local props = child.props or {};
|
||||||
|
if props.flex then
|
||||||
|
flexSize = flexSize + props.flex;
|
||||||
|
else
|
||||||
|
fixedSize = fixedSize + childAxisSize(child, direction);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
remaining = axisSize - fixedSize - math.max(#children - 1, 0) * gap;
|
||||||
|
if remaining < 0 then
|
||||||
|
remaining = 0;
|
||||||
|
end
|
||||||
|
|
||||||
|
local cursor = direction == 'row' and rect.x or rect.y;
|
||||||
|
local lastFlexIndex = nil;
|
||||||
|
local usedFlexSize = 0;
|
||||||
|
|
||||||
|
for index, child in ipairs(children) do
|
||||||
|
if (child.props or {}).flex then
|
||||||
|
lastFlexIndex = index;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for index, child in ipairs(children) do
|
||||||
|
local props = child.props or {};
|
||||||
|
local size;
|
||||||
|
|
||||||
|
if props.flex then
|
||||||
|
if index == lastFlexIndex then
|
||||||
|
size = remaining - usedFlexSize;
|
||||||
|
else
|
||||||
|
size = math.floor(remaining * props.flex / flexSize);
|
||||||
|
usedFlexSize = usedFlexSize + size;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
size = childAxisSize(child, direction);
|
||||||
|
end
|
||||||
|
|
||||||
|
if size < 0 then
|
||||||
|
size = 0;
|
||||||
|
end
|
||||||
|
|
||||||
|
if direction == 'row' then
|
||||||
|
table.insert(layouts, { node = child, rect = { x = cursor, y = rect.y, w = size, h = props.height or rect.h } });
|
||||||
|
else
|
||||||
|
table.insert(layouts, { node = child, rect = { x = rect.x, y = cursor, w = props.width or rect.w, h = size } });
|
||||||
|
end
|
||||||
|
|
||||||
|
cursor = cursor + size + gap;
|
||||||
|
end
|
||||||
|
|
||||||
|
return layouts;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function createTui(eventloop)
|
||||||
|
assert(type(eventloop) == 'table', 'bad argument #1 (eventloop expected)');
|
||||||
|
assert(type(eventloop.register) == 'function', 'bad argument #1 (eventloop expected)');
|
||||||
|
assert(type(eventloop.startLoop) == 'function', 'bad argument #1 (eventloop expected)');
|
||||||
|
|
||||||
|
local api = {};
|
||||||
|
local root = nil;
|
||||||
|
local clickables = {};
|
||||||
|
local finalEvent = nil;
|
||||||
|
local previousState = nil;
|
||||||
|
|
||||||
|
local function createErrorEvent(reason)
|
||||||
|
if type(reason) == 'table' then
|
||||||
|
return {
|
||||||
|
type = 'error',
|
||||||
|
error = {
|
||||||
|
name = reason.name or 'libtui error',
|
||||||
|
reason = reason.reason or tostring(reason),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
type = 'error',
|
||||||
|
error = {
|
||||||
|
name = 'libtui error',
|
||||||
|
reason = tostring(reason),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stopWith(event)
|
||||||
|
finalEvent = finalEvent or event;
|
||||||
|
if eventloop.isRunningLoop and eventloop.isRunningLoop() then
|
||||||
|
eventloop.stopLoop();
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local renderNode;
|
||||||
|
|
||||||
|
local function addClickable(node, rect)
|
||||||
|
local props = node.props or {};
|
||||||
|
local handler = firstFunction(props.onClick);
|
||||||
|
|
||||||
|
if not handler or props.disabled then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(clickables, {
|
||||||
|
rect = rect,
|
||||||
|
node = node,
|
||||||
|
handler = handler,
|
||||||
|
});
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawTextNode(node, rect)
|
||||||
|
local props = node.props or {};
|
||||||
|
|
||||||
|
if rect.w <= 0 or rect.h <= 0 then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
fillRect(rect, firstColor(props.bgColor, DEFAULT_BG_COLOR));
|
||||||
|
applyNodeColors(props);
|
||||||
|
writeAt(rect.x, rect.y, props.text or '', rect.w);
|
||||||
|
addClickable(node, rect);
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawButtonNode(node, rect)
|
||||||
|
local props = node.props or {};
|
||||||
|
|
||||||
|
if rect.w <= 0 or rect.h <= 0 then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
fillRect(rect, firstColor(props.bgColor, DEFAULT_BG_COLOR));
|
||||||
|
term.setTextColor(props.disabled and DISABLED_COLOR or firstColor(props.color, DEFAULT_COLOR));
|
||||||
|
term.setBackgroundColor(firstColor(props.bgColor, DEFAULT_BG_COLOR));
|
||||||
|
writeAt(rect.x, rect.y, buttonLabel(props), rect.w);
|
||||||
|
addClickable(node, rect);
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawBoxNode(node, rect)
|
||||||
|
local props = node.props or {};
|
||||||
|
local children = props.children or {};
|
||||||
|
local contentRect = rect;
|
||||||
|
local padding = props.padding or 0;
|
||||||
|
local direction = props.direction or 'column';
|
||||||
|
local gap = props.gap or 0;
|
||||||
|
|
||||||
|
if rect.w <= 0 or rect.h <= 0 then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
fillRect(rect, firstColor(props.bgColor, DEFAULT_BG_COLOR));
|
||||||
|
addClickable(node, rect);
|
||||||
|
|
||||||
|
if props.border then
|
||||||
|
drawBorder(rect, props);
|
||||||
|
contentRect = shrinkRect(contentRect, 1);
|
||||||
|
end
|
||||||
|
|
||||||
|
if padding > 0 then
|
||||||
|
contentRect = shrinkRect(contentRect, padding);
|
||||||
|
end
|
||||||
|
|
||||||
|
if contentRect.w <= 0 or contentRect.h <= 0 then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, item in ipairs(layoutChildren(children, contentRect, direction, gap)) do
|
||||||
|
renderNode(item.node, item.rect);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function renderNode(node, rect)
|
||||||
|
if node.kind == NODE_TEXT then
|
||||||
|
drawTextNode(node, rect);
|
||||||
|
elseif node.kind == NODE_BUTTON then
|
||||||
|
drawButtonNode(node, rect);
|
||||||
|
else
|
||||||
|
drawBoxNode(node, rect);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function redraw()
|
||||||
|
local width, height = term.getSize();
|
||||||
|
|
||||||
|
clickables = {};
|
||||||
|
term.setCursorBlink(false);
|
||||||
|
term.setTextColor(DEFAULT_COLOR);
|
||||||
|
term.setBackgroundColor(DEFAULT_BG_COLOR);
|
||||||
|
term.clear();
|
||||||
|
renderNode(resolveNode(root), { x = 1, y = 1, w = width, h = height });
|
||||||
|
end
|
||||||
|
|
||||||
|
local function safeRedraw()
|
||||||
|
local ok, reason = pcall(redraw);
|
||||||
|
if not ok then
|
||||||
|
stopWith(createErrorEvent(reason));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function safeClick(handler, event)
|
||||||
|
local ok, reason = pcall(handler, api, event);
|
||||||
|
if not ok then
|
||||||
|
stopWith(createErrorEvent(reason));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function api.exitUI(reason)
|
||||||
|
stopWith({ type = 'exitUI', reason = reason });
|
||||||
|
end
|
||||||
|
|
||||||
|
function api.rerender()
|
||||||
|
safeRedraw();
|
||||||
|
end
|
||||||
|
|
||||||
|
function api.render(nextRoot)
|
||||||
|
root = nextRoot;
|
||||||
|
finalEvent = nil;
|
||||||
|
|
||||||
|
previousState = {
|
||||||
|
color = term.getTextColor(),
|
||||||
|
bgColor = term.getBackgroundColor(),
|
||||||
|
cursorX = 1,
|
||||||
|
cursorY = 1,
|
||||||
|
cursorBlink = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if term.getCursorPos then
|
||||||
|
previousState.cursorX, previousState.cursorY = term.getCursorPos();
|
||||||
|
end
|
||||||
|
|
||||||
|
if term.getCursorBlink then
|
||||||
|
previousState.cursorBlink = term.getCursorBlink();
|
||||||
|
end
|
||||||
|
|
||||||
|
eventloop.onStart(safeRedraw);
|
||||||
|
eventloop.onStop(function()
|
||||||
|
term.setTextColor(previousState.color);
|
||||||
|
term.setBackgroundColor(previousState.bgColor);
|
||||||
|
term.clear();
|
||||||
|
term.setCursorPos(previousState.cursorX, previousState.cursorY);
|
||||||
|
term.setCursorBlink(previousState.cursorBlink);
|
||||||
|
end);
|
||||||
|
|
||||||
|
eventloop.register('mouse_click', function(button, x, y)
|
||||||
|
for index = #clickables, 1, -1 do
|
||||||
|
local item = clickables[index];
|
||||||
|
if isInside(item.rect, x, y) then
|
||||||
|
safeClick(item.handler, {
|
||||||
|
type = 'mouse_click',
|
||||||
|
button = button,
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
node = item.node,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end);
|
||||||
|
|
||||||
|
eventloop.register('term_resize', function()
|
||||||
|
safeRedraw();
|
||||||
|
end);
|
||||||
|
|
||||||
|
eventloop.register('terminate', function()
|
||||||
|
finalEvent = finalEvent or { type = 'terminate' };
|
||||||
|
end);
|
||||||
|
|
||||||
|
local ok, reason = pcall(eventloop.startLoop);
|
||||||
|
if not ok then
|
||||||
|
finalEvent = createErrorEvent(reason);
|
||||||
|
end
|
||||||
|
|
||||||
|
root = nil;
|
||||||
|
clickables = {};
|
||||||
|
|
||||||
|
return finalEvent or { type = 'exitUI' };
|
||||||
|
end
|
||||||
|
|
||||||
|
api.Text = makeText;
|
||||||
|
api.Button = makeButton;
|
||||||
|
api.Box = makeBox;
|
||||||
|
api.List = makeList;
|
||||||
|
api.Fragment = makeFragment;
|
||||||
|
api.version = _VERSION;
|
||||||
|
api.eventloop = eventloop;
|
||||||
|
|
||||||
|
api.text = makeText;
|
||||||
|
api.button = makeButton;
|
||||||
|
api.box = makeBox;
|
||||||
|
api.list = makeList;
|
||||||
|
|
||||||
|
return api;
|
||||||
|
end
|
||||||
|
|
||||||
|
return createTui;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TrapOS",
|
"name": "TrapOS",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"branch": "master",
|
"branch": "master",
|
||||||
"files": [
|
"files": [
|
||||||
"startup/motd.lua",
|
"startup/motd.lua",
|
||||||
@ -9,9 +9,11 @@
|
|||||||
"programs/router.lua",
|
"programs/router.lua",
|
||||||
"programs/events.lua",
|
"programs/events.lua",
|
||||||
"programs/ping.lua",
|
"programs/ping.lua",
|
||||||
|
"programs/tuidemo.lua",
|
||||||
"programs/upgrade.lua",
|
"programs/upgrade.lua",
|
||||||
"apis/net.lua",
|
"apis/net.lua",
|
||||||
"apis/eventloop.lua"
|
"apis/eventloop.lua",
|
||||||
|
"apis/libtui.lua"
|
||||||
],
|
],
|
||||||
"autostart": [
|
"autostart": [
|
||||||
"servers/ping-server"
|
"servers/ping-server"
|
||||||
|
|||||||
211
programs/tuidemo.lua
Normal file
211
programs/tuidemo.lua
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
local _VERSION = '0.1.0';
|
||||||
|
|
||||||
|
local command = ...;
|
||||||
|
|
||||||
|
local function printUsage()
|
||||||
|
print('tuidemo usage:');
|
||||||
|
print();
|
||||||
|
print('\t\t\ttuidemo');
|
||||||
|
print('\t\t\ttuidemo version');
|
||||||
|
print('\t\t\ttuidemo help');
|
||||||
|
end
|
||||||
|
|
||||||
|
if command == 'version' or command == '-version' or command == '--version' then
|
||||||
|
print('tuidemo v' .. _VERSION);
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
if command == 'help' or command == '-help' or command == '--help' then
|
||||||
|
printUsage();
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
if command ~= nil and command ~= '' then
|
||||||
|
printUsage();
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
local createEventLoop = require('/apis/eventloop');
|
||||||
|
local createTui = require('/apis/libtui');
|
||||||
|
|
||||||
|
local eventloop = createEventLoop();
|
||||||
|
local ui = createTui(eventloop);
|
||||||
|
|
||||||
|
local Text = ui.Text;
|
||||||
|
local Button = ui.Button;
|
||||||
|
local Box = ui.Box;
|
||||||
|
local List = ui.List;
|
||||||
|
|
||||||
|
local page = 1;
|
||||||
|
local pageCount = 3;
|
||||||
|
|
||||||
|
local function previousPage()
|
||||||
|
page = page - 1;
|
||||||
|
if page < 1 then
|
||||||
|
page = pageCount;
|
||||||
|
end
|
||||||
|
ui.rerender();
|
||||||
|
end
|
||||||
|
|
||||||
|
local function nextPage()
|
||||||
|
page = page + 1;
|
||||||
|
if page > pageCount then
|
||||||
|
page = 1;
|
||||||
|
end
|
||||||
|
ui.rerender();
|
||||||
|
end
|
||||||
|
|
||||||
|
local function Header()
|
||||||
|
return Box({
|
||||||
|
direction = 'row',
|
||||||
|
bgColor = colors.gray,
|
||||||
|
children = {
|
||||||
|
Text('Trap UI Demo', {
|
||||||
|
flex = 1,
|
||||||
|
color = colors.white,
|
||||||
|
bgColor = colors.gray,
|
||||||
|
}),
|
||||||
|
Button('X', {
|
||||||
|
color = colors.white,
|
||||||
|
bgColor = colors.red,
|
||||||
|
onClick = function(tui)
|
||||||
|
tui.exitUI('closed');
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
end
|
||||||
|
|
||||||
|
local function PageText()
|
||||||
|
return List({
|
||||||
|
gap = 1,
|
||||||
|
padding = 1,
|
||||||
|
children = {
|
||||||
|
Text('Text components render strings inside their assigned rectangle.'),
|
||||||
|
Text('This line uses pink background and black foreground.', {
|
||||||
|
color = colors.black,
|
||||||
|
bgColor = colors.pink,
|
||||||
|
}),
|
||||||
|
Text('Resize the terminal to force a redraw.'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
end
|
||||||
|
|
||||||
|
local function PageLayout()
|
||||||
|
return Box({
|
||||||
|
direction = 'row',
|
||||||
|
gap = 1,
|
||||||
|
padding = 1,
|
||||||
|
children = {
|
||||||
|
Box({
|
||||||
|
flex = 1,
|
||||||
|
border = true,
|
||||||
|
title = 'Left',
|
||||||
|
children = {
|
||||||
|
Text('flex = 1'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Box({
|
||||||
|
width = 18,
|
||||||
|
border = true,
|
||||||
|
title = 'Fixed',
|
||||||
|
children = {
|
||||||
|
Text('width = 18'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Box({
|
||||||
|
flex = 2,
|
||||||
|
border = true,
|
||||||
|
title = 'Right',
|
||||||
|
children = {
|
||||||
|
Text('flex = 2'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
end
|
||||||
|
|
||||||
|
local function PageButtons()
|
||||||
|
return List({
|
||||||
|
gap = 1,
|
||||||
|
padding = 1,
|
||||||
|
children = {
|
||||||
|
Text('Buttons are clickable hitboxes.'),
|
||||||
|
Button('Click me to go next', {
|
||||||
|
color = colors.black,
|
||||||
|
bgColor = colors.lime,
|
||||||
|
onClick = function()
|
||||||
|
nextPage();
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
Button('Exit demo', {
|
||||||
|
color = colors.white,
|
||||||
|
bgColor = colors.red,
|
||||||
|
onClick = function(tui)
|
||||||
|
tui.exitUI('button');
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
end
|
||||||
|
|
||||||
|
local function CurrentPage()
|
||||||
|
if page == 1 then
|
||||||
|
return PageText();
|
||||||
|
end
|
||||||
|
|
||||||
|
if page == 2 then
|
||||||
|
return PageLayout();
|
||||||
|
end
|
||||||
|
|
||||||
|
return PageButtons();
|
||||||
|
end
|
||||||
|
|
||||||
|
local function Footer()
|
||||||
|
return Box({
|
||||||
|
direction = 'row',
|
||||||
|
gap = 1,
|
||||||
|
children = {
|
||||||
|
Button('Previous', {
|
||||||
|
onClick = function()
|
||||||
|
previousPage();
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
Text('Page ' .. page .. '/' .. pageCount, { flex = 1 }),
|
||||||
|
Button('Next', {
|
||||||
|
onClick = function()
|
||||||
|
nextPage();
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
end
|
||||||
|
|
||||||
|
local function App()
|
||||||
|
return Box({
|
||||||
|
direction = 'column',
|
||||||
|
children = {
|
||||||
|
Header(),
|
||||||
|
Box({
|
||||||
|
flex = 1,
|
||||||
|
border = true,
|
||||||
|
title = 'Demo page ' .. page,
|
||||||
|
children = {
|
||||||
|
CurrentPage(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Footer(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
end
|
||||||
|
|
||||||
|
local finalEvent = ui.render(App);
|
||||||
|
|
||||||
|
if finalEvent.type == 'terminate' then
|
||||||
|
print('> User terminated the app');
|
||||||
|
elseif finalEvent.type == 'error' then
|
||||||
|
print('> error name: ' .. tostring(finalEvent.error.name));
|
||||||
|
print('> error reason/details: ' .. tostring(finalEvent.error.reason));
|
||||||
|
elseif finalEvent.type == 'exitUI' then
|
||||||
|
print('> User exited the app using the UI');
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue
Block a user