Skip to content

feat(text): add styled layout query support#105

Merged
cloudwu merged 12 commits into
cloudwu:masterfrom
yuchanns:feat/font-extapi-opaque
Jul 5, 2026
Merged

feat(text): add styled layout query support#105
cloudwu merged 12 commits into
cloudwu:masterfrom
yuchanns:feat/font-extapi-opaque

Conversation

@yuchanns

@yuchanns yuchanns commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

text layout 新增/扩展的接口:

  • style 配置增加 line_height 字段。
  • layout 查询对象提供:
    • height():文本实际高度;
    • line_height():最大行高;
    • line_count():行数;
    • cursor_count():cursor position 数量;
    • line(index):查询某一行的 start/finish/x/y/width/height
    • style(position):查询 visible position 对应的 style id。

另外新增了 soluna.material.clip

  • matclip.rect(width, height):打开一个矩形裁剪范围;
  • matclip.rect():关闭最近一次打开的裁剪范围。

Yoga 绑定增加了 overflow,可以设置 visible/hidden/scroll


现有的 soluna font 没法满足我编写 app 的需求, 不过对文字的复杂排版、测量和自定义渲染我觉得也不是游戏引擎会太关心的事情, 所以基于 extapi 自己写了个文字扩展材质,但是缺少一些 api, 需要通过 solunaapi 导出, 主要是字体测量和 glyph 查询相关的.

我不确定这样实现边界是否合适.

@yuchanns yuchanns force-pushed the feat/font-extapi-opaque branch from 8d172cb to 81b1eac Compare June 27, 2026 10:47
@cloudwu

cloudwu commented Jun 27, 2026

Copy link
Copy Markdown
Owner

讨论一下这里的 api 设计。如之前所说,extapi 其实是不太好一次设计好的,所以修改在所难免。但最好可以保持通用简洁。

我之前也没有太仔细看 extapi 的接口设计,刚才重新看了一下。

先整理一下 soluna 的材质模块的设计:

soluna 内置的材质数据流,是基于 struct draw_primitive 这个结构的数组。它定义在 batch.h 中:

struct draw_primitive {
	int32_t x;		// sign bit + 23 + 8   fix number
	int32_t y;
	uint32_t sr;	// scale + rot
	int32_t sprite;		// negative : material 
};

默认的材质,每个 sprite 就用一个结构体描述它,这个结构就包含了 sprite id 和它的 SRT 变换。

当我们扩展材质后,如果扩展材质的每个图元需要额外的数据,就采用两个这么长的结构描述一个图元。为了和默认材质区分开,在 sprite 字段填入负数,表示材质编号,但 SRT 依旧保留。接下来的 16 字节(即这个结构长度)则可以放一些扩展材质需要的图元数据。但大多数扩展材质依然公用原有的 sprite bank ,也就是继续去画系统管理的 sprite (它们的像素也储存在系统管理的贴图上),由于扩展材质的 sprite 字段变成了材质号,所以真实的 sprite 就放在了接下来的 16 字节的前 4 字节。即这个结构

struct draw_primitive_external {
	int sprite;
};

真正供扩展材质放额外图元数据的是接下来的 12 字节。

例如,内核实现的 text 材质,就是这样定义扩展结构的:

struct text {
	struct draw_primitive_external header;
	int codepoint;
	uint16_t font;
	uint16_t size;
	uint32_t color;
};

以上 stream 是放在 CPU 内存中的数据,不同材质的数据全部放在统一的 steam 中。

在绘制阶段,一部分的数据会被组织起来提交到 GPU 的 buffer 中(VB),这个过程在引擎中叫做 submit ;另一部分数据会从 stream 里分离出独立的回值流程,被称为 draw 。draw 过程通常是从前面的 stream 中获取图元相关的信息填到 uniform 里,然后调用 sokol 的 sg_draw 完成。

submit 和 draw 都依赖 stream 里的数据。


下面是我推敲的 extapi 的设计思路,并不一定全部正确。

