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",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"branch": "master",
|
||||
"files": [
|
||||
"startup/motd.lua",
|
||||
@ -9,9 +9,11 @@
|
||||
"programs/router.lua",
|
||||
"programs/events.lua",
|
||||
"programs/ping.lua",
|
||||
"programs/tuidemo.lua",
|
||||
"programs/upgrade.lua",
|
||||
"apis/net.lua",
|
||||
"apis/eventloop.lua"
|
||||
"apis/eventloop.lua",
|
||||
"apis/libtui.lua"
|
||||
],
|
||||
"autostart": [
|
||||
"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