fix(ui): handle utf8 text widths

This commit is contained in:
Guillaume ARM 2026-06-09 05:18:59 +02:00
parent b2a68fc11a
commit 208b7282d8
6 changed files with 207 additions and 13 deletions

View File

@ -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';

View File

@ -1,6 +1,6 @@
{
"name": "TrapOS",
"version": "0.5.2",
"version": "0.5.3",
"branch": "next",
"packages": [
"trapos"

View File

@ -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"
}
}

View File

@ -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": [

View File

@ -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
View 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();