extapi 要做的事情是想把以上,尽可能的封装起来,减少细节的暴露。

soluna_material_push_stream() 用来把数据按内核规定的 stream格式组织起来。它只指定图元的个数,每个图元都会调用一个 soluna_material_stream_write_func callback 。

从前面的描述可知,每个图元必须固定的数据是 SRT / sprite 和额外的最多 12 字节的 payload 。extapi 放弃了 SR 的支持,只允许写入 T ,也就是 x,y 字段,同时允许写入额外的 sprite 和 payload 。注:虽然扩展材质在这里没有支持写入旋转和缩放,但是因为内核支持图层的旋转缩放,所以在最终的 stream 数据中,还是可能有 SR 的。

soluna_material_submit() 封装了把 CPU stream 数据提交到 GPU buffer 的过程。为什么要封装?因为在从 CPU 的 stream buffer 向 GPU 的 buffer 转换时,涉及了某种数据结构的转换,需要一个临时的内存 buffer 中转。在内核原有的数据中,使用了一个固定长度的 tmp_buffer 。这导致了批量 submit 不可以有太多数据,导致其超出这个固定 tmp buffer 的长度。

疑问 :其实在 extlua_sample.c 中,tmp_ptr 是自己实现的,并没有使用内核的固定长度的 tmp buffer 。所以,理论上并不受此限制。

看起来,extapi.c 中的 submit_material_stride() 函数,用一个循环把 submit 这个 callback 的每次调用的图元个数限制在 batch_n 之下,看起来就没有什么意义。而这个函数所作的也仅仅是把 submit 分开。如果不受 batch_n 之限制,其实就没有必要把 soluna_material_submit 作成 callback 驱动的形式了,extapi 会简单得多。


soluna_material_stream_read() 是用来把以上组织在 CPU stream 里的数据再读回来。它在用户写的 submit 过程中调用,读出每个图元的信息,通过 sg_append_buffer() 写入 GPU 的 buffer 。这里,通过结构 struct soluna_material_stream_data 把图元的 x,y,sprite 读回来。扩展材质模块可以在 submit 里调用它组织真正放进 VB 的数据。

疑问 :这个 patch 扩展了 struct soluna_material_stream_data,增加了 axis_x 和 axis_y ,看起来有点草率?原始的内核 stream 里其实是 SRT 和 sprite ,这里转换成另一种形式,感觉不太直观。可能有更好的定义方法?


关于这个 patch ,增加了大量 font 有关的 api 。我的直觉是,把 font 再归到一个独立的模块里,类似之前 sokol lua 的分类感觉更好,这样或许可以和 soluna 这块的变动分离开。然后 font 相关的东西也可以独立探讨设计。

@cloudwu

cloudwu commented Jun 27, 2026

Copy link
Copy Markdown
Owner

抛开目前的 extapi 中的 soluna api 部分。我说说我对扩展材质的想法。首先,这个感觉应该叫 material api (也就是之前版本的 soluna api ),这样就可以和 font api 并列了。

底层因为把所有的材质都放在了一个 stream 里,也就是 struct draw_primitive 数组,扩展材质必须是两个 struct draw_primitive 长度,共 32 字节。其中,留个用户自定义的部分就是 12 字节。前 20 字节都是固定的,也就是 12 字节的 SRT ,4 字节的材质编号,4 字节的 sprite 编号。

如果不使用内部的 sprite 图像数据和贴图管理(如果使用内部 sprite ,底层会利用它管理贴图),那么 sprite 编号固定为 -1 ,就是内部的 text 材质实现。

我觉得不如就固定给扩展材质预留 12 字节的自定义数据,不用额外作变长的处理更简单?这样 payload 相关的 api 设计似乎可以简化不少。

扩展材质模块通常需要自定义什么呢?除了 shader 外,主要是和一致的 stream 交换数据。框架是通过分析 stream 里的材质编号来找到一段相同的数据,交给对应的材质模块 submit 和 draw 的。

