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/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/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/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); 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..e3146f7 --- /dev/null +++ b/src/material_clip.c @@ -0,0 +1,275 @@ +#include +#include +#include +#include +#include +#include + +#include "sokol/sokol_app.h" +#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) { + 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 +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; +} diff --git a/src/material_text.c b/src/material_text.c index af285c7..ee96b3c 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 == '[') { @@ -367,6 +370,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); @@ -379,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); } @@ -392,25 +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); + 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; - fs->ascent = ascent; - fs->decent = -decent + gap; - fs->gap = gap; - } -} - static int ltext_styles(lua_State *L) { luaL_checktype(L, 1, LUA_TLIGHTUSERDATA); @@ -429,7 +444,11 @@ ltext_styles(lua_State *L) { for (i=0;icap); +} + +static inline size_t +layout_size(int cap) { + return sizeof(struct layout) + + (size_t)cap * sizeof(struct position) + + (size_t)cap * sizeof(struct line_info); +} + +static inline int +layout_line_bottom(struct layout *pos, struct line_info *lines, int line) { + if (line + 1 < pos->line_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) { + 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; + } +} + +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 = (int)(ctx->fs - ctx->s->s); + 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; @@ -488,6 +562,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; @@ -496,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; @@ -528,16 +620,20 @@ 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; 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; } } } @@ -608,7 +704,7 @@ 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; @@ -632,21 +728,26 @@ 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), external ? 1 : 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; + set_layout_styles(L, pos, s, external); } else { 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) @@ -658,11 +759,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 { @@ -695,15 +793,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; - pos->n = n + 1; + set_position(pos, n, &ctx); } struct font_glyph g, og; @@ -717,14 +807,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; + set_position(pos, n, &ctx); } advance(pos, &ctx, g.advance_x); } @@ -737,7 +820,6 @@ ltext_(lua_State *L, struct styles *s, int gen_layout) { } ++n; } - ++i; } } if (ctx.x > ctx.line_width) @@ -746,14 +828,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; + 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; @@ -764,12 +839,14 @@ 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; + int i; switch (valign) { case VALIGNMENT_CENTER: offy = (ctx.height - height) / 2 * PIXEL_SCALE; @@ -795,8 +872,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; } @@ -818,20 +896,21 @@ get_styles(lua_State *L, struct styles *tmp) { fs->size = lua_tointeger(L, lua_upvalueindex(3)); fs->color = lua_tointeger(L, lua_upvalueindex(4)); - style_getinfo(tmp); + style_getinfo(tmp->font, fs, 0); return 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; @@ -846,17 +925,171 @@ 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 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); - 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; } +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 i; + struct line_info *lines = layout_lines(pos); + for (i=0;iline_count;i++) { + struct line_info *line = &lines[i]; + int line_height = layout_line_bottom(pos, lines, i) - line->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; + } + lua_pushinteger(L, pos->line_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) { + struct position *positions = layout_positions(pos); + struct line_info *lines = layout_lines(pos); + int x0 = positions[start].x; + int x1 = x0; + int y = lines[positions[start].line].y; + int y1 = y; + int text_bottom = pos->top + pos->text_height; + int i; + for (i=start;ix < x0) { + x0 = p->x; + } + int right = p->x + p->w; + if (right > x1) { + x1 = right; + } + int bottom = layout_line_bottom(pos, lines, p->line); + 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 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=0;i= 0) { + break; + } + } + if (start >= 0) { + push_line(L, pos, start, finish); + 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, layout_positions(pos)[n].style); + return 1; +} + static int get_pos(lua_State *L, int index) { int isnum; @@ -871,12 +1104,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 >= layout_line_bottom(pos, lines, p->line)) { from = mid + 1; } else if (x < p->x) { to = mid; @@ -901,7 +1137,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; } @@ -953,18 +1189,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); - alignment = parse_alignment(L, 2); + + if (luaL_testudata(L, 1, "SOLUNA_TEXT_STYLES")) { + font_mgr = lua_touserdata(L, 1); + styles_arg = 1; + 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); @@ -976,13 +1218,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 @@ -1008,8 +1258,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); diff --git a/test/text.lua b/test/text.lua index 8e3cbd5..aa2b7cf 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 @@ -59,16 +63,30 @@ 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) -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,22 +94,73 @@ 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 + local cx, cy, cw, ch, n = label_layout:cursor(cursor_pos) + cursor_pos = n 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 -- press if keycode == 262 then -- right - CURSOR_N = CURSOR_N + 1 + cursor_pos = cursor_pos + 1 elseif keycode == 263 then -- left - CURSOR_N = CURSOR_N - 1 + cursor_pos = cursor_pos - 1 else print(keycode) end @@ -102,22 +171,37 @@ 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