fix(ui): handle utf8 text widths
This commit is contained in:
parent
b2a68fc11a
commit
208b7282d8
@ -1,4 +1,5 @@
|
||||
local _VERSION = '0.1.1';
|
||||
local _VERSION = '0.1.2';
|
||||
local utf8 = rawget(_G, 'utf8');
|
||||
|
||||
local NODE_TEXT = 'text';
|
||||
local NODE_BUTTON = 'button';
|
||||
@ -65,6 +66,64 @@ local function shallowCopy(value)
|
||||
return result;
|
||||
end
|
||||
|
||||
local function utf8Len(value)
|
||||
value = tostring(value or '');
|
||||
|
||||
local ok, iter, state, init = pcall(utf8.codes, value);
|
||||
if not ok then
|
||||
return string.len(value);
|
||||
end
|
||||
|
||||
local count = 0;
|
||||
for _ in iter, state, init do
|
||||
count = count + 1;
|
||||
end
|
||||
|
||||
return count;
|
||||
end
|
||||
|
||||
local function utf8Sub(value, startIndex, endIndex)
|
||||
value = tostring(value or '');
|
||||
startIndex = math.max(1, math.floor(startIndex or 1));
|
||||
endIndex = endIndex == nil and math.huge or math.floor(endIndex);
|
||||
|
||||
if endIndex < startIndex then
|
||||
return '';
|
||||
end
|
||||
|
||||
local ok, iter, state, init = pcall(utf8.codes, value);
|
||||
if not ok then
|
||||
return string.sub(value, startIndex, endIndex);
|
||||
end
|
||||
|
||||
local byteStart;
|
||||
local byteEnd;
|
||||
local index = 0;
|
||||
|
||||
for pos, _ in iter, state, init do
|
||||
index = index + 1;
|
||||
|
||||
if index == startIndex then
|
||||
byteStart = pos;
|
||||
end
|
||||
|
||||
if index == endIndex + 1 then
|
||||
byteEnd = pos - 1;
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
if not byteStart then
|
||||
return '';
|
||||
end
|
||||
|
||||
if not byteEnd then
|
||||
byteEnd = string.len(value);
|
||||
end
|
||||
|
||||
return string.sub(value, byteStart, byteEnd);
|
||||
end
|
||||
|
||||
local function makeNode(kind, props)
|
||||
props = props or {};
|
||||
return {
|
||||
@ -116,11 +175,12 @@ local function lineText(value, width)
|
||||
return '';
|
||||
end
|
||||
|
||||
if string.len(value) > width then
|
||||
return string.sub(value, 1, width);
|
||||
local valueLength = utf8Len(value);
|
||||
if valueLength > width then
|
||||
return utf8Sub(value, 1, width);
|
||||
end
|
||||
|
||||
return value .. string.rep(' ', width - string.len(value));
|
||||
return value .. string.rep(' ', width - valueLength);
|
||||
end
|
||||
|
||||
local function shrinkRect(rect, amount)
|
||||
@ -171,8 +231,9 @@ local function drawBorder(rect, props)
|
||||
|
||||
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)) .. '+';
|
||||
local titleLength = utf8Len(titleText);
|
||||
if titleLength < rect.w - 1 then
|
||||
top = '+' .. titleText .. string.rep('-', rect.w - 2 - titleLength) .. '+';
|
||||
end
|
||||
end
|
||||
|
||||
@ -314,11 +375,11 @@ function naturalSize(node)
|
||||
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;
|
||||
return props.width or utf8Len(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;
|
||||
return props.width or utf8Len(buttonLabel(props)), props.height or 1;
|
||||
end
|
||||
|
||||
local direction = props.direction or 'column';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "TrapOS",
|
||||
"version": "0.5.2",
|
||||
"version": "0.5.3",
|
||||
"branch": "next",
|
||||
"packages": [
|
||||
"trapos"
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
"trapos-test": "0.2.0",
|
||||
"trapos-boot": "0.2.1",
|
||||
"trapos-net": "0.2.0",
|
||||
"trapos-ui": "0.2.0",
|
||||
"trapos-ui": "0.2.1",
|
||||
"trapos-ai": "0.3.0",
|
||||
"trapos": "0.5.2"
|
||||
"trapos": "0.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos-ui",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"description": "TrapOS terminal UI toolkit and demo",
|
||||
"dependencies": ["trapos-core"],
|
||||
"files": [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos",
|
||||
"version": "0.5.2",
|
||||
"version": "0.5.3",
|
||||
"description": "TrapOS full install meta-package",
|
||||
"dependencies": ["trapos-boot", "trapos-net", "trapos-ui", "trapos-test", "trapos-ai"],
|
||||
"files": [],
|
||||
|
||||
133
tests/libtui.lua
Normal file
133
tests/libtui.lua
Normal file
@ -0,0 +1,133 @@
|
||||
-- luacheck: globals term
|
||||
|
||||
local createLibTest = require('/apis/libtest');
|
||||
local createTui = require('/apis/libtui');
|
||||
|
||||
local testlib = createLibTest({ ... });
|
||||
|
||||
local function fakeEventLoop()
|
||||
local onStart = nil;
|
||||
local onStop = nil;
|
||||
|
||||
return {
|
||||
onStart = function(fn) onStart = fn; end,
|
||||
onStop = function(fn) onStop = fn; end,
|
||||
register = function() end,
|
||||
startLoop = function()
|
||||
if onStart then
|
||||
onStart();
|
||||
end
|
||||
return true;
|
||||
end,
|
||||
stopLoop = function()
|
||||
if onStop then
|
||||
onStop();
|
||||
end
|
||||
end,
|
||||
isRunningLoop = function()
|
||||
return false;
|
||||
end,
|
||||
};
|
||||
end
|
||||
|
||||
local function withFakeTerm(fn)
|
||||
local originalTerm = term;
|
||||
local writes = {};
|
||||
local state = {
|
||||
textColor = colors.white,
|
||||
bgColor = colors.black,
|
||||
cursorX = 1,
|
||||
cursorY = 1,
|
||||
cursorBlink = false,
|
||||
width = 20,
|
||||
height = 5,
|
||||
};
|
||||
|
||||
term = {
|
||||
setCursorBlink = function(value) state.cursorBlink = value; end,
|
||||
setTextColor = function(value) state.textColor = value; end,
|
||||
setBackgroundColor = function(value) state.bgColor = value; end,
|
||||
clear = function() writes[#writes + 1] = { kind = 'clear' }; end,
|
||||
getTextColor = function() return state.textColor; end,
|
||||
getBackgroundColor = function() return state.bgColor; end,
|
||||
getCursorPos = function() return state.cursorX, state.cursorY; end,
|
||||
getCursorBlink = function() return state.cursorBlink; end,
|
||||
setCursorPos = function(x, y)
|
||||
state.cursorX = x;
|
||||
state.cursorY = y;
|
||||
end,
|
||||
getSize = function() return state.width, state.height; end,
|
||||
write = function(text)
|
||||
writes[#writes + 1] = {
|
||||
kind = 'write',
|
||||
text = text,
|
||||
x = state.cursorX,
|
||||
y = state.cursorY,
|
||||
color = state.textColor,
|
||||
bgColor = state.bgColor,
|
||||
};
|
||||
state.cursorX = state.cursorX + string.len(text);
|
||||
end,
|
||||
};
|
||||
|
||||
local ok, err = pcall(fn, writes, state);
|
||||
term = originalTerm;
|
||||
|
||||
if not ok then
|
||||
error(err, 0);
|
||||
end
|
||||
end
|
||||
|
||||
testlib.test('text nodes pad utf8 text by characters', function()
|
||||
withFakeTerm(function(writes)
|
||||
local ui = createTui(fakeEventLoop());
|
||||
ui.render(ui.Box({
|
||||
children = {
|
||||
ui.Box({
|
||||
width = 3,
|
||||
height = 1,
|
||||
children = { ui.Text('é') },
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
local found = false;
|
||||
for _, item in ipairs(writes) do
|
||||
if item.kind == 'write' and item.text == 'é ' then
|
||||
found = true;
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
testlib.assertTrue(found, 'expected padded utf8 text');
|
||||
end);
|
||||
end);
|
||||
|
||||
testlib.test('box borders keep utf8 titles intact', function()
|
||||
withFakeTerm(function(writes)
|
||||
local ui = createTui(fakeEventLoop());
|
||||
ui.render(ui.Box({
|
||||
children = {
|
||||
ui.Box({
|
||||
width = 5,
|
||||
height = 3,
|
||||
border = true,
|
||||
title = 'Ç',
|
||||
children = {},
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
local found = false;
|
||||
for _, item in ipairs(writes) do
|
||||
if item.kind == 'write' and item.text == '+ Ç +' then
|
||||
found = true;
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
testlib.assertTrue(found, 'expected utf8 border title');
|
||||
end);
|
||||
end);
|
||||
|
||||
testlib.run();
|
||||
Loading…
Reference in New Issue
Block a user