材质模块从 lua side 看,主要就是需要提供三个 lua 函数:

第一,用于把用户数据放进底层的 stream 中,通常只有那 12 字节是自定义的。某些材质可能还需要 sprite 。至于图元的 SRT ,用户可以统一用底层的嵌套图层设置。对应内置的 text 材质,就是 block 和 char 两个函数干的事情。

第二,用于把 stream 的数据提交到 VB 。我认为有两种做法,先讨论目前已有的实现,即让用户自己调用 sokol 的 sg_append_buffer() 完成。这样自由度最高。但我觉得其实目前的 api 可以简化。尤其是用户并不需要特别对扩展材质作性能优化,完全可以把合并图元去掉,一次只画一个图元。其实目前的 sample 也是如此。

目前,每个材质的 lua 接口会提供一个名为 submit 的函数。现在 extapi 的思路是:如果用户自定义材质,就让用户自己实现一个 submit() 函数供框架调用。框架调用它时,会传入三个参数,分别是材质对象、stream、图元个数 。这个 submit 需要用户自己用 C 实现,解析 stream ,调用 sg_append_buffer() 。这里 stream 我们不希望公布数据结构的细节,细节本身也很复杂,不适合用户直接取用,所以又提供了 soluna_material_stream_read 解析它。

但我们换个思路,接口可以简单得多。

用户自定义材质并不需要直接实现 submit() ,而实现一个变体 submit_one() ,它只提交一个图元。我们在框架实现一个统一的 submit 用于用户自定义材质,它把 stream 数据转换为方便用户解析的 SRT sprite 以及 12 字节 payload ,取代 stream 直接把数据传给用户。这个函数就好实现的多,也不需要 callback 了。甚至还可以把 sg_append_buffer() 也封装起来。这样用户写的函数要做的仅仅是 CPU 的数据格式转换,转换为 VB 的数据结构而已。

第三,用户根据 stream 数据调用 sg_draw 做真正的绘制。这里框架会调用一个叫 draw() 的函数,传入四个参数:材质对象、stream、图元数量,以及根据 sprite 获取的贴图 id 。

当前的 sample 忽略了 stream 。但实际上大多数材质需要解析 stream 里图元内的数据设置 uniform 。比如 text 材质就是从额外的 payload 里找到颜色设到 uniform 里的。这里解析图元中的 SRT 似乎没什么意义,主要是额外的 payload 才有意义。若想多封装一点,其实和上一条思路一样,未必需要暴露 sokol api ,只需要让用户自定义函数填写单个图元的 uniform 即可。一次 draw 一个图元。除了填写 uniform 外,所有的用户自定义材质的行为都是一致的。如果没有额外的 uniform 需要填写,现在的 sample 就是通用的材质 draw 。


总结:用户自定义材质本质上是写一个 shader 来用自定义方式渲染图元数据。核心在定义 shader ,但 shader 绘制数据的来源分别是 VB 里的 instance 和每次 draw 的 uniform 。我感觉用户自定义材质只需要用户去实现三个 lua 函数,它们都是做数据转换,sokol api 则可以放在之上,由框架实现好。

  1. 把 lua 数据转换为 12 字节的 payload 。
  2. 把 stream 内单个图元的 SRT / sprite / payload 转换为 VB 中的 instance 结构。
  3. 把 stream 内单个图元的 payload 填充到材质对象中的 uniform 结构中,或者可以使用通用 draw 。

@yuchanns

Copy link
Copy Markdown
Contributor Author

其实在 extlua_sample.c 中,tmp_ptr 是自己实现的,并没有使用内核的固定长度的 tmp buffer 。所以,理论上并不受此限制。

tmp_buffer = ctx.tmp_buffer,

m->tmp_ptr = lua_touserdata(L, -1);

目前使用的是内核传过来的 tmp buffer 。不过你说得对,确实可以自己实现. 我只是根据现有的材质的注册流程总结出来这样一个模式直接照搬过来实现。

