Sign in

    moui_tui_renderer

    Experimental terminal cell renderer: projects MoUI semantics frames into ANSI text

    moui
    tui
    renderer
    terminal
    ansi
    experimental
    Download zip
    Author
    Version
    0.2.1
    License
    Apache-2.0
    Last updated
    9 hours ago
    Downloads
    3

    Dependencies

    wzzc-dev/moui_tui_renderer does not have a README file

    CellBox

    pub(all) struct CellBox {
    width : Double
    height : Double
    }

    字符格物理尺寸(逻辑像素)。投影器的坐标系只有这一个原点: 布局盒用像素表达,cell 栅格用它换算。

    CellBox::cols_of

    可视区列数:向下取整,至少 1 列。

    CellBox::default

    fn CellBox::default() -> CellBox

    CellBox::new

    fn CellBox::new(width~ : Double, height~ : Double) -> CellBox

    宿主按自己终端的实际字宽构造。cell 尺寸是投影唯一的标定入口: 它同时决定栅格量化和等宽文本系统的度量,标定错了分栏边界就会错格。

    CellBox::rows_of

    可视区行数:向下取整,至少 1 行。

    Clip

    pub struct Clip {
    chrome :
    Rect

    content :
    Rect

    }

    继承范围要分两条,因为终端的「线」是一整格,而布局的边框只有几像素。

    • chrome:区域画边框可用的范围。List/Tree 的帧和它所属的 ScrollView 重合,两道框必须还能落在同一圈格子上,否则会套出第二道框。
    • content:字形与高亮带可用的范围,也就是那一圈边框的内侧。 边框量化要吃掉一整格,内容却按像素贴着框的内沿起排,两者共用一格时 列表行就写在框线上(实测 100x30:树行首格压在顶边框,末行残格压在底边框)。

    DegradeLog

    pub struct DegradeLog {
    // private fields
    }

    降级诊断日志。投影器不拒绝帧,但必须把"画不出来的东西"记下来, 否则不可投影控件会静默消失,使用者只会看到空白。

    这个句柄由 composition root 创建并持有:RendererSession 会擦掉 renderer 具体类型,所以诊断必须能从会话外面拿到。

    DegradeLog::clear

    fn DegradeLog::clear(self : DegradeLog) -> Unit

    DegradeLog::find

    fn DegradeLog::find(self : DegradeLog, source : String) -> DegradeRecord?

    DegradeLog::new

    fn DegradeLog::new() -> DegradeLog

    DegradeLog::note

    fn DegradeLog::note(self : DegradeLog, source : String, detail : String) -> Unit

    同一来源合并计数,细节保留首次那条。

    DegradeLog::records

    fn DegradeLog::records(self : DegradeLog) -> Array[DegradeRecord]

    DegradeLog::total

    fn DegradeLog::total(self : DegradeLog) -> Int

    被丢弃的项目总数(按来源合并后的次数累加)。

    DegradeRecord

    pub(all) struct DegradeRecord {
    source : String
    detail : String
    count : Int
    } derive(Eq,
    Debug
    )

    一条降级记录:哪个来源、丢的是什么、这一帧出现了几次。

    DegradeRecord::equal

    DegradeRecord::not_equal

    fn DegradeRecord::not_equal(x : DegradeRecord, y : DegradeRecord) -> Bool

    RoleKind

    pub(all) enum RoleKind {
    Region
    Item
    Label
    Content
    Placeholder
    } derive(Eq,
    Debug
    )

    角色投影策略。语义树里没有「这个容器有没有边框」这种视觉事实, 所以画不画框必须由投影器决定,而不是由控件决定。类分五档:
    • Region 区域容器:画边框盒,自己不写字(命名区域的框就是它的表达)
    • Item 结构条目(List/Tree 的一行):自己不写字,内容归子节点, 焦点/选中由整行高亮带表达
    • Label 自带文本的条目(Tab/Option/Cell/Link):写字,不画框
    • Content 叶子内容:把自己的文本写进量化盒
    • Placeholder 不可投影控件:固定占位文本并写诊断,不允许静默消失

    放弃的方案:每个容器都画「带标题的盒」。实测证明它把 Selectable list、Entry 0, row 0 这类聚合名字画在条目同一行上, 被更深一层的文本打断成 e list、EnEntry 0row 0 这种残字 —— 半截标题比没有标题更难读。

    RoleKind::equal

    fn RoleKind::equal(RoleKind, RoleKind) -> Bool

    RoleKind::not_equal

    fn RoleKind::not_equal(x : RoleKind, y : RoleKind) -> Bool

    RoleKind::to_repr

    TuiPalette

    cell 调色板:语义角色 → 终端颜色的唯一入口。

    默认调色板就是 S2–S7 期间的颜色常量,零行为漂移; from_theme 把 @core.Theme 的语义色量化进 xterm-256,让「同一个 @moui.View[Msg] 应用」在终端里也带着自己的主题身份。

    放弃的方案:
    • 在 SemanticsNode 上加颜色字段(owner 已否决):颜色是渲染策略, 不是语义事实,而且会让每个像素渲染器和语义协议为一条输出路径付费。
    • 直接走 truecolor(38;2):保真更高,但冒烟解码器与被测终端都按 xterm-256 假设在走,16/256 量化在视觉上也更接近终端应用的观感。 若未来要 truecolor,只需替换 quantize 的一个函数。
    • 中亮度自动提亮:浅色主题在未知终端底色上的对比问题是 ready 晋升要带着证据解决的问题,不在这里猜。

    TuiPalette::attrs_for

    语义状态 + 角色 → SGR 属性。焦点与选中必须有可见差异, 否则键盘导航在终端里无法辨认。

    TuiPalette::default

    fn TuiPalette::default() -> TuiPalette

    S2–S7 的固定颜色:这就是默认,任何改动都会撞两条 golden 帧。

    TuiPalette::from_theme

    主题 → 调色板:primary 家族归按钮与焦点,secondary 归交互控件, on_surface 系归文本与标题,error 归警示。

    语义角色和调色板角色不是双射:终端只有一层前景色通道,所以 on_*_container 这类「印在色块上的颜色」在这里没有对应物 —— 色块 本身在 cell 投影里不存在。

    TuiRenderer

    pub struct TuiRenderer {
    metrics :
    SurfaceMetrics

    cell : CellBox
    text_system :
    TextSystem

    sink : TuiSink
    diagnostics : DegradeLog
    palette : TuiPalette
    frame_count : Int
    present_count : Int
    offscreen_nodes : Int
    // private fields
    }

    Terminal ANSI renderer:语义帧 → 等宽 cell 栅格 → ANSI 增量。

    纯 MoonBit,不 link native 库也不碰 GPU。渲染输入只有 @core.DrawFrame.semantics,像素命令一律走降级诊断; 量化、归属抢占与盒内折行都留在这个模块里, moui/core 与 moui/runtime 不知道终端的存在。

    TuiRenderer::cols

    fn TuiRenderer::cols(self : TuiRenderer) -> Int

    TuiRenderer::diagnostics

    fn TuiRenderer::diagnostics(self : TuiRenderer) -> DegradeLog

    TuiRenderer::frame_count

    fn TuiRenderer::frame_count(self : TuiRenderer) -> Int

    TuiRenderer::line_text

    fn TuiRenderer::line_text(self : TuiRenderer, row : Int) -> String

    投影后的第 row 行可见文本,用于结构断言而不是字节比对。

    TuiRenderer::new

    fn TuiRenderer::new(metrics~ :
    SurfaceMetrics
    , sink~ : TuiSink, cell? : CellBox, diagnostics? : DegradeLog, palette? : TuiPalette) -> TuiRenderer

    TuiRenderer::offscreen_node_count

    fn TuiRenderer::offscreen_node_count(self : TuiRenderer) -> Int

    最后一帧被丢掉的离屏语义节点数。

    TuiRenderer::present_count

    fn TuiRenderer::present_count(self : TuiRenderer) -> Int

    TuiRenderer::render_frame

    渲染一帧:投影语义树、登记被丢弃的像素命令、只输出脏行增量。

    零增量帧返回 presented=false 且不写一个字节。这不是失败, 而是增量通路生效的证据:内容没变就不该重画终端。

    TuiRenderer::resize

    TuiRenderer::rows

    fn TuiRenderer::rows(self : TuiRenderer) -> Int

    TuiRenderer::set_palette

    fn TuiRenderer::set_palette(self : TuiRenderer, palette : TuiPalette) -> Unit

    换调色板并强制下一帧整屏重建:attrs 遍布每一格,增量通路比较的是 「上次发出的格子」,换色后所有格子都算变了。重建走 Canvas::resize 同款的全帧标记,由下一次 render_frame 发一个 ESC[2J 帧。

    TuiRenderer::text_system

    TuiRenderer::to_renderer_session

    TuiSink

    pub(all) struct TuiSink {
    description : String
    write : (String) -> Unit
    }

    字节出口。cell 投影的产物是 ANSI 文本而不是像素,而 HostSurface::cpu_present() 的 PresentTarget 与 PixelFrame 几何(宽/高/row_bytes/scale)绑死,拿它传字节流会逼协议层为文本 造一个假的像素帧。所以出口由 composition root 提供: terminal backend 把它接到 stdout,测试把它接到捕获缓冲。

    这是对 sun 模板的唯一偏离,host_surface_metrics_error 仍然是 共用的 bind 门禁。

    TuiSink::describe

    fn TuiSink::describe(self : TuiSink) -> String

    TuiSink::discard

    fn TuiSink::discard() -> TuiSink

    丢弃所有输出:用于只要结构不要字节的 headless 场景。

    TuiSink::emit

    fn TuiSink::emit(self : TuiSink, bytes : String) -> Unit

    TuiSink::new

    fn TuiSink::new(description~ : String, write~ : (String) -> Unit) -> TuiSink

    ansi

    fn ansi(sink~ : TuiSink, cell? : CellBox, diagnostics? : DegradeLog, palette? : TuiPalette) ->
    RendererProvider

    Build a terminal ANSI provider from the renderer-neutral host contract.

    Unlike the Sun raster factory this provider presents to a composition-root TuiSink instead of surface.cpu_present(): the PresentTarget contract carries PixelFrame geometry, and ANSI bytes have no width/row_bytes. host_surface_metrics_error stays the shared bind gate, so a zero-sized surface is still rejected the same way for every renderer.

    ansi_runs

    fn ansi_runs(runs : Array[(String, Int,
    CellAttrs
    )]) -> String

    一行 = 若干同属性文本段。宽字符右半格已经在 runs() 里跳过。

    attrs_for

    默认调色板的角色 → 样式入口;渲染器持有 TuiPalette 并显式调用 palette.attrs_for。这里保留的默认版是给测试与「无宿主注入主题」 场景的便捷门面(也是两条 golden 帧锁定的颜色路径)。

    cell_text_system

    fn cell_text_system(cell : CellBox) ->
    TextSystem

    等宽 cell 文本系统:让既有 layout() 在不接 GPU/字体栈时也能算出盒高。 度量单位就是 CellBox,所以布局结果与投影栅格共用一套网格。

    cell_units

    fn cell_units(text : String) -> Int

    文本占用的 cell 数(换行符不占格,CJK/emoji 按双宽计)。

    clips_in_tree

    继承范围按广度优先序逐层收窄,与 paint_order 同一条通路。 树里指不到或来自环之外的节点拿可视区,和「补在尾部」的规则一致。

    frame_update

    fn frame_update(screen :
    TermScreen
    , damage :
    Damage
    ) -> String

    脏行 → 逐行重绘指令。整屏脏时先清屏,避免缩小窗口后的残影。

    node_text

    fn node_text(node :
    SemanticsNode
    ) -> String

    语义节点的可见文本:label 优先,其次 value,最后 description。

    note_dropped_commands

    cell 投影只吃语义树,像素命令一律丢弃。丢弃必须点名,不能沉默。

    paint_order

    从根开始的广度优先序:浅层先画,深层后可以抢占浅层的格子。 这就是 S0 定下的 z-order 策略。游离于根之外的节点补在尾部, 保证不会因为树不完整而整个丢掉。

    quantize

    fn quantize(frame :
    Rect
    , cell : CellBox, cols : Int, rows : Int) -> (Int, Int, Int, Int)

    量化成绘制盒。子帧 ⊆ 父帧,所以按中心取格不会让分栏边界散架。

    quantize_color

    颜色 → xterm-256 索引:三通道接近的中性色走 24 级灰阶,其余走 216 色立方。

    role_kind

    全匹配 SemanticsRole,新增角色会让这里编译失败,而不是静默降级。

    role_priority

    fn role_priority(kind : RoleKind) -> Int

    绘制优先级:边框 1 < 标题 2 < 叶子内容 3。同优先级先到先得。 Item 不写字,拿边框档,免得它的占位符挤掉真正的叶子。

    role_tag

    sgr

    fn sgr(attrs :
    CellAttrs
    ) -> String

    damage → ANSI。整屏脏就清屏重建,否则只按行定位重绘, 未变的行一个字节都不出。

    tui_feature_capabilities

    全特性能力表。provider_id 是能力报告的聚合身份, backend 只是诊断分类元数据(ADR 0027)。

    tui_feature_note

    fn tui_feature_note(feature :
    RendererFeature
    ) -> String

    tui_feature_status

    TUI cell 投影的特性能力。这里记录的是投影器真正能做到的事, 不是像素渲染器的能力表换个名字:等宽栅格没有渐变、阴影、 变换与矢量路径,把它们写成 supported 会让能力报告说谎。

    tui_terminal_ansi_native_descriptor

    fn tui_terminal_ansi_native_descriptor() ->
    RendererDescriptor

    viewport

    fn viewport(cell : CellBox, cols : Int, rows : Int) ->
    Rect

    一个节点能被画到的最大范围。根节点用它起步, 深层节点只会继承更窄的范围。