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_TEXT = 'text';
|
||||||
local NODE_BUTTON = 'button';
|
local NODE_BUTTON = 'button';
|
||||||
@ -65,6 +66,64 @@ local function shallowCopy(value)
|
|||||||
return result;
|
return result;
|
||||||
end
|
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)
|
local function makeNode(kind, props)
|
||||||
props = props or {};
|
props = props or {};
|
||||||
return {
|
return {
|
||||||
@ -116,11 +175,12 @@ local function lineText(value, width)
|
|||||||
return '';
|
return '';
|
||||||
end
|
end
|
||||||
|
|
||||||
if string.len(value) > width then
|
local valueLength = utf8Len(value);
|
||||||
return string.sub(value, 1, width);
|
if valueLength > width then
|
||||||
|
return utf8Sub(value, 1, width);
|
||||||
end
|
end
|
||||||
|
|
||||||
return value .. string.rep(' ', width - string.len(value));
|
return value .. string.rep(' ', width - valueLength);
|
||||||
end
|
end
|
||||||
|
|
||||||
local function shrinkRect(rect, amount)
|
local function shrinkRect(rect, amount)
|
||||||
@ -171,8 +231,9 @@ local function drawBorder(rect, props)
|
|||||||
|
|
||||||
if title then
|
if title then
|
||||||
local titleText = ' ' .. tostring(title) .. ' ';
|
local titleText = ' ' .. tostring(title) .. ' ';
|
||||||
if string.len(titleText) < rect.w - 1 then
|
local titleLength = utf8Len(titleText);
|
||||||
top = '+' .. titleText .. string.rep('-', rect.w - 2 - string.len(titleText)) .. '+';
|
if titleLength < rect.w - 1 then
|
||||||
|
top = '+' .. titleText .. string.rep('-', rect.w - 2 - titleLength) .. '+';
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -314,11 +375,11 @@ function naturalSize(node)
|
|||||||
local props = node.props or {};
|
local props = node.props or {};
|
||||||
|
|
||||||
if node.kind == NODE_TEXT then
|
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
|
end
|
||||||
|
|
||||||
if node.kind == NODE_BUTTON then
|
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
|
end
|
||||||
|
|
||||||
local direction = props.direction or 'column';
|
local direction = props.direction or 'column';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TrapOS",
|
"name": "TrapOS",
|
||||||
"version": "0.5.2",
|
"version": "0.5.3",
|
||||||
"branch": "next",
|
"branch": "next",
|
||||||
"packages": [
|
"packages": [
|
||||||
"trapos"
|
"trapos"
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
"trapos-test": "0.2.0",
|
"trapos-test": "0.2.0",
|
||||||
"trapos-boot": "0.2.1",
|
"trapos-boot": "0.2.1",
|
||||||
"trapos-net": "0.2.0",
|
"trapos-net": "0.2.0",
|
||||||
"trapos-ui": "0.2.0",
|
"trapos-ui": "0.2.1",
|
||||||
"trapos-ai": "0.3.0",
|
"trapos-ai": "0.3.0",
|
||||||
"trapos": "0.5.2"
|
"trapos": "0.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trapos-ui",
|
"name": "trapos-ui",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"description": "TrapOS terminal UI toolkit and demo",
|
"description": "TrapOS terminal UI toolkit and demo",
|
||||||
"dependencies": ["trapos-core"],
|
"dependencies": ["trapos-core"],
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trapos",
|
"name": "trapos",
|
||||||
"version": "0.5.2",
|
"version": "0.5.3",
|
||||||
"description": "TrapOS full install meta-package",
|
"description": "TrapOS full install meta-package",
|
||||||
"dependencies": ["trapos-boot", "trapos-net", "trapos-ui", "trapos-test", "trapos-ai"],
|
"dependencies": ["trapos-boot", "trapos-net", "trapos-ui", "trapos-test", "trapos-ai"],
|
||||||
"files": [],
|
"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