这个 patch 扩展了 struct soluna_material_stream_data,增加了 axis_x 和 axis_y ,看起来有点草率?原始的内核 stream 里其实是 SRT 和 sprite ,这里转换成另一种形式,感觉不太直观。可能有更好的定义方法?

这里是因为我觉得不应该披露 bit packing 的细节。

这个感觉应该叫 material api

确实这样更合适。全塞在 solunaapi 有点太大了

现在 extapi 的思路是:如果用户自定义材质,就让用户自己实现一个 submit() 函数供框架调用。框架调用它时,会传入三个参数,分别是材质对象、stream、图元个数 。这个 submit 需要用户自己用 C 实现,解析 stream ,调用 sg_append_buffer() 。这里 stream 我们不希望公布数据结构的细节,细节本身也很复杂,不适合用户直接取用,所以又提供了 soluna_material_stream_read 解析它。

确实是这么想的。我不确定应该封装到什么程度合适,会不会导致限制太大,所以就决定自由点自己 submit

基于上述建议, 我重写了一版: #106

@yuchanns yuchanns marked this pull request as draft June 28, 2026 10:03
@cloudwu

cloudwu commented Jun 28, 2026

Copy link
Copy Markdown
Owner

现在 extapi 的思路是:如果用户自定义材质,就让用户自己实现一个 submit() 函数供框架调用。框架调用它时,会传入三个参数,分别是材质对象、stream、图元个数 。这个 submit 需要用户自己用 C 实现,解析 stream ,调用 sg_append_buffer() 。这里 stream 我们不希望公布数据结构的细节,细节本身也很复杂,不适合用户直接取用,所以又提供了 soluna_material_stream_read 解析它。

确实是这么想的。我不确定应该封装到什么程度合适,会不会导致限制太大,所以就决定自由点自己 submit

我觉得用户自定义材质,可以先限制大一点,以更少的 api 和数据结构优先。也不用过于考虑性能。后面遇到新需求时再重构更好。

我甚至觉得,绘制 sprite 只是加一些特效的可能可以归为一类;
而渲染非 sprite 对象,比如文本甚至 3d 模型或者粒子片这些都有可能分开。渲染管线或许会不一样。

目前没那么多需求时,才把两者合并起来处理。有填充 vb 和 uniform 的能力已经能解决大部分自定义需求了。毕竟不是要做 3A 游戏会去抠性能和特别的效果。

现在常见的材质需求是缺少类似 render target 这种设施,但底层框架本来就没有支持。

@yuchanns yuchanns force-pushed the feat/font-extapi-opaque branch 2 times, most recently from 95c0d30 to 0330e60 Compare June 28, 2026 14:06
@yuchanns yuchanns changed the title feat(extapi): expose font queries and stream basis feat(extapi): expose font apis Jun 28, 2026
@cloudwu

cloudwu commented Jun 28, 2026

Copy link
Copy Markdown
Owner

font 现在提供的 api 是在 C side 调用吗?

如果主要是给 lua 用的话,我有一些想法。如果是给 C 用,意见不大。

@yuchanns

Copy link
Copy Markdown
Contributor Author

现在实现上是给 C 用。 不过可以说说看。

@yuchanns

yuchanns commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author
image

写了个 demo 展示下使用方式
https://github.com/yuchanns/soluna_fontapi

@yuchanns yuchanns force-pushed the feat/font-extapi-opaque branch from bb4690d to b67746e Compare June 28, 2026 18:46
@yuchanns yuchanns marked this pull request as ready for review June 28, 2026 18:54
@cloudwu

cloudwu commented Jun 29, 2026

Copy link
Copy Markdown
Owner

目前内核里的 text 部分也有些未实现的功能,只是还没想好对应的应用去做。主要是想增加点击特定文字段的支持,以及显示上加下划线。目前已有的功能很难做到。

我设想的是在输入文本时可以给特定文本加标签,标记上 tag ,有 api 可以查询特定位置的点是否落在某个标签上;同时可以在渲染时给标签传入颜色。这样可以实现鼠标悬停的时候文本变色。

