From 9dcffeda9017bdf0573bd45a4a3c8c716f92aa54 Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Sat, 4 Jul 2026 17:14:06 +0800 Subject: [PATCH 01/12] Extend text layout queries --- docs/material_text.lua | 124 ++++++++++++++++++++++++-- src/material_text.c | 192 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 304 insertions(+), 12 deletions(-) diff --git a/docs/material_text.lua b/docs/material_text.lua index 4ecfb00..b3ed081 100644 --- a/docs/material_text.lua +++ b/docs/material_text.lua @@ -1,27 +1,133 @@ ---@meta soluna.material.text ----文本块创建函数 +---text style 配置。 +---Text style configuration. +---@class soluna.material.text.Style +---@field font integer `font.name()` 返回的字体 id / Font id returned by `font.name()` +---@field size? integer 字体像素大小,默认 24 / Font pixel size, default 24 +---@field color? integer ARGB 颜色,默认 `0xff000000` / ARGB color, default `0xff000000` +---@field line_height? integer 行高;小于字体自然高度时使用自然高度 / Line height; clamped to the natural font height + +---text style set。 +---Text style set. +---@class soluna.material.text.Styles: userdata + +---文本行信息。 +---Text line information. +--- +---`start` 和 `finish` 是 0-based visible position,`finish` 为 exclusive。 +---`start` and `finish` are 0-based visible positions. `finish` is exclusive. +---@class soluna.material.text.Line +---@field start integer 行起始 visible position / First visible position on the line +---@field finish integer 行结束 visible position,exclusive / End visible position, exclusive +---@field x integer 行矩形 X / Line rectangle X +---@field y integer 行矩形 Y / Line rectangle Y +---@field width integer 行矩形宽度 / Line rectangle width +---@field height integer 行矩形高度 / Line rectangle height + +---文本布局查询对象。 +---Text layout query object. +---@class soluna.material.text.Layout +local Layout = {} + +---返回文本实际高度。 +---Returns text content height. +---@return integer height 文本实际高度 / Text content height +function Layout:height() +end + +---返回最大行高。 +---Returns maximum line height. +---@return integer height 最大行高 / Maximum line height +function Layout:line_height() +end + +---返回行数。 +---Returns line count. +---@return integer count 行数 / Line count +function Layout:line_count() +end + +---返回 cursor position 数量。 +---Returns cursor position count. +---@return integer count cursor position 数量 / Cursor position count +function Layout:cursor_count() +end + +---返回 1-based 行信息。 +---Returns 1-based line information. +---@param index integer 1-based 行号 / 1-based line index +---@return soluna.material.text.Line? line 行信息 / Line information +function Layout:line(index) +end + +---查询 cursor 矩形。 +---Queries cursor rectangle. +---@param position integer 0-based cursor position / 0-based cursor position +---@return integer x cursor X / Cursor X +---@return integer y cursor Y / Cursor Y +---@return integer width cursor 宽度 / Cursor width +---@return integer height cursor 高度 / Cursor height +---@return integer position clamp 后的 cursor position / Clamped cursor position +---@return integer decent 字体 descent / Font descent +function Layout:cursor(position) +end + +---命中测试,返回 0-based cursor position。 +---Hit-tests and returns a 0-based cursor position. +---@param x number 本地 X / Local X +---@param y number 本地 Y / Local Y +---@return integer position cursor position / Cursor position +---@return boolean out_of_box 是否在布局盒外 / Whether the point is outside the layout box +function Layout:hit_test(x, y) +end + +---返回 visible position 使用的 0-based style id。 +---Returns the 0-based style id at a visible position. +---@param position integer 0-based visible position / 0-based visible position +---@return integer? style style id;无效 position 返回 nil / Style id; nil for invalid position +function Layout:style(position) +end + +---文本块创建函数。 ---Text block builder function. ---@alias soluna.material.text.Block fun(text: string, width?: integer, height?: integer): string, integer ----光标位置查询函数 ----Text cursor query function. ----@alias soluna.material.text.Cursor fun(text: string, position: integer, width?: integer, height?: integer): integer, integer, integer, integer, integer, integer +---文本布局创建函数。 +---Text layout builder function. +---@alias soluna.material.text.LayoutBuilder fun(text: string, width?: integer, height?: integer): soluna.material.text.Layout ----text material 模块 +---text material 模块。 ---Text material module. +--- +---Tagged text stream 支持 `[i42]` icon、`[hex]` 临时颜色、`[sN]` style 切换、 +---`[s]` 回到 style 0,以及 `[[` 输出字面量 `[`。`[sN]` 中的 `N` 是 +---0-based style id,Lua style 数组第一项对应 style 0。 +--- +---Tagged text streams support `[i42]` icons, `[hex]` temporary colors, `[sN]` +---style switching, `[s]` reset to style 0, and `[[` for a literal `[`. +---`N` in `[sN]` is a 0-based style id. The first Lua style entry is style 0. ---@class soluna.material.text local mattext = {} ----创建文本块和光标查询函数 ----Creates text block and cursor query functions. +---创建 style set。 +---Creates a style set. +---@param fontcobj lightuserdata `font.cobj()` 返回的字体管理器指针 / Font manager pointer returned by `font.cobj()` +---@param styles soluna.material.text.Style[] style 数组,第一项是 style 0 / Style array; first entry is style 0 +---@return soluna.material.text.Styles styles style set / Style set +function mattext.styles(fontcobj, styles) +end + +---创建文本块和布局查询函数。 +---Creates text block and layout query functions. +---@overload fun(styles: soluna.material.text.Styles, alignment?: string): soluna.material.text.Block, soluna.material.text.LayoutBuilder ---@param fontcobj lightuserdata `font.cobj()` 返回的字体管理器指针 / Font manager pointer returned by `font.cobj()` ---@param fontid integer `font.name()` 返回的字体 id / Font id returned by `font.name()` ----@param size? integer 字体像素大小,默认 16 / Font pixel size, default 16 +---@param size? integer 字体像素大小,默认 24 / Font pixel size, default 24 ---@param color? integer ARGB 颜色,默认 `0xff000000` / ARGB color, default `0xff000000` ---@param alignment? string 对齐代码,如 `"LT"`、`"CV"`、`"RB"` / Alignment code such as `"LT"`, `"CV"`, `"RB"` ---@return soluna.material.text.Block block 创建 packed text stream / Creates packed text stream ----@return soluna.material.text.Cursor cursor 查询光标矩形 / Queries cursor rectangle +---@return soluna.material.text.LayoutBuilder layout 创建布局查询对象 / Creates layout query object function mattext.block(fontcobj, fontid, size, color, alignment) end diff --git a/src/material_text.c b/src/material_text.c index af285c7..393cb98 100644 --- a/src/material_text.c +++ b/src/material_text.c @@ -351,6 +351,7 @@ struct font_style { uint32_t color; int fontid; int size; + int line_height; // read from font int ascent; int decent; @@ -367,6 +368,7 @@ static int get_field(lua_State *L, const char *key, int def) { int t = lua_getfield(L, -1, key); if (t == LUA_TNIL) { + lua_pop(L, 1); return def; } else if (t != LUA_TNUMBER) { return luaL_error(L, ".%s should be integer", key); @@ -392,6 +394,7 @@ read_style(lua_State *L, int index, struct styles *s, int i) { if (!(fs->color & 0xff000000)) fs->color |= 0xff000000; fs->size = get_field(L, "size", DEFAULT_FONTSIZE); + fs->line_height = get_field(L, "line_height", 0); lua_pop(L, 1); } @@ -405,9 +408,19 @@ style_getinfo(struct styles *s) { font_manager_fontheight(mgr, fs->fontid, fs->size, &ascent, &decent, &gap); if (gap == 0) gap = 1; + int natural_height = ascent - decent; fs->ascent = ascent; - fs->decent = -decent + gap; - fs->gap = gap; + if (fs->line_height > 0) { + if (fs->line_height < natural_height) { + fs->line_height = natural_height; + } + fs->decent = fs->line_height - ascent; + fs->gap = 0; + } else { + fs->line_height = natural_height; + fs->decent = -decent + gap; + fs->gap = gap; + } } } @@ -436,6 +449,7 @@ ltext_styles(lua_State *L) { struct block_context { struct styles *s; struct font_style *fs; + int style; int width; int height; int x; @@ -457,6 +471,7 @@ struct position { int gap; int ascent; int decent; + int style; }; struct layout { @@ -584,6 +599,7 @@ parse_bracket(struct block_context *ctx, const char *str, int *icon) { if (style < 0 || style >=n) { style = 0; } + ctx->style = style; ctx->fs = &ctx->s->s[style]; ctx->color = ctx->fs->color; } else if ((hex = tohex(c)) >= 0) { @@ -600,6 +616,7 @@ parse_bracket(struct block_context *ctx, const char *str, int *icon) { color |= 0xff000000; ctx->color = color; } else if (c == 'n') { + ctx->style = 0; ctx->fs = &ctx->s->s[0]; ctx->color = ctx->fs->color; } @@ -614,6 +631,7 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { struct block_context ctx; ctx.s = s; ctx.fs = &s->s[0]; + ctx.style = 0; ctx.width = luaL_optinteger(L, 2, MAX_WIDTH); ctx.height = luaL_optinteger(L, 3, MAX_HEIGHT); @@ -703,6 +721,7 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { p->gap = 0; p->ascent = ctx.fs->ascent; p->decent = ctx.fs->decent; + p->style = ctx.style; pos->n = n + 1; } @@ -725,6 +744,7 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { p->gap = 0; p->ascent = ctx.fs->ascent; p->decent = ctx.fs->decent; + p->style = ctx.style; } advance(pos, &ctx, g.advance_x); } @@ -754,6 +774,7 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { p->gap = 0; p->ascent = ctx.fs->ascent; p->decent = ctx.fs->decent; + p->style = ctx.style; ctx.line_ascent = ctx.fs->ascent; ctx.line_decent = ctx.fs->decent; ctx.line_height = ctx.fs->ascent + ctx.fs->decent - ctx.fs->gap; @@ -817,6 +838,7 @@ get_styles(lua_State *L, struct styles *tmp) { fs->fontid = fontid; fs->size = lua_tointeger(L, lua_upvalueindex(3)); fs->color = lua_tointeger(L, lua_upvalueindex(4)); + fs->line_height = 0; style_getinfo(tmp); return tmp; @@ -857,6 +879,164 @@ ltext_cursor(lua_State *L) { return 6; } +static int +ltext_height(lua_State *L) { + struct layout * pos = (struct layout *)lua_touserdata(L, 1); + lua_pushinteger(L, pos->text_height); + return 1; +} + +static int +ltext_line_height(lua_State *L) { + struct layout * pos = (struct layout *)lua_touserdata(L, 1); + int n = pos->n; + if (n <= 0) { + lua_pushinteger(L, pos->text_height); + return 1; + } + int height = 0; + int bottom = pos->top + pos->text_height; + int i; + for (i=0;ipos[i]; + int line_height = p->h + p->gap; + if (p->y + line_height > bottom) { + line_height = bottom - p->y; + } + if (line_height > height) { + height = line_height; + } + } + lua_pushinteger(L, height); + return 1; +} + +static int +ltext_line_count(lua_State *L) { + struct layout * pos = (struct layout *)lua_touserdata(L, 1); + int n = pos->n; + if (n <= 0) { + lua_pushinteger(L, pos->text_height > 0 ? 1 : 0); + return 1; + } + int count = 1; + int y = pos->pos[0].y; + int i; + for (i=1;ipos[i].y != y) { + y = pos->pos[i].y; + ++count; + } + } + lua_pushinteger(L, count); + return 1; +} + +static int +ltext_cursor_count(lua_State *L) { + struct layout * pos = (struct layout *)lua_touserdata(L, 1); + lua_pushinteger(L, pos->n + 1); + return 1; +} + +static void +set_integer_field(lua_State *L, const char *key, int value) { + lua_pushinteger(L, value); + lua_setfield(L, -2, key); +} + +static void +push_line(lua_State *L, struct layout *pos, int start, int finish) { + int x0 = pos->pos[start].x; + int x1 = x0; + int y = pos->pos[start].y; + int y1 = y; + int text_bottom = pos->top + pos->text_height; + int i; + for (i=start;ipos[i]; + if (p->x < x0) { + x0 = p->x; + } + int right = p->x + p->w; + if (right > x1) { + x1 = right; + } + int bottom = p->y + p->h + p->gap; + if (bottom > y1) { + y1 = bottom; + } + } + if (y1 > text_bottom) { + y1 = text_bottom; + } + lua_createtable(L, 0, 6); + set_integer_field(L, "start", start); + set_integer_field(L, "finish", finish); + set_integer_field(L, "x", x0); + set_integer_field(L, "y", y); + set_integer_field(L, "width", x1 - x0); + set_integer_field(L, "height", y1 > y ? y1 - y : 0); +} + +static int +ltext_line(lua_State *L) { + struct layout * pos = (struct layout *)lua_touserdata(L, 1); + int line = luaL_checkinteger(L, 2); + if (line <= 0) { + lua_pushnil(L); + return 1; + } + int n = pos->n; + if (n <= 0) { + if (line != 1) { + lua_pushnil(L); + return 1; + } + lua_createtable(L, 0, 6); + set_integer_field(L, "start", 0); + set_integer_field(L, "finish", 0); + set_integer_field(L, "x", 0); + set_integer_field(L, "y", pos->top); + set_integer_field(L, "width", 0); + set_integer_field(L, "height", pos->text_height); + return 1; + } + int current_line = 1; + int start = 0; + int y = pos->pos[0].y; + int i; + for (i=1;ipos[i].y != y) { + if (current_line == line) { + push_line(L, pos, start, i); + return 1; + } + ++current_line; + start = i; + y = pos->pos[i].y; + } + } + if (current_line == line) { + push_line(L, pos, start, n); + return 1; + } + lua_pushnil(L); + return 1; +} + +static int +ltext_style(lua_State *L) { + struct layout * pos = (struct layout *)lua_touserdata(L, 1); + int n = luaL_checkinteger(L, 2); + if (n < 0 || n >= pos->n) { + lua_pushnil(L); + return 1; + } + lua_pushinteger(L, pos->pos[n].style); + return 1; +} + static int get_pos(lua_State *L, int index) { int isnum; @@ -901,7 +1081,7 @@ ltext_hit_test(lua_State *L) { return 2; } if (y - top >= pos->text_height) { - lua_pushinteger(L, pos->n-1); + lua_pushinteger(L, pos->n); lua_pushboolean(L, y >= pos->height); // out of box return 2; } @@ -1008,8 +1188,14 @@ luaopen_material_text(lua_State *L) { if (luaL_newmetatable(L, "SOLUNA_TEXT_LAYOUT")) { luaL_Reg meta[] = { + { "height", ltext_height }, + { "line_height", ltext_line_height }, + { "line_count", ltext_line_count }, + { "cursor_count", ltext_cursor_count }, + { "line", ltext_line }, { "cursor", ltext_cursor }, { "hit_test", ltext_hit_test }, + { "style", ltext_style }, { NULL, NULL }, }; luaL_setfuncs(L, meta, 0); From 24f6d50d93fae62b6521b239809e10ca776779f7 Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Sat, 4 Jul 2026 17:14:12 +0800 Subject: [PATCH 02/12] Add clip material rect command --- docs/material_clip.lua | 27 ++++ src/embedlua.c | 2 + src/luamods.c | 2 + src/material/matclip.lua | 25 ++++ src/material_clip.c | 267 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 323 insertions(+) create mode 100644 docs/material_clip.lua create mode 100644 src/material/matclip.lua create mode 100644 src/material_clip.c diff --git a/docs/material_clip.lua b/docs/material_clip.lua new file mode 100644 index 0000000..06e0fc5 --- /dev/null +++ b/docs/material_clip.lua @@ -0,0 +1,27 @@ +---@meta soluna.material.clip + +---clip material 模块。 +---Clip material module. +--- +---`rect(width, height)` 打开一个矩形裁剪范围,`rect()` 关闭最近一次打开的裁剪范围。 +---裁剪矩形位于屏幕空间,是轴对齐矩形;它不是 layer-local clip,也不为旋转 layer +---提供精确裁剪。 +--- +---`rect(width, height)` opens a rectangular clipping region, and `rect()` +---closes the latest opened clipping region. The clipping rectangle is +---screen-space and axis-aligned. It is not a layer-local clip and does not +---provide exact clipping for rotated layers. +---@class soluna.material.clip +local matclip = {} + +---打开或关闭矩形裁剪范围。 +---Opens or closes a rectangular clipping region. +---@overload fun(): string +---@overload fun(width: number, height: number): string +---@param width number 逻辑像素宽度 / Width in logical pixels. +---@param height number 逻辑像素高度 / Height in logical pixels. +---@return string stream 打包后的 clip 命令流 / Packed clip command stream. +function matclip.rect(width, height) +end + +return matclip diff --git a/src/embedlua.c b/src/embedlua.c index d9fe9c9..2469089 100644 --- a/src/embedlua.c +++ b/src/embedlua.c @@ -26,6 +26,7 @@ #include "mattext.lua.h" #include "matquad.lua.h" #include "matmask.lua.h" +#include "matclip.lua.h" #include "lua.h" #include "lauxlib.h" @@ -102,6 +103,7 @@ luaopen_embedsource(lua_State *L) { REG_MATERIAL(mattext) REG_MATERIAL(matquad) REG_MATERIAL(matmask) + REG_MATERIAL(matclip) lua_setfield(L, -2, "material"); lua_newtable(L); // data list diff --git a/src/luamods.c b/src/luamods.c index 764c2f5..af3d965 100644 --- a/src/luamods.c +++ b/src/luamods.c @@ -21,6 +21,7 @@ int luaopen_material_default(lua_State *L); int luaopen_material_text(lua_State *L); int luaopen_material_quad(lua_State *L); int luaopen_material_mask(lua_State *L); +int luaopen_material_clip(lua_State *L); int luaopen_material_blit(lua_State *L); int luaopen_material_external(lua_State *L); int luaopen_soluna_app(lua_State *L); @@ -53,6 +54,7 @@ void soluna_embed(lua_State* L) { { "soluna.material.text", luaopen_material_text }, { "soluna.material.quad", luaopen_material_quad }, { "soluna.material.mask", luaopen_material_mask }, + { "soluna.material.clip", luaopen_material_clip }, { "soluna.material.blit", luaopen_material_blit }, { "soluna.material.ext", luaopen_material_external }, { "soluna.datalist", luaopen_datalist }, diff --git a/src/material/matclip.lua b/src/material/matclip.lua new file mode 100644 index 0000000..a61137f --- /dev/null +++ b/src/material/matclip.lua @@ -0,0 +1,25 @@ +local clipmat = require "soluna.material.clip" + +local ctx = ... +local state = ctx.state +clipmat.set_material_id(ctx.id) + +state.material_clip = clipmat.new { + uniform = state.uniform, +} + +local material = {} + +function material.reset() + state.material_clip:reset() +end + +function material.submit(ptr, n) + state.material_clip:submit(ptr, n) +end + +function material.draw(ptr, n) + state.material_clip:draw(ptr, n) +end + +return material diff --git a/src/material_clip.c b/src/material_clip.c new file mode 100644 index 0000000..709c398 --- /dev/null +++ b/src/material_clip.c @@ -0,0 +1,267 @@ +#include +#include +#include +#include +#include +#include + +#include "sokol/sokol_gfx.h" +#include "batch.h" +#include "material_util.h" + +#define PIXEL_SCALE 256.0f +#define CLIP_STACK_MAX 64 + +enum scissor_command { + SCISSOR_PUSH = 1, + SCISSOR_POP = 2, +}; + +struct scissor_rect { + int x; + int y; + int w; + int h; +}; + +struct scissor_payload { + struct draw_primitive_external header; + int command; + float w; + float h; +}; + +struct scissor_primitive { + struct draw_primitive pos; + union { + struct draw_primitive dummy; + struct scissor_payload scissor; + } u; +}; + +struct material_clip { + float *uniform; + int stack_n; + struct scissor_rect stack[CLIP_STACK_MAX]; +}; + +static int material_id = 0; + +static struct scissor_rect +full_scissor(struct material_clip *m) { + float sx = m->uniform[0]; + float sy = m->uniform[1]; + int w = sx == 0.0f ? 0 : (int)floorf(2.0f / sx + 0.5f); + int h = sy == 0.0f ? 0 : (int)floorf(-2.0f / sy + 0.5f); + struct scissor_rect r = { 0, 0, w, h }; + return r; +} + +static struct scissor_rect +intersect_scissor(struct scissor_rect a, struct scissor_rect b) { + int x0 = a.x > b.x ? a.x : b.x; + int y0 = a.y > b.y ? a.y : b.y; + int x1 = a.x + a.w < b.x + b.w ? a.x + a.w : b.x + b.w; + int y1 = a.y + a.h < b.y + b.h ? a.y + a.h : b.y + b.h; + struct scissor_rect r = { + x0, + y0, + x1 > x0 ? x1 - x0 : 0, + y1 > y0 ? y1 - y0 : 0, + }; + return r; +} + +static void +apply_scissor(struct scissor_rect r) { + sg_apply_scissor_rect(r.x, r.y, r.w, r.h, true); +} + +static void +sr_matrix(uint32_t sr, float m[4]) { + uint32_t scale_fix = sr >> 12; + float scale = 1.0f; + if (scale_fix != 0) { + if (scale_fix >= 0xff000) { + scale = (float)(scale_fix & 0xfff) * (1.0f / 4096.0f); + } else { + scale = (float)scale_fix * (1.0f / 256.0f) + 1.0f; + } + } + uint32_t rot_fix = sr & 0xfff; + if (rot_fix == 0) { + m[0] = scale; + m[1] = 0.0f; + m[2] = 0.0f; + m[3] = scale; + } else { + const float pi = 3.1415927f; + float rot = (float)rot_fix * (pi / 2048.0f); + float cosr = cosf(rot) * scale; + float sinr = sinf(rot) * scale; + m[0] = cosr; + m[1] = -sinr; + m[2] = sinr; + m[3] = cosr; + } +} + +static void +point_bounds(float base_x, float base_y, const float m[4], float x, float y, float *min_x, float *min_y, float *max_x, float *max_y) { + float tx = base_x + x * m[0] + y * m[1]; + float ty = base_y + x * m[2] + y * m[3]; + if (tx < *min_x) + *min_x = tx; + if (tx > *max_x) + *max_x = tx; + if (ty < *min_y) + *min_y = ty; + if (ty > *max_y) + *max_y = ty; +} + +static struct scissor_rect +primitive_scissor(struct draw_primitive *pos, struct scissor_payload *payload) { + float base_x = (float)pos->x / PIXEL_SCALE; + float base_y = (float)pos->y / PIXEL_SCALE; + float m[4]; + sr_matrix(pos->sr, m); + float min_x = base_x; + float min_y = base_y; + float max_x = base_x; + float max_y = base_y; + point_bounds(base_x, base_y, m, payload->w, 0.0f, &min_x, &min_y, &max_x, &max_y); + point_bounds(base_x, base_y, m, 0.0f, payload->h, &min_x, &min_y, &max_x, &max_y); + point_bounds(base_x, base_y, m, payload->w, payload->h, &min_x, &min_y, &max_x, &max_y); + struct scissor_rect r = { + (int)floorf(min_x), + (int)floorf(min_y), + (int)ceilf(max_x) - (int)floorf(min_x), + (int)ceilf(max_y) - (int)floorf(min_y), + }; + return r; +} + +static int +lmaterial_clip_reset(lua_State *L) { + struct material_clip *m = (struct material_clip *)luaL_checkudata(L, 1, "SOLUNA_MATERIAL_CLIP"); + m->stack_n = 0; + return 0; +} + +static int +lmaterial_clip_submit(lua_State *L) { + return 0; +} + +static int +lmaterial_clip_draw(lua_State *L) { + struct material_clip *m = (struct material_clip *)luaL_checkudata(L, 1, "SOLUNA_MATERIAL_CLIP"); + struct draw_primitive *prim = lua_touserdata(L, 2); + int prim_n = luaL_checkinteger(L, 3); + int i; + for (i=0;isprite == -material_id); + struct scissor_payload *payload = (struct scissor_payload *)&prim[i*2+1]; + switch (payload->command) { + case SCISSOR_PUSH: { + if (m->stack_n >= CLIP_STACK_MAX) + return luaL_error(L, "Too many nested clip commands"); + struct scissor_rect current = m->stack_n > 0 ? m->stack[m->stack_n - 1] : full_scissor(m); + struct scissor_rect next = intersect_scissor(current, primitive_scissor(pos, payload)); + m->stack[m->stack_n++] = next; + apply_scissor(next); + break; + } + case SCISSOR_POP: + if (m->stack_n <= 0) + return luaL_error(L, "Clip close without open"); + --m->stack_n; + apply_scissor(m->stack_n > 0 ? m->stack[m->stack_n - 1] : full_scissor(m)); + break; + default: + return luaL_error(L, "Invalid clip command %d", payload->command); + } + } + return 0; +} + +static int +lnew_material_clip(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + struct material_clip *m = (struct material_clip *)lua_newuserdatauv(L, sizeof(*m), 1); + memset(m, 0, sizeof(*m)); + util_ref_object(L, &m->uniform, 1, "uniform", "SOKOL_UNIFORM", 1); + + if (luaL_newmetatable(L, "SOLUNA_MATERIAL_CLIP")) { + luaL_Reg l[] = { + { "__index", NULL }, + { "reset", lmaterial_clip_reset }, + { "submit", lmaterial_clip_submit }, + { "draw", lmaterial_clip_draw }, + { NULL, NULL }, + }; + luaL_setfuncs(L, l, 0); + + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + } + lua_setmetatable(L, -2); + return 1; +} + +static int +lset_material_id(lua_State *L) { + int id = luaL_checkinteger(L, 1); + if (id <= 0) { + return luaL_error(L, "Invalid clip material id %d", id); + } + material_id = id; + return 0; +} + +static void +push_command(lua_State *L, int command, float w, float h) { + if (material_id <= 0) { + luaL_error(L, "Clip material is not registered"); + } + struct scissor_primitive prim; + memset(&prim, 0, sizeof(prim)); + prim.pos.sprite = -material_id; + prim.u.scissor.header.sprite = -1; + prim.u.scissor.command = command; + prim.u.scissor.w = w; + prim.u.scissor.h = h; + lua_pushlstring(L, (const char *)&prim, sizeof(prim)); +} + +static int +lrect(lua_State *L) { + int n = lua_gettop(L); + if (n == 0) { + push_command(L, SCISSOR_POP, 0.0f, 0.0f); + return 1; + } + if (n != 2) + return luaL_error(L, "Invalid clip rect arguments"); + float w = luaL_checknumber(L, 1); + float h = luaL_checknumber(L, 2); + if (w < 0 || h < 0) + return luaL_error(L, "Invalid clip rect size %f x %f", w, h); + push_command(L, SCISSOR_PUSH, w, h); + return 1; +} + +int +luaopen_material_clip(lua_State *L) { + luaL_checkversion(L); + luaL_Reg l[] = { + { "set_material_id", lset_material_id }, + { "new", lnew_material_clip }, + { "rect", lrect }, + { NULL, NULL }, + }; + luaL_newlib(L, l); + return 1; +} From 9bd0aaa48f50fdef44fe9e87febabaf2b5b73a3b Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Sat, 4 Jul 2026 17:14:17 +0800 Subject: [PATCH 03/12] test: update text case --- test/text.lua | 117 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 20 deletions(-) diff --git a/test/text.lua b/test/text.lua index 8e3cbd5..6dacd15 100644 --- a/test/text.lua +++ b/test/text.lua @@ -2,6 +2,7 @@ local soluna = require "soluna" local ltask = require "ltask" local mattext = require "soluna.material.text" local matquad = require "soluna.material.quad" +local matclip = require "soluna.material.clip" local font = require "soluna.font" local file = require "soluna.file" @@ -48,6 +49,9 @@ local fontcobj = font.cobj() local callback = {} local WIDTH = 200 local HEIGHT = 200 +local VIEWPORT_MIN = 48 +local VIEWPORT_MAX = HEIGHT +local CLIP_BLEED = 8 local screen_w = args.width local screen_h = args.height @@ -68,7 +72,15 @@ local block, layout = mattext.block(styles, "CV") local label = block(TEXT, WIDTH, HEIGHT) local label_layout = layout(TEXT, WIDTH, HEIGHT) -local CURSOR_N = 0 +local cursor_pos = 0 +local selection_anchor = nil +local selection_focus = nil +local dragging = false + +local function viewport_height(count) + local t = (math.sin(count * 0.025) + 1) * 0.5 + return math.floor(VIEWPORT_MIN + (VIEWPORT_MAX - VIEWPORT_MIN) * t) +end local function position() local x = (screen_w - WIDTH) / 2 @@ -76,48 +88,113 @@ local function position() return x, y end +local function ordered_selection() + if not selection_anchor or not selection_focus then + return nil + end + local from = selection_anchor + local to = selection_focus + if to < from then + from, to = to, from + end + if from == to then + return nil + end + return from, to +end + +local function draw_selection(x, y) + local from, to = ordered_selection() + if not from then + return + end + for i = 1, label_layout:line_count() do + local line = label_layout:line(i) + if line then + local a = math.max(from, line.start) + local b = math.min(to, line.finish) + if a < b then + local sx + if a <= line.start then + sx = line.x + else + sx = label_layout:cursor(a) + end + local ex + if b >= line.finish then + ex = line.x + line.width + else + ex = label_layout:cursor(b) + end + local w = math.floor(ex - sx + 0.5) + if w > 0 then + batch:add(matquad.quad(w, line.height, 0x664f7dff), x + sx, y + line.y) + end + end + end + end +end + function callback.frame(count) local x, y = position() - batch:add(matquad.quad(WIDTH, HEIGHT, 0x400000ff), x, y) + local clip_h = viewport_height(count) + batch:add(matquad.quad(WIDTH, clip_h, 0x400000ff), x, y) + batch:add(matclip.rect(WIDTH + CLIP_BLEED * 2, clip_h), x - CLIP_BLEED, y) + draw_selection(x, y) batch:add(label, x, y) - -- cursor - local cx, cy, cw, ch, n = label_layout:cursor(CURSOR_N) - CURSOR_N = n - batch:add(matquad.quad(cw, ch, 0xffffff), cx + x, cy + y) + local cx, cy, cw, ch, n = label_layout:cursor(cursor_pos) + cursor_pos = n + batch:add(matquad.quad(cw, ch, 0xffffff), x + cx, y + cy) + batch:add(matclip.rect()) end function callback.key(keycode, state) - if state == 1 then -- press - if keycode == 262 then -- right - CURSOR_N = CURSOR_N + 1 - elseif keycode == 263 then -- left - CURSOR_N = CURSOR_N - 1 - else - print(keycode) - end + if state ~= 1 then + return + end + if keycode == 262 then + cursor_pos = cursor_pos + 1 + elseif keycode == 263 then + cursor_pos = cursor_pos - 1 + else + print(keycode) end end - local mouse_x = 0 local mouse_y = 0 +local function hit_text(x, y) + local ox, oy = position() + return label_layout:hit_test(x - ox, y - oy) +end + function callback.mouse_move(x, y) mouse_x = x mouse_y = y + if dragging then + selection_focus = hit_text(x, y) + cursor_pos = selection_focus + end end local MOUSE_LEFT = 0 +local MOUSE_RELEASE = 0 local MOUSE_PRESS = 1 function callback.mouse_button(button, state) - if button ~= MOUSE_LEFT or state ~= MOUSE_PRESS then + if button ~= MOUSE_LEFT then return end - local x, y = position() - x = mouse_x - x - y = mouse_y - y - CURSOR_N = label_layout:hit_test(x, y) + if state == MOUSE_PRESS then + local hit = hit_text(mouse_x, mouse_y) + selection_anchor = hit + selection_focus = hit + cursor_pos = hit + dragging = true + elseif state == MOUSE_RELEASE then + dragging = false + end end return callback From 358dea646b7839a5d4e72c140dbf093eebdc55ab Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Sat, 4 Jul 2026 20:43:17 +0800 Subject: [PATCH 04/12] fix: scale clip scissor rects for dpi --- src/material_clip.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/material_clip.c b/src/material_clip.c index 709c398..e3146f7 100644 --- a/src/material_clip.c +++ b/src/material_clip.c @@ -5,6 +5,7 @@ #include #include +#include "sokol/sokol_app.h" #include "sokol/sokol_gfx.h" #include "batch.h" #include "material_util.h" @@ -74,7 +75,14 @@ intersect_scissor(struct scissor_rect a, struct scissor_rect b) { static void apply_scissor(struct scissor_rect r) { - sg_apply_scissor_rect(r.x, r.y, r.w, r.h, true); + float scale = sapp_dpi_scale(); + if (scale <= 0.0f) + scale = 1.0f; + int x0 = (int)floorf((float)r.x * scale); + int y0 = (int)floorf((float)r.y * scale); + int x1 = (int)ceilf((float)(r.x + r.w) * scale); + int y1 = (int)ceilf((float)(r.y + r.h) * scale); + sg_apply_scissor_rect(x0, y0, x1 > x0 ? x1 - x0 : 0, y1 > y0 ? y1 - y0 : 0, true); } static void From 9cc816af558d61dfda8cea23c21b8d764d205995 Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Sat, 4 Jul 2026 20:43:38 +0800 Subject: [PATCH 05/12] fix: center custom text line height leading --- src/material_text.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/material_text.c b/src/material_text.c index 393cb98..cd851ab 100644 --- a/src/material_text.c +++ b/src/material_text.c @@ -408,17 +408,21 @@ style_getinfo(struct styles *s) { font_manager_fontheight(mgr, fs->fontid, fs->size, &ascent, &decent, &gap); if (gap == 0) gap = 1; - int natural_height = ascent - decent; - fs->ascent = ascent; + int natural_decent = -decent; + int natural_height = ascent + natural_decent; if (fs->line_height > 0) { if (fs->line_height < natural_height) { fs->line_height = natural_height; } - fs->decent = fs->line_height - ascent; + int leading = fs->line_height - natural_height; + int leading_top = leading / 2; + fs->ascent = ascent + leading_top; + fs->decent = natural_decent + leading - leading_top; fs->gap = 0; } else { + fs->ascent = ascent; fs->line_height = natural_height; - fs->decent = -decent + gap; + fs->decent = natural_decent + gap; fs->gap = gap; } } From d04d51fabcfd85e7020feb6dfa1bf6fc3764a22f Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Sat, 4 Jul 2026 20:43:58 +0800 Subject: [PATCH 06/12] fix: keep text styles alive in block closures --- src/material_text.c | 14 ++++++++++++-- test/text.lua | 39 +++++++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/material_text.c b/src/material_text.c index cd851ab..6fd1164 100644 --- a/src/material_text.c +++ b/src/material_text.c @@ -1138,6 +1138,7 @@ ltext_block(lua_State *L) { return luaL_error(L, "Text material is not registered"); } void * font_mgr = lua_touserdata(L, 1); + int styles_arg = 0; int fontid = 0; int fontsize = 0; uint32_t color = 0; @@ -1146,6 +1147,7 @@ ltext_block(lua_State *L) { if (lua_type(L, 2) == LUA_TSTRING) { // font_mgr is struct styles luaL_checktype(L, 1, LUA_TUSERDATA); + styles_arg = 1; alignment = parse_alignment(L, 2); } else { luaL_checktype(L, 1, LUA_TLIGHTUSERDATA); @@ -1160,13 +1162,21 @@ ltext_block(lua_State *L) { color |= 0xff000000; } - lua_pushlightuserdata(L, font_mgr); // 1 + if (styles_arg) { + lua_pushvalue(L, 1); + } else { + lua_pushlightuserdata(L, font_mgr); + } lua_pushinteger(L, fontid); // 2 lua_pushinteger(L, fontsize); // 3 lua_pushinteger(L, color); // 4 lua_pushinteger(L, alignment); // 5 lua_pushcclosure(L, ltext, 5); - lua_pushlightuserdata(L, font_mgr); // 1 + if (styles_arg) { + lua_pushvalue(L, 1); + } else { + lua_pushlightuserdata(L, font_mgr); + } lua_pushinteger(L, fontid); // 2 lua_pushinteger(L, fontsize); // 3 lua_pushinteger(L, color); // 4 diff --git a/test/text.lua b/test/text.lua index 6dacd15..aa2b7cf 100644 --- a/test/text.lua +++ b/test/text.lua @@ -63,12 +63,18 @@ end local TEXT = "[004000]Hello[n], 这个句子中有[s1]大字[n],也有[s2]小字[n]。这句话会在文本区居中。" -- size 32; color 0; alignment center -- local block, layout = mattext.block(fontcobj, fontid, 32, 0, "CV") -local styles = mattext.styles(fontcobj, { - { font = fontid, size = 24, color = 0 }, - { font = fontid, size = 32, color = 0x800000 }, - { font = fontid, size = 16, color = 0x000080 }, -}) -local block, layout = mattext.block(styles, "CV") +local function text_block() + local styles = mattext.styles(fontcobj, { + { font = fontid, size = 24, color = 0 }, + { font = fontid, size = 32, color = 0x800000 }, + { font = fontid, size = 16, color = 0x000080 }, + }) + return mattext.block(styles, "CV") +end + +local block, layout = text_block() +-- Verify block closures keep the local styles object alive. +collectgarbage "collect" local label = block(TEXT, WIDTH, HEIGHT) local label_layout = layout(TEXT, WIDTH, HEIGHT) @@ -142,25 +148,26 @@ function callback.frame(count) batch:add(matclip.rect(WIDTH + CLIP_BLEED * 2, clip_h), x - CLIP_BLEED, y) draw_selection(x, y) batch:add(label, x, y) + -- cursor local cx, cy, cw, ch, n = label_layout:cursor(cursor_pos) cursor_pos = n - batch:add(matquad.quad(cw, ch, 0xffffff), x + cx, y + cy) + batch:add(matquad.quad(cw, ch, 0xffffff), cx + x, cy + y) batch:add(matclip.rect()) end function callback.key(keycode, state) - if state ~= 1 then - return - end - if keycode == 262 then - cursor_pos = cursor_pos + 1 - elseif keycode == 263 then - cursor_pos = cursor_pos - 1 - else - print(keycode) + if state == 1 then -- press + if keycode == 262 then -- right + cursor_pos = cursor_pos + 1 + elseif keycode == 263 then -- left + cursor_pos = cursor_pos - 1 + else + print(keycode) + end end end + local mouse_x = 0 local mouse_y = 0 From 8406ce4af4d1ad0c6a2423b63b08e032db1e19ad Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Sat, 4 Jul 2026 23:19:28 +0800 Subject: [PATCH 07/12] support yoga overflow --- src/luayoga.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/luayoga.c b/src/luayoga.c index c39e598..aace2ce 100644 --- a/src/luayoga.c +++ b/src/luayoga.c @@ -12,6 +12,7 @@ #define Wrap 8 #define Display 16 #define PositionType 32 +#define Overflow 64 #define ENUM(x, what) { YG##x##ToString(YG##x##what), (x) << 16 | (YG##x##what) }, @@ -512,6 +513,11 @@ lsetPosition(lua_State *L, YGNodeRef node) { YGNodeStyleSetPositionType(node, getEnum(L, PositionType, "position")); } +static void +lsetOverflow(lua_State *L, YGNodeRef node) { + YGNodeStyleSetOverflow(node, getEnum(L, Overflow, "overflow")); +} + static void setPosition(lua_State *L, YGNodeRef node, YGEdge edge) { static const struct set_edge_number setter = { @@ -636,6 +642,7 @@ luaopen_layout_yoga(lua_State *L) { { "display", lsetDisplay }, { "flex", lsetFlex }, { "position", lsetPosition }, + { "overflow", lsetOverflow }, { "top", lsetTop }, { "bottom", lsetBottom }, { "left", lsetLeft }, @@ -678,6 +685,9 @@ luaopen_layout_yoga(lua_State *L) { ENUM(PositionType, Static) ENUM(PositionType, Relative) ENUM(PositionType, Absolute) + ENUM(Overflow, Visible) + ENUM(Overflow, Hidden) + ENUM(Overflow, Scroll) }; n = sizeof(estr) / sizeof(estr[0]); lua_createtable(L, n, 0); From 9c3e29c9aa3225592c2b3b5971bab6b7417f05ad Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Sat, 4 Jul 2026 23:38:57 +0800 Subject: [PATCH 08/12] split text layout style and line metrics --- src/material_text.c | 216 ++++++++++++++++++++++++++------------------ 1 file changed, 128 insertions(+), 88 deletions(-) diff --git a/src/material_text.c b/src/material_text.c index 6fd1164..c97c3e8 100644 --- a/src/material_text.c +++ b/src/material_text.c @@ -467,30 +467,80 @@ struct block_context { int alignment; }; -struct position { - int x; +struct layout_style { + int ascent; + int decent; + int gap; +}; + +struct line_info { int y; - int w; int h; int gap; - int ascent; - int decent; +}; + +struct position { + int x; + int w; int style; + int line; }; struct layout { int n; + int cap; int width; int height; int top; int text_height; - struct position pos[]; + int style_count; + int line_count; + struct layout_style styles[]; }; +static inline struct position * +layout_positions(struct layout *pos) { + return (struct position *)(pos->styles + pos->style_count); +} + +static inline struct line_info * +layout_lines(struct layout *pos) { + return (struct line_info *)(layout_positions(pos) + pos->cap); +} + +static inline size_t +layout_size(int cap, int style_count) { + return sizeof(struct layout) + + (size_t)style_count * sizeof(struct layout_style) + + (size_t)cap * sizeof(struct position) + + (size_t)cap * sizeof(struct line_info); +} + +static void +copy_layout_styles(struct layout *pos, struct styles *styles) { + int i; + pos->style_count = styles->n; + for (i=0;in;i++) { + pos->styles[i].ascent = styles->s[i].ascent; + pos->styles[i].decent = styles->s[i].decent; + pos->styles[i].gap = styles->s[i].gap; + } +} + +static inline void +set_position(struct layout *pos, int n, struct block_context *ctx) { + struct position *p = &layout_positions(pos)[n]; + p->x = ctx->x; + p->w = 0; + p->style = ctx->style; + p->line = 0; + pos->n = n + 1; +} + static inline int advance(struct layout *pos, struct block_context *ctx, int x) { if (pos && pos->n > 0) { - pos->pos[pos->n-1].w = x; + layout_positions(pos)[pos->n-1].w = x; } if (x + ctx->x > ctx->width) return 1; @@ -547,16 +597,21 @@ newline(struct block_context *ctx, struct text_primitive * prim, int n, struct l } } if (pos != NULL) { - if (offx > 0) { + if (n > from) { + struct position *positions = layout_positions(pos); + struct line_info *lines = layout_lines(pos); + int line = pos->line_count++; + lines[line].y = ctx->y; + lines[line].h = line_height; + lines[line].gap = line_gap; for (i=from;ipos[i].x += offx; - pos->pos[i].h = line_height; - pos->pos[i].gap = line_gap; + positions[i].line = line; } - } else { + } + if (offx > 0) { + struct position *positions = layout_positions(pos); for (i=from;ipos[i].h = line_height; - pos->pos[i].gap = line_gap; + positions[i].x += offx; } } } @@ -654,12 +709,16 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { struct text_primitive * prim = NULL; struct layout * pos = NULL; if (gen_layout) { - pos = (struct layout *)lua_newuserdatauv(L, sizeof(struct layout) + (count+1) * sizeof(struct position), 0); + int cap = count + 1; + pos = (struct layout *)lua_newuserdatauv(L, layout_size(cap, s->n), 0); pos->n = 0; + pos->cap = cap; pos->width = ctx.width; pos->height = ctx.height; pos->text_height = 0; pos->top = 0; + pos->line_count = 0; + copy_layout_styles(pos, s); } else { buffer = (char *)malloc(count * sizeof(struct text_primitive)+1); prim = (struct text_primitive *)buffer; @@ -717,16 +776,7 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { prim[n].pos.sprite = -material_id; } else { // assert(pos != NULL) - struct position *p = &pos->pos[n]; - p->x = ctx.x; - p->y = ctx.y; - p->w = 0; - p->h = 0; - p->gap = 0; - p->ascent = ctx.fs->ascent; - p->decent = ctx.fs->decent; - p->style = ctx.style; - pos->n = n + 1; + set_position(pos, n, &ctx); } struct font_glyph g, og; @@ -740,15 +790,7 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { prim[n].pos.x = ctx.x * PIXEL_SCALE; prim[n].pos.y = ( ctx.y + dy ) * PIXEL_SCALE; } else { - struct position *p = &pos->pos[n]; - p->x = ctx.x; - p->y = ctx.y; - p->w = 0; - p->h = 0; - p->gap = 0; - p->ascent = ctx.fs->ascent; - p->decent = ctx.fs->decent; - p->style = ctx.style; + set_position(pos, n, &ctx); } advance(pos, &ctx, g.advance_x); } @@ -770,15 +812,7 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { if (n == 0 && pos) { // no text, set one if (pos) { - struct position *p = &pos->pos[0]; - p->x = 0; - p->y = 0; - p->w = 0; - p->h = 0; - p->gap = 0; - p->ascent = ctx.fs->ascent; - p->decent = ctx.fs->decent; - p->style = ctx.style; + set_position(pos, 0, &ctx); ctx.line_ascent = ctx.fs->ascent; ctx.line_decent = ctx.fs->decent; ctx.line_height = ctx.fs->ascent + ctx.fs->decent - ctx.fs->gap; @@ -789,9 +823,10 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { newline(&ctx, prim, n, pos); } if (pos && n>0) { - pos->pos[n] = pos->pos[n-1]; - pos->pos[n].x = pos->pos[n-1].x + pos->pos[n-1].w; - pos->pos[n].w = 0; + struct position *positions = layout_positions(pos); + positions[n] = positions[n-1]; + positions[n].x = positions[n-1].x + positions[n-1].w; + positions[n].w = 0; } int offy; int valign = ctx.alignment & VALIGNMENT_MASK; @@ -820,8 +855,9 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { pos->n = n; if (offy != 0) { int offset = offy / PIXEL_SCALE; - for (i=0;i<=n;i++) { - pos->pos[i].y += offset; + struct line_info *lines = layout_lines(pos); + for (i=0;iline_count;i++) { + lines[i].y += offset; } pos->top = offset; } @@ -872,14 +908,16 @@ ltext_cursor(lua_State *L) { } else if (n > pos->n ) { n = pos->n; } - struct position *p = &pos->pos[n]; - int height = p->ascent + p->decent - p->gap; + struct position *p = &layout_positions(pos)[n]; + struct layout_style *style = &pos->styles[p->style]; + struct line_info *line = &layout_lines(pos)[p->line]; + int height = style->ascent + style->decent - style->gap; lua_pushinteger(L, p->x); - lua_pushinteger(L, p->y + p->h - height); + lua_pushinteger(L, line->y + line->h - height); lua_pushinteger(L, 2); lua_pushinteger(L, height); lua_pushinteger(L, n); - lua_pushinteger(L, p->decent); + lua_pushinteger(L, style->decent); return 6; } @@ -901,11 +939,12 @@ ltext_line_height(lua_State *L) { int height = 0; int bottom = pos->top + pos->text_height; int i; - for (i=0;ipos[i]; - int line_height = p->h + p->gap; - if (p->y + line_height > bottom) { - line_height = bottom - p->y; + struct line_info *lines = layout_lines(pos); + for (i=0;iline_count;i++) { + struct line_info *line = &lines[i]; + int line_height = line->h + line->gap; + if (line->y + line_height > bottom) { + line_height = bottom - line->y; } if (line_height > height) { height = line_height; @@ -923,16 +962,7 @@ ltext_line_count(lua_State *L) { lua_pushinteger(L, pos->text_height > 0 ? 1 : 0); return 1; } - int count = 1; - int y = pos->pos[0].y; - int i; - for (i=1;ipos[i].y != y) { - y = pos->pos[i].y; - ++count; - } - } - lua_pushinteger(L, count); + lua_pushinteger(L, pos->line_count); return 1; } @@ -951,14 +981,17 @@ set_integer_field(lua_State *L, const char *key, int value) { static void push_line(lua_State *L, struct layout *pos, int start, int finish) { - int x0 = pos->pos[start].x; + struct position *positions = layout_positions(pos); + struct line_info *lines = layout_lines(pos); + int x0 = positions[start].x; int x1 = x0; - int y = pos->pos[start].y; + int y = lines[positions[start].line].y; int y1 = y; int text_bottom = pos->top + pos->text_height; int i; for (i=start;ipos[i]; + struct position *p = &positions[i]; + struct line_info *line = &lines[p->line]; if (p->x < x0) { x0 = p->x; } @@ -966,7 +999,7 @@ push_line(lua_State *L, struct layout *pos, int start, int finish) { if (right > x1) { x1 = right; } - int bottom = p->y + p->h + p->gap; + int bottom = line->y + line->h + line->gap; if (bottom > y1) { y1 = bottom; } @@ -1006,23 +1039,27 @@ ltext_line(lua_State *L) { set_integer_field(L, "height", pos->text_height); return 1; } - int current_line = 1; - int start = 0; - int y = pos->pos[0].y; + int target = line - 1; + if (target >= pos->line_count) { + lua_pushnil(L); + return 1; + } + int start = -1; + int finish = -1; + struct position *positions = layout_positions(pos); int i; - for (i=1;ipos[i].y != y) { - if (current_line == line) { - push_line(L, pos, start, i); - return 1; + for (i=0;ipos[i].y; + finish = i + 1; + } else if (start >= 0) { + break; } } - if (current_line == line) { - push_line(L, pos, start, n); + if (start >= 0) { + push_line(L, pos, start, finish); return 1; } lua_pushnil(L); @@ -1037,7 +1074,7 @@ ltext_style(lua_State *L) { lua_pushnil(L); return 1; } - lua_pushinteger(L, pos->pos[n].style); + lua_pushinteger(L, layout_positions(pos)[n].style); return 1; } @@ -1055,12 +1092,15 @@ static int bsearch_pos(struct layout * pos, int x, int y) { int from = 0; int to = pos->n; + struct position *positions = layout_positions(pos); + struct line_info *lines = layout_lines(pos); while (from < to) { int mid = from + (to - from) / 2; - struct position *p = &pos->pos[mid]; - if (y < p->y) { + struct position *p = &positions[mid]; + struct line_info *line = &lines[p->line]; + if (y < line->y) { to = mid; - } else if (y >= p->y + p->h + p->gap) { + } else if (y >= line->y + line->h + line->gap) { from = mid + 1; } else if (x < p->x) { to = mid; From 55a4320da10cc589293ddf9965ed2ef059869a76 Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Sun, 5 Jul 2026 01:31:25 +0800 Subject: [PATCH 09/12] fix: text cursor positions for spaces --- src/material_text.c | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/material_text.c b/src/material_text.c index c97c3e8..4f41de7 100644 --- a/src/material_text.c +++ b/src/material_text.c @@ -307,7 +307,10 @@ count_string(const char *str) { while ((str = utf8_decode(str, &val))) { if (val == 0) break; - if (val > 32) { + if (val <= 32) { + if (val != '\n') + ++n; + } else { if (val == '[') { char c = *str; if (c == '[') { @@ -498,6 +501,9 @@ struct layout { struct layout_style styles[]; }; +static inline int +newline(struct block_context *ctx, struct text_primitive * prim, int n, struct layout *pos); + static inline struct position * layout_positions(struct layout *pos) { return (struct position *)(pos->styles + pos->style_count); @@ -557,6 +563,25 @@ advance(struct layout *pos, struct block_context *ctx, int x) { return 0; } +static inline int +advance_space(struct block_context *ctx, struct text_primitive *prim, struct layout *pos, int *n, int advance_x) { + if (pos) { + set_position(pos, *n, ctx); + } + if (advance(pos, ctx, advance_x)) { + if (newline(ctx, prim, *n, pos)) + return 1; + if (pos) { + set_position(pos, *n, ctx); + } + advance(pos, ctx, advance_x); + } + if (pos) { + ++*n; + } + return 0; +} + static inline int newline(struct block_context *ctx, struct text_primitive * prim, int n, struct layout *pos) { int from = ctx->line_prim; @@ -723,11 +748,12 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { buffer = (char *)malloc(count * sizeof(struct text_primitive)+1); prim = (struct text_primitive *)buffer; } - int i; int n = 0; - for (i=0;i ctx.line_width) @@ -739,11 +765,8 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { if (font_manager_glyph(mgr, ctx.fs->fontid, ' ', ctx.fs->size, &g, &og) == NULL) { if (ctx.x > ctx.line_width) ctx.line_width = ctx.x; - if (advance(pos, &ctx, g.advance_x)) { - if (newline(&ctx, prim, n, pos)) - break; - advance(pos, &ctx, g.advance_x); - } + if (advance_space(&ctx, prim, pos, &n, g.advance_x)) + break; } } } else { @@ -803,7 +826,6 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { } ++n; } - ++i; } } if (ctx.x > ctx.line_width) @@ -830,6 +852,7 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { } int offy; int valign = ctx.alignment & VALIGNMENT_MASK; + int i; switch (valign) { case VALIGNMENT_CENTER: offy = (ctx.height - height) / 2 * PIXEL_SCALE; From e20036fa78b65c5c63ec5776a45c92775fd34203 Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Sun, 5 Jul 2026 11:50:50 +0800 Subject: [PATCH 10/12] fix: text block args handling --- src/material_text.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/material_text.c b/src/material_text.c index 4f41de7..256f3f2 100644 --- a/src/material_text.c +++ b/src/material_text.c @@ -450,6 +450,11 @@ ltext_styles(lua_State *L) { read_style(L, 2, s, i); } style_getinfo(s); + if (luaL_newmetatable(L, "SOLUNA_TEXT_STYLES")) { + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + } + lua_setmetatable(L, -2); return 1; } @@ -1200,20 +1205,24 @@ ltext_block(lua_State *L) { if (material_id <= 0) { return luaL_error(L, "Text material is not registered"); } - void * font_mgr = lua_touserdata(L, 1); + void * font_mgr = NULL; int styles_arg = 0; int fontid = 0; int fontsize = 0; uint32_t color = 0; uint32_t alignment = 0; - - if (lua_type(L, 2) == LUA_TSTRING) { - // font_mgr is struct styles - luaL_checktype(L, 1, LUA_TUSERDATA); + + if (luaL_testudata(L, 1, "SOLUNA_TEXT_STYLES")) { + font_mgr = lua_touserdata(L, 1); styles_arg = 1; - alignment = parse_alignment(L, 2); + if (lua_type(L, 2) == LUA_TSTRING) { + alignment = parse_alignment(L, 2); + } else { + luaL_opt(L, luaL_checkstring, 2, NULL); + } } else { luaL_checktype(L, 1, LUA_TLIGHTUSERDATA); + font_mgr = lua_touserdata(L, 1); fontid = luaL_checkinteger(L, 2); fontsize = luaL_optinteger(L, 3, DEFAULT_FONTSIZE); color = luaL_optinteger(L, 4, 0xff000000); From 4483fbe67072c5b8afba2629e62ffd1b4acbdf7b Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Sun, 5 Jul 2026 13:13:33 +0800 Subject: [PATCH 11/12] rework: simplify text layout style store --- src/material_text.c | 49 ++++++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/material_text.c b/src/material_text.c index 256f3f2..a5c2cd1 100644 --- a/src/material_text.c +++ b/src/material_text.c @@ -461,7 +461,6 @@ ltext_styles(lua_State *L) { struct block_context { struct styles *s; struct font_style *fs; - int style; int width; int height; int x; @@ -475,12 +474,6 @@ struct block_context { int alignment; }; -struct layout_style { - int ascent; - int decent; - int gap; -}; - struct line_info { int y; int h; @@ -501,9 +494,9 @@ struct layout { int height; int top; int text_height; - int style_count; int line_count; - struct layout_style styles[]; + struct font_style *styles; + struct font_style style; }; static inline int @@ -511,7 +504,7 @@ newline(struct block_context *ctx, struct text_primitive * prim, int n, struct l static inline struct position * layout_positions(struct layout *pos) { - return (struct position *)(pos->styles + pos->style_count); + return (struct position *)(pos + 1); } static inline struct line_info * @@ -520,21 +513,21 @@ layout_lines(struct layout *pos) { } static inline size_t -layout_size(int cap, int style_count) { +layout_size(int cap) { return sizeof(struct layout) - + (size_t)style_count * sizeof(struct layout_style) + (size_t)cap * sizeof(struct position) + (size_t)cap * sizeof(struct line_info); } static void -copy_layout_styles(struct layout *pos, struct styles *styles) { - int i; - pos->style_count = styles->n; - for (i=0;in;i++) { - pos->styles[i].ascent = styles->s[i].ascent; - pos->styles[i].decent = styles->s[i].decent; - pos->styles[i].gap = styles->s[i].gap; +set_layout_styles(lua_State *L, struct layout *pos, struct styles *styles, int external) { + if (external) { + pos->styles = styles->s; + lua_pushvalue(L, lua_upvalueindex(1)); + lua_setiuservalue(L, -2, 1); + } else { + pos->style = styles->s[0]; + pos->styles = &pos->style; } } @@ -543,7 +536,7 @@ set_position(struct layout *pos, int n, struct block_context *ctx) { struct position *p = &layout_positions(pos)[n]; p->x = ctx->x; p->w = 0; - p->style = ctx->style; + p->style = (int)(ctx->fs - ctx->s->s); p->line = 0; pos->n = n + 1; } @@ -688,7 +681,6 @@ parse_bracket(struct block_context *ctx, const char *str, int *icon) { if (style < 0 || style >=n) { style = 0; } - ctx->style = style; ctx->fs = &ctx->s->s[style]; ctx->color = ctx->fs->color; } else if ((hex = tohex(c)) >= 0) { @@ -705,7 +697,6 @@ parse_bracket(struct block_context *ctx, const char *str, int *icon) { color |= 0xff000000; ctx->color = color; } else if (c == 'n') { - ctx->style = 0; ctx->fs = &ctx->s->s[0]; ctx->color = ctx->fs->color; } @@ -714,13 +705,12 @@ parse_bracket(struct block_context *ctx, const char *str, int *icon) { } static int -ltext_(lua_State *L, struct styles *s, int gen_layout) { +ltext_(lua_State *L, struct styles *s, int gen_layout, int external) { const char * str = luaL_checkstring(L, 1); int count = count_string(str); struct block_context ctx; ctx.s = s; ctx.fs = &s->s[0]; - ctx.style = 0; ctx.width = luaL_optinteger(L, 2, MAX_WIDTH); ctx.height = luaL_optinteger(L, 3, MAX_HEIGHT); @@ -740,7 +730,7 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { struct layout * pos = NULL; if (gen_layout) { int cap = count + 1; - pos = (struct layout *)lua_newuserdatauv(L, layout_size(cap, s->n), 0); + pos = (struct layout *)lua_newuserdatauv(L, layout_size(cap), external ? 1 : 0); pos->n = 0; pos->cap = cap; pos->width = ctx.width; @@ -748,7 +738,7 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { pos->text_height = 0; pos->top = 0; pos->line_count = 0; - copy_layout_styles(pos, s); + set_layout_styles(L, pos, s, external); } else { buffer = (char *)malloc(count * sizeof(struct text_primitive)+1); prim = (struct text_primitive *)buffer; @@ -915,13 +905,14 @@ get_styles(lua_State *L, struct styles *tmp) { static int ltext(lua_State *L) { struct styles tmp; - return ltext_(L, get_styles(L, &tmp), 0); + return ltext_(L, get_styles(L, &tmp), 0, 0); } static int ltext_layout(lua_State *L) { struct styles tmp; - ltext_(L, get_styles(L, &tmp), 1); + int external = luaL_testudata(L, lua_upvalueindex(1), "SOLUNA_TEXT_STYLES") != NULL; + ltext_(L, get_styles(L, &tmp), 1, external); lua_pushvalue(L, lua_upvalueindex(6)); lua_setmetatable(L, -2); return 1; @@ -937,7 +928,7 @@ ltext_cursor(lua_State *L) { n = pos->n; } struct position *p = &layout_positions(pos)[n]; - struct layout_style *style = &pos->styles[p->style]; + struct font_style *style = &pos->styles[p->style]; struct line_info *line = &layout_lines(pos)[p->line]; int height = style->ascent + style->decent - style->gap; lua_pushinteger(L, p->x); From cfd6427aa6b0c0d552939bd171ab9f81c2ccbf64 Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Sun, 5 Jul 2026 13:22:02 +0800 Subject: [PATCH 12/12] rework: simplify text layout metrics --- src/material_text.c | 85 +++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/src/material_text.c b/src/material_text.c index a5c2cd1..ee96b3c 100644 --- a/src/material_text.c +++ b/src/material_text.c @@ -354,7 +354,6 @@ struct font_style { uint32_t color; int fontid; int size; - int line_height; // read from font int ascent; int decent; @@ -384,9 +383,34 @@ get_field(lua_State *L, const char *key, int def) { return v; } +static void +style_getinfo(struct font_manager *mgr, struct font_style *fs, int line_height) { + int ascent, decent, gap; + font_manager_fontheight(mgr, fs->fontid, fs->size, &ascent, &decent, &gap); + if (gap == 0) + gap = 1; + int natural_decent = -decent; + int natural_height = ascent + natural_decent; + if (line_height > 0) { + if (line_height < natural_height) { + line_height = natural_height; + } + int leading = line_height - natural_height; + int leading_top = leading / 2; + fs->ascent = ascent + leading_top; + fs->decent = natural_decent + leading - leading_top; + fs->gap = 0; + } else { + fs->ascent = ascent; + fs->decent = natural_decent + gap; + fs->gap = gap; + } +} + static void read_style(lua_State *L, int index, struct styles *s, int i) { struct font_style *fs = &s->s[i]; + int line_height; if (lua_geti(L, index, i+1) != LUA_TTABLE) { luaL_error(L, "Invalid style at %d, need a table", i+1); } @@ -397,40 +421,11 @@ read_style(lua_State *L, int index, struct styles *s, int i) { if (!(fs->color & 0xff000000)) fs->color |= 0xff000000; fs->size = get_field(L, "size", DEFAULT_FONTSIZE); - fs->line_height = get_field(L, "line_height", 0); + line_height = get_field(L, "line_height", 0); + style_getinfo(s->font, fs, line_height); lua_pop(L, 1); } -static void -style_getinfo(struct styles *s) { - struct font_manager *mgr = s->font; - int i; - for (i=0;in;i++) { - int ascent, decent, gap; - struct font_style *fs = &s->s[i]; - font_manager_fontheight(mgr, fs->fontid, fs->size, &ascent, &decent, &gap); - if (gap == 0) - gap = 1; - int natural_decent = -decent; - int natural_height = ascent + natural_decent; - if (fs->line_height > 0) { - if (fs->line_height < natural_height) { - fs->line_height = natural_height; - } - int leading = fs->line_height - natural_height; - int leading_top = leading / 2; - fs->ascent = ascent + leading_top; - fs->decent = natural_decent + leading - leading_top; - fs->gap = 0; - } else { - fs->ascent = ascent; - fs->line_height = natural_height; - fs->decent = natural_decent + gap; - fs->gap = gap; - } - } -} - static int ltext_styles(lua_State *L) { luaL_checktype(L, 1, LUA_TLIGHTUSERDATA); @@ -449,7 +444,6 @@ ltext_styles(lua_State *L) { for (i=0;iline_count) { + return lines[line + 1].y; + } + return pos->top + pos->text_height; +} + static void set_layout_styles(lua_State *L, struct layout *pos, struct styles *styles, int external) { if (external) { @@ -588,7 +589,6 @@ newline(struct block_context *ctx, struct text_primitive * prim, int n, struct l int line_ascent = ctx->line_ascent; int line_decent = ctx->line_decent; int line_height = ctx->line_height; - int line_gap = line_ascent + line_decent - line_height; ctx->line_width = 0; ctx->line_height = 0; ctx->line_ascent = 0; @@ -626,7 +626,6 @@ newline(struct block_context *ctx, struct text_primitive * prim, int n, struct l int line = pos->line_count++; lines[line].y = ctx->y; lines[line].h = line_height; - lines[line].gap = line_gap; for (i=from;ifontid = fontid; fs->size = lua_tointeger(L, lua_upvalueindex(3)); fs->color = lua_tointeger(L, lua_upvalueindex(4)); - fs->line_height = 0; - style_getinfo(tmp); + style_getinfo(tmp->font, fs, 0); return tmp; } @@ -956,15 +954,11 @@ ltext_line_height(lua_State *L) { return 1; } int height = 0; - int bottom = pos->top + pos->text_height; int i; struct line_info *lines = layout_lines(pos); for (i=0;iline_count;i++) { struct line_info *line = &lines[i]; - int line_height = line->h + line->gap; - if (line->y + line_height > bottom) { - line_height = bottom - line->y; - } + int line_height = layout_line_bottom(pos, lines, i) - line->y; if (line_height > height) { height = line_height; } @@ -1010,7 +1004,6 @@ push_line(lua_State *L, struct layout *pos, int start, int finish) { int i; for (i=start;iline]; if (p->x < x0) { x0 = p->x; } @@ -1018,7 +1011,7 @@ push_line(lua_State *L, struct layout *pos, int start, int finish) { if (right > x1) { x1 = right; } - int bottom = line->y + line->h + line->gap; + int bottom = layout_line_bottom(pos, lines, p->line); if (bottom > y1) { y1 = bottom; } @@ -1119,7 +1112,7 @@ bsearch_pos(struct layout * pos, int x, int y) { struct line_info *line = &lines[p->line]; if (y < line->y) { to = mid; - } else if (y >= line->y + line->h + line->gap) { + } else if (y >= layout_line_bottom(pos, lines, p->line)) { from = mid + 1; } else if (x < p->x) { to = mid;