目前的理念是,尽量少在核心部分加特性。但当下文本显示是单向的,无法查询具体文字所在的区域,和鼠标做交互很难。通过现有功能,在 lua 层难以实现点击一段标注的文本。所以我觉得这个是必要的特性。

@cloudwu

cloudwu commented Jun 29, 2026

Copy link
Copy Markdown
Owner

我的想法是,看起来现在增加的 font api 都是查询接口,功用上是类似的。可能合并成一个比较好?

查询时增加一个枚举量,返回到 union 里面,根据枚举量决定返回什么。

我猜想潜在的好处是 ABI 会更稳定一些。因为即使后面增加和修改功能,只用增加新的枚举值,返回对应结构,并不需要改变 ABI 。新版本去查老版本结构,也只是失败而已。

不过,多个接口也有好处,可以在编译链接时做更多校验。ABI 不变似乎也不是特别重要。

@yuchanns

Copy link
Copy Markdown
Contributor Author

我说明一下最早想引入 fontapi 的动机:

一开始我尝试使用现有的 mattext.block 实现以下能力:

local block, cursor = mattext.block(font.cobj(), fontid, size, color, align, line_height)
  • 文本高度测量:对整段文本取末尾 position,调用 cursor(text, len, width, large_height),用返回的 y +
    h 作为实际文本高度。
  local TEXT_MEASURE_HEIGHT = 4096

  local function text_height(text, width)
    text = text or ""

    local position = utf8.len(text) or 0
    local _, y, _, h = cursor(
      text,
      position,
      width,
      TEXT_MEASURE_HEIGHT
    )

    return y + h
  end
  • 鼠标命中字符位置:从 x/y 反推 text position,Lua 层遍历 0..len,对每个 position 调
    cursor(...),找离鼠标点最近的位置。
  local function text_position(text, width, height, x, y)
    text = text or ""

    local len = utf8.len(text) or 0
    local best_position = 0
    local best_score = math.huge

    for position = 0, len do
      local cx, cy, _, ch = cursor(text, position, width, height)

      local dy = 0
      if y < cy then
        dy = cy - y
      elseif y > cy + ch then
        dy = y - cy - ch
      end

      local score = dy * 10000 + math.abs(x - cx)
      if score < best_score then
        best_score = score
        best_position = position
      end
    end

    return best_position
  end
  • 选区绘制:为Lua 层逐字符调用 cursor(position) 和 cursor(position + 1),再拼出
    每个字符或每一段的矩形。
  local function append_selection_rect(rects, width, height, text, position)
    local x1, y1, _, h1 = cursor(text, position, width, height)
    local x2, y2 = cursor(text, position + 1, width, height)

    local w
    if y1 == y2 and x2 >= x1 then
      w = x2 - x1
    else
      w = width - x1
    end

    if w > 0 then
      rects[#rects + 1] = {
        x = x1,
        y = y1,
        w = w,
        h = h1,
      }
    end
  end

  local function selection_rects(text, width, height, start_pos, end_pos)
    local rects = {}

    for position = start_pos, end_pos - 1 do
      append_selection_rect(rects, width, height, text, position)
    end

    return rects
  end
  • 滚动裁剪:为了找到某个 scroll_y 对应的可见起始文本位置,需要遍历 position 并观察 cursor 的 y 变
    化。
  local function visible_start(text, width, scroll_y)
    text = text or ""

    local len = utf8.len(text) or 0
    local start = 0
    local start_y = 0
    local last_y = nil

    for position = 0, len do
      local _, y = cursor(
        text,
        position,
        width,
        TEXT_MEASURE_HEIGHT
      )

      if y ~= last_y then
        if y > scroll_y then
          break
        end

        start = position
        start_y = y
        last_y = y
      end
    end

    return start, start_y
  end

遇到的问题:

  1. 测量会隐式触发排版, cursor 方法不便宜。根本原因是排版结果没保存。
  2. 反向查询命中需要枚举整段文字,在大段内容也有性能问题
  3. 文本跨行拼接来实现选中高亮

BTW, 滚动裁剪可能还涉及到 BATCH 支持裁剪渲染, 不过目前我用了一些绕过的方法, 而且我觉得游戏好像都是通过 layer 变换来实现的,所以先不管.

一开始我觉得这些东西要改造 text 的话,和游戏关系不大动力不强,干脆自己写个文字材质来使用,保留排版结果,形状大概是这样:

  local text = require "ishiku.text_engine"
  local font = require "soluna.font"

  local width = 360
  local size = 16
  local color = 0xffffffff
  local align = "LT"
  local line_height = 22

  local height, actual_line_height, line_count = text.measure(
    font.cobj(),
    font_id,
    "hello world",
    width,
    0,
    size,
    color,
    align,
    line_height
  )

  local layout = text.layout(
    font.cobj(),
    font_id,
    "hello world",
    width,
    height,
    size,
    color,
    align,
    line_height
  )

  local stream = layout:stream()
  batch:add(stream, x, y)

  local cx, cy, cw, ch = layout:cursor(position)

  local position = layout:hit(mouse_x - x, mouse_y - y)

  local rects = layout:selection(start_pos, finish_pos, width, height, 1, 3, scroll_y)

  local start_pos, start_y, finish_pos = layout:visible(scroll_y, viewport_height)

layout 保留了排版信息, 可以重复查询.

因为是从实现一个材质的角度出发,所以我首先想到的就是暴露一些 C APIs:

  • font_metrics(font, font_id, size, &metrics): 计算文本总高度; line height / line count;每个 glyph / cluster 的 advance; 自动换行; cursor position; hit-test 所需的 line/cursor 数据
  • font_glyph_metrics(font, font_id, codepoint, size, &glyph): 同上
  • font_glyph_image(font, font_id, codepoint, size, &image): submit 阶段查 glyph image / atlas 位置等

你说得没错, 这几个看起来确实可以合并成一个 font query.

当然如果能直接复用 soluna text lua side 来支持这些能力, 那我省的写材质了, 这些 c api 多半也不需要了.

@yuchanns yuchanns marked this pull request as draft June 29, 2026 04:41
@yuchanns yuchanns force-pushed the feat/font-extapi-opaque branch from b67746e to 6fc00de Compare June 29, 2026 04:55
@yuchanns

yuchanns commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

合并了以后保留两个方法: font_inspect 和 font_resolve_glyph 。后者不是纯查询,会生成 SDF ,不太确定是不是应该导出,这个主要是因为自己实现 text 材质 submit 的时候需要。如果能走内置 mattext 应该就不需要。

@cloudwu

cloudwu commented Jun 29, 2026

Copy link
Copy Markdown
Owner

我想想怎么实现这个功能。不过,即使额外实现,也可以用生成好的 sdf ,为什么需要额外生成

@yuchanns

Copy link
Copy Markdown
Contributor Author

也可以用生成好的 sdf ,为什么需要额外生成

因为我现在这样的实现完全不走 mattext, 没有调用 block 也没有 submit 不会生成 sdf.

@yuchanns yuchanns force-pushed the feat/font-extapi-opaque branch from 6fc00de to a2a8df4 Compare June 29, 2026 09:43
@cloudwu

cloudwu commented Jun 29, 2026

Copy link
Copy Markdown
Owner

我感觉这个需求是:

  1. 针对屏幕位置求出在文本中的字符位置
  2. 求出文本中的一段的包围矩形,或多个矩形(如果有分行)
  3. 如果可能,在 2 的基础上画下划线或矩形。

3 可以做在文本渲染模块中,也可以只取得数据额外做。

我会试着扩展以下文本材质模块。

@yuchanns

Copy link
Copy Markdown
Contributor Author

我觉得3取得数据,额外做比较有自由度

@cloudwu

cloudwu commented Jun 30, 2026

Copy link
Copy Markdown
Owner

我今天想重构一下这个文字模块。之前写的就是根据 deepfuture 的需求演变做了点刚刚够用的。

  1. 最早的版本,只有一个函数用来排版一个字符串。接口是 mattext.block(fontcobj, fontid, 32, 0, "CV") 也就是根据 font 对象,fontid ,文本大小,文本颜色,排版的对齐规则,生成一个排版绘制函数,这个函数用来把具体文本内容转化为绘图的 batch 指令流。

  2. 随后发现还需要绘制光标。因为不想破坏写好的接口(偷懒/大多数文本显示不需要光标),就简单的返回了第二个光标位置查询函数。

1 和 2 算法上有一定的相关性,但最终需要的数据不同。1 要的是每个字符的位置信息 2 要的是在特定两个字符之间的光标的位置。实现上,把两者实现在了同一个函数中,通过参数决定要哪一份数据。

现在看起来,2 这个功能增加的非常草率。其实还会有更多的需求。

我觉得或许可以改成一个好一点的设计:

继续保留 1 的排版函数,但按需把额外的排版信息记录到一个 userdata 中。这个 userdata 可以用来做进一步的查询。比如查询光标位置,查询一个选块的区域等等。

这样, 2 的接口就挪到了后者之中。但不再是重新做一次排版,而是根据 1 步骤获得的 userdata 获取相关信息。

这是个破坏性修改,我就把 deepfuture 相关的改过来,应该影响不大。

而这一步完成之后,你可以再根据具体需求直接在此基础上增加需要的其它特性。

@yuchanns

Copy link
Copy Markdown
Contributor Author

SGTM

@cloudwu

cloudwu commented Jul 1, 2026

Copy link
Copy Markdown
Owner

改了个初版,谨慎起见,尽可能的和之前的接口保持一致。 fddf5b9

mattext.block() 返回的第一个渲染器完全没有变,第二个从获取 cursor 改为获取一个布局器。然后可以用 layout:cursor() 查询光标位置。返回值和过去也保持一致。

我认为下面可以从这个布局器接口入手修改。它是一个 userdata ,为 C 结构 struct layout 。保存了每个字符的位置等信息。
其中 struct position 数组的 x,y 和之前 cursor 查询到的位置语义一样,cursor 的高度在 layout.line_height 中。

position 结构中还有一个 w 是那个字符的 advance_x ,目前的实现没有用到。但我认为如果要实现选择框应该需要。因为,每行最后一个字符只有左侧坐标,而下一个字符就去到下行的开头了。

@cloudwu

cloudwu commented Jul 1, 2026

Copy link
Copy Markdown
Owner

更新 soluna 后,deepfuture 的 Nightly Deploy 失败了 。但看起来并不是这次修改引起的。麻烦 @yuchanns 看看。

@yuchanns yuchanns force-pushed the feat/font-extapi-opaque branch from db248b4 to b4ea3fa Compare July 3, 2026 09:49
@cloudwu

cloudwu commented Jul 3, 2026

Copy link
Copy Markdown
Owner

我把计划中的完成了,暂不打算做更多的东西。所以 @yuchanns 可以基于这个版本修补。因为逻辑比较复杂,所以不排除有 bug 。有实际使用应该可以更容易发现 bug 。

目前的 test/text.lua 展示了更改 style 的效果。还是统一用的 [n] 重置回 0 号默认 style (第一个)。没支持嵌套语法是因为这样更简单一些,icon 和 style 都只用数字 id 也是一个道理。

这样让核心部分的代码尽量简单点,应用层可以再这个基础上封装成带嵌套语义的也不难。基本是一个字符串转换。style 可以在上面的封装层用单词代替数字(类似 deepfuture 加了个封装层,用单词描述 icon id )。

@cloudwu

cloudwu commented Jul 3, 2026

Copy link
Copy Markdown
Owner

ps. 我不反对改 block 的签名。目前的版本兼容了新老版本。新的当然也不是很好看。如果想改一下的话,最后把 deepfuture 一起改完 :D

@cloudwu

cloudwu commented Jul 3, 2026

Copy link
Copy Markdown
Owner

渲染半行文字用裁减器实现最为简单。

裁减器理论上可以通过增加一个扩展材质就可以完成。当然,写到内核更简单。

我还没想好具体的接口怎么设计更好。但本质上就是加一个特殊的材质 batch 到 stream 里。这个材质的对象不渲染任何东西,只是在 draw 方法里调用一下 sg_apply_scissor_rect 就可以了。

如果 layer 没有旋转,裁减器也可以内置到 mattext 里,因为它本身就有个矩形。但内核的其它部分都支持 layer 的旋转缩放,所以文本也是可以通过 layer 变换的,所以硬编码到里面不太好。

@yuchanns yuchanns force-pushed the feat/font-extapi-opaque branch from b4ea3fa to 826df74 Compare July 4, 2026 13:53
@yuchanns yuchanns force-pushed the feat/font-extapi-opaque branch from 826df74 to cc214de Compare July 4, 2026 13:54
Comment thread src/material_text.c
@yuchanns yuchanns force-pushed the feat/font-extapi-opaque branch from 82136eb to ab759ee Compare July 5, 2026 03:50
Comment thread src/material_text.c Outdated
@cloudwu

cloudwu commented Jul 5, 2026

Copy link
Copy Markdown
Owner

我简单浏览了一下实现,里面在 layout 结构里做了一些改动,主要是合并了重复的 font style ,避免每个字符的 position 重复一些类似 ascent 这样的数据。我认同目前的改动,实际上我原来打算作类似的工作(放在最初的迭代工作之后)。

但我设想的实现方法有所区别。写一下供参考:

在 struct layout 结构中直接放一个 struct font_style 指针(这时 n 不重要,因为 layout 数据是不变的,不会越界),而不是额外在生成一个 struct layout_style 结构。

这里有一个问题:生成 layout 的时候,兼容老 api 同时也为了很多并不需要多种 style 混排的场合,并不一定有一个额外的 struct styles userdata 。如果为这种场合硬额外生成一个 n=1 的 struct styles userdata 就浪费了。

为了解决这个问题,可以在 struct layout 中固定保留一个 struct font_style ,处理单一 style 的情况。让指针指向内部这个结构即可。如果外部额外传了一个 userdata ,就指向外部 userdata ,同时需要在 layout 的 uservalue 里作一次引用(避免 gc 提前回收),即,lua_newuserdatauv 时传 1 多分配一个 uservalue 出来,用 lua_setiuservalue 引用。

当然,这个保留单一 struct font_style 未必需要在 struct layout 中定义,也可以像现在这样保留在尾部,只是代码需要多写几行计算动态的尾部(就像现在做的那样)。

这样做的潜在好处是:不用定义两个用途非常类似的结构,struct layout 的数据结构也相对简单一点。代价是 lua object 的引用关系略复杂一点。

@yuchanns yuchanns force-pushed the feat/font-extapi-opaque branch from ab759ee to cfd6427 Compare July 5, 2026 05:22
@yuchanns

yuchanns commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

已根据意见处理。顺便把其他几处可以计算的也简化了结构。

@cloudwu

cloudwu commented Jul 5, 2026

Copy link
Copy Markdown
Owner

如果暂时不做更多改动了,那么就可以先合并。等出了问题再改也行。

@yuchanns yuchanns marked this pull request as ready for review July 5, 2026 05:58
@yuchanns

yuchanns commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

已就绪

@yuchanns yuchanns changed the title feat(extapi): expose font apis feat(text): add styled layout query support Jul 5, 2026
@cloudwu cloudwu merged commit 19c4e37 into cloudwu:master Jul 5, 2026
7 checks passed
@yuchanns yuchanns deleted the feat/font-extapi-opaque branch July 5, 2026 07:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants