Skip to content

Repository files navigation

fbiw

fbiw 是一个使用 Go 编写的轻量级 GUI 框架,最初面向使用 Linux framebuffer 和游戏手柄按键的固定屏幕设备,现在也可在 macOS 窗口和支持 WebGL 的浏览器中运行。

它自行实现了精简的 HTML/CSS 风格界面描述、DOM、布局和事件传播,并通过平台后端绘制:Linux 使用 framebuffer 或 GLES,macOS 通过 SDL2 创建窗口并优先使用 Metal,浏览器使用 Go js/wasm 和 WebGL。浏览器端复用框架自己的界面系统,不使用浏览器的 DOM/CSS 引擎进行布局和绘制。

项目目前仍处于开发阶段,适合方向键操作的掌机菜单和系统界面,不应视为完整的浏览器布局引擎或通用桌面 GUI 框架。

特性

  • 使用类似 HTML 的文档描述界面;
  • 支持标签、ID、class、后代和直接子元素等 CSS 选择器;
  • 内置纵向、横向、单行 Flex、叠层和虚拟列表布局;
  • 支持始终位于文档栈上方的系统覆盖层,以及供文档避让覆盖层的安全区域;
  • 支持颜色、背景图片、边框、内边距、尺寸、字体和对齐等样式;
  • 支持 OpenType 字体、字形缓存和文本分段;
  • 支持 PNG 等 Go image 包可解码的图片,并提供缩放缓存;<img> 中的 GIF 自动循环播放;
  • 事件支持捕获、目标和冒泡阶段;
  • 提供异步资源加载和主线程 UI 回调;
  • Linux 使用 /dev/fb0/dev/input/event*
  • macOS 使用 SDL2 模拟屏幕和按键;
  • 浏览器通过 Go js/wasm 和 WebGL 绘制界面,并支持键盘输入。

环境要求

  • Go 1.27,以 go.mod 的声明为准;当前使用实验性的 simd/archsimd,构建和测试时需要设置 GOEXPERIMENT=simd
  • macOS 开发环境需要安装 SDL2 及其开发文件;
  • Linux 目标设备需要提供 framebuffer 和 evdev 输入设备,并允许程序访问:
    • /dev/fb0
    • /dev/input/event*

安装 Go 包:

go get github.com/movsb/fbiw

浏览器示例

demo/browser 提供 WebGL 浏览器端口的示例。构建 wasm 并启动本地 HTTP 服务:

GOOS=js GOARCH=wasm go build -o demo/browser/app.wasm ./demo/browser
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" demo/browser/wasm_exec.js
python3 -m http.server 8765 --directory demo/browser

打开 http://localhost:8765/。页面需提供 ID 为 fbiw-canvas<canvas>,并加载与 Go 版本匹配的 wasm_exec.js。画布随浏览器窗口调整;键位与 macOS 端口一致(W/A/S/D、K/J 等)。

快速开始

准备一个界面文件 main.html

<document>
<style>
    #panel {
        width: 400;
        height: 240;
        padding: 20;
        background-color: white;
        align: both;
    }

    .title {
        color: black;
        font-size: 28;
    }
</style>

<block id="panel">
    <text class="title">Hello, fbiw!</text>
</block>
</document>

在 Go 中嵌入文档和字体并运行应用:

package main

import (
    "embed"

    "github.com/movsb/fbiw"
)

//go:embed main.html regular.ttf
var assets embed.FS

func main() {
    app := fbiw.NewApp(
        fbiw.WithSystemFont(assets, "regular.ttf"),
    )
    defer app.Close()

    doc := app.New(assets, "main.html")
    app.Show(doc)
    app.Run()
}

系统字体是必需资源。如果指定字体加载失败,真正绘制文字时会因为找不到可回退字体而终止。

文档结构

一个界面文档必须包含一个 <document> 根节点:

<document>
    <style>
        /* 文档样式 */
    </style>

    <block>
        <!-- 唯一的内容根节点 -->
    </block>
</document>

规则如下:

  • <document> 下最多有一个 <style>
  • 内容根节点只能有一个,且必须是内置容器或通过 fbiw.Define 注册的组件;
  • 普通容器中不能直接放置非空文本,文字必须放在 <text> 中;
  • <b><i> 只能出现在 <text><b><i> 内;
  • <img><spacer> 是无子节点元素;
  • 解析使用 Go 的 HTML5 parser,自定义标签不要使用 <spacer/> 形式,应写成 <spacer></spacer>

内置组件

标签 用途
block 子元素纵向排列
inline 子元素单行横向排列
flex 子元素单行弹性排列,支持横向或纵向
stack 子元素叠放在同一位置
safe-area 根据系统覆盖层占用的四边区域,为内容设置安全内边距
scroll 裁剪并按像素偏移任意内容的滚动视口
list 固定行列、固定可视槽位的虚拟列表
spacer 在布局主轴上分配剩余空间
button 带默认样式、A 键交互和禁用状态的按钮容器
toggle 不接受子节点,激活后按 A 键切换 checked 状态的开关
check 不接受子节点,激活后按 A 键切换 checked 状态的复选框
progress 不接受子节点,绘制 [0,1] 范围内的确定进度
select 不接受子节点,使用模态列表选择预定义选项
text 文本内容和文本分段
b 粗体文本片段
i 斜体文本片段
img 图片

也可以使用 fbiw.Define 注册实现了 Box 接口的自定义标签。

表格是可选组件;导入聚合包即可注册相关标签:

import _ "github.com/movsb/fbiw/widgets"

需要在 Go 代码中引用类型时,可使用 widgets.Tablewidgets.TableRowwidgets.TableCell;也可以单独导入 github.com/movsb/fbiw/widgets/table

表格默认按实际字体和内容自动测量列宽,使用单线折叠网格;tableborder-widthborder-color 同时控制外框及内部网格,cell 自身的边框会被忽略。 tdth 支持正整数 rowspancolspan,也可用 width 提示首选宽度:

<table border-width="1" border-color="#666">
    <tr>
        <th><text>名称</text></th>
        <th><text>说明</text></th>
    </tr>
    <tr>
        <td rowspan="2"><text>网络</text></td>
        <td><text>无线网络状态</text></td>
    </tr>
    <tr>
        <td><text>当前 IP 地址</text></td>
    </tr>
</table>

结构必须是 table > tr > td|th,cell 内的文字仍须放在 <text> 中。未声明 width 时表格按内容收缩;父空间不足时列宽压缩到最小可排版宽度并换行,仍放不下 时横向溢出,可放入横向 <scroll>。HTML parser 生成的 tbody/thead/tfoot 会被透明 展开,不进入 Box 树。当前不支持 col、caption、分离边框或编辑交互。

button 支持普通、主按钮和危险操作三种样式,以及禁用状态:

<button><text>普通按钮</text></button>
<button variant="primary"><text>主按钮</text></button>
<button variant="destructive"><text>删除</text></button>
<button disabled><text>不可用</text></button>

激活 Button 后按 A 键会触发点击;按住 A 产生的重复事件会被忽略:

button := doc.GetBoxByID[*fbiw.Button]("submit")
remove := button.OnClick(func() {
    submit()
})
defer remove()

button.Activate()
button.SetDisabled(false)

也可以使用 SetVariant 动态切换 fbiw.ButtonNormalfbiw.ButtonPrimaryfbiw.ButtonDestructive。默认样式由框架样式表提供, 文档中的 CSS 可以继续覆盖背景、文字、边框、尺寸和间距。

Alert Dialog 使用 Popup 文档显示在当前文档之上,由框架生成背景遮罩、 标题、可滚动说明和一到两个按钮:

app.ShowAlertDialog(doc, fbiw.AlertDialogOptions{
    Title:         "删除存档?",
    Description:   "此操作无法撤销。",
    ActionText:    "删除",
    ActionVariant: fbiw.ButtonDestructive,
    CancelText:    "取消",
    OnAction: func() {
        deleteSave()
    },
    OnCancel: func() {
        log.Println("已取消")
    },
})

操作规则如下:

  • A 执行确认操作;
  • 有取消按钮时,B 执行取消;单按钮弹窗忽略 B;
  • 上下键逐行滚动较长的 Description;
  • 动作触发时先关闭弹窗,再调用相应回调;
  • ShowAlertDialog 返回的对象可以通过 Close() 无回调地关闭。

ActionText 默认为“确定”,ActionVariant 默认为 fbiw.ButtonPrimaryCancelText 为空时只显示一个按钮。

toggle 只绘制开关本身,文字等内容由外部元素提供:

<inline align="middle">
    <text>Wi-Fi</text>
    <spacer></spacer>
    <toggle id="wifi"></toggle>
</inline>

默认尺寸跟随其计算后的 font-size:宽度为 2.25em,高度为 1.25em。也可以使用 widthheight 显式覆盖。轨道和滑块颜色 可以通过元素属性设置:

<toggle
    track-color="#656b76"
    checked-track-color="#34c759"
    knob-color="white">
</toggle>

在 Go 中激活 Toggle,并使用类型安全的 OnChange 监听状态变化:

toggle := doc.GetBoxByID[*fbiw.Toggle]("wifi")
remove := toggle.OnChange(func(checked bool) {
    log.Println("Wi-Fi:", checked)
})
defer remove()

toggle.Activate()

OnChange 返回解除监听的函数。需要访问事件目标、传播阶段或调用 StopPropagation 时,可以改用底层的 Listenfbiw.ToggleChangeEvent

Toggle 首次显示时直接呈现当前状态。显示过后切换状态,滑块会在 250ms 内以 EaseOut 平滑移动;快速反复切换会取消旧动画,从当前显示位置转向新目标。 checked、对应类名和 OnChange 立即更新,不等待滑块位置与轨道颜色动画结束。 SetProp("checked", ...) 同样更新动效,但仍不派发状态事件。

滑块动画每帧只请求重绘;切换 checked 类名本身仍可能因 CSS 规则触发布局。 动画期间尺寸变化会按新的滑动距离绘制。尚未首次绘制时直接更新位置; 后台和 Detach 沿用 Timeline 的暂停回调、时间继续策略,关闭文档自动取消动画。

check 与 Toggle 使用相同的状态和输入约定,但绘制为方框与勾号:

<inline align="middle">
    <check id="music" checked></check>
    <text>背景音乐</text>
</inline>

默认宽高均为 1.25em,也可显式设置尺寸。box-colorchecked-box-colormark-color 分别设置未选中边框、选中方框和勾号颜色。 Go API 为 CheckedSetCheckedOnChange,事件类型为 fbiw.CheckChangeEvent。切换状态后方框与勾号会立即重绘。

progress 只绘制轨道和完成部分,进度使用 [0,1] 范围内的浮点数:

<progress
    id="download"
    value="0.35"
    track-color="#656b76"
    value-color="#3358d4">
</progress>
progress := doc.GetBoxByID[*fbiw.ProgressBar]("download")
err := progress.SetValue(float64(completed) / float64(total))

ValueSetValue 操作逻辑进度;设置新值后,显示进度会在 250ms 内以 EaseOut 从当前位置追到目标。连续更新会取消旧动画并从当前显示位置继续, 动画帧只请求重绘。首次绘制前直接显示目标值;后台、Detach 和文档关闭行为 与其他 Timeline 动画一致。

不知道完成比例时,可以启用不确定模式:

<progress indeterminate></progress>
progress.SetIndeterminate(true)
// 得到实际进度后:
progress.SetValue(0.35)
progress.SetIndeterminate(false)

不确定模式显示一个往返移动的色块;它会在轨道两端完全移出并短暂停留, 再从同一侧重新进入。期间 ValueSetValue 仍保存 确定进度,切回确定模式后立即显示该值。循环由组件内部续订普通 Animate, 端点停留期间停止请求动画帧;整个循环不会在动画公共接口中引入 repeat。

标题、两端数值和百分比文字由外部元素提供。两端文字可以和 Progress 一起放在 block 中,中间覆盖的百分比可以使用 stack 将文字叠在 Progress 上方。默认尺寸为 8em × 0.5em,也可以通过 CSS 覆盖尺寸、 padding、背景和边框。

select 用于从预定义项目中选择一项。它会显示当前值或 placeholder, 激活后按 A 打开居中的列表;方向键移动高亮,A 提交,B 取消:

<select id="language" placeholder="请选择语言"></select>
<select id="device" disabled></select>
language := doc.GetBoxByID[*fbiw.SelectBox]("language")
language.SetItems([]string{"简体中文", "English", "日本語"})
language.OnChange(func(index int) {
    value, _ := language.Selected()
    log.Println(index, value)
})
language.Activate()

SelectBox 支持 SetIndex(-1) 清空选择,Items() 返回项目副本; 动态调用 SetDisabled(true) 会关闭已打开的列表。方向键的重复事件用于 按住连续导航,而重复的 A、B 事件不会提交或关闭。

它不是完整的 Combobox:不接受字符输入,也不提供文本过滤。需要从固定 选项中使用手柄选择时使用 SelectBox;需要搜索大量选项时,应另行实现 可输入过滤的 Combobox。

文本自动滚动

<text> 可以在内容超出固定尺寸时进行逐像素往返滚动。滚动到另一端后会 停留一段时间,再反向滚回起点并再次停留。

横向滚动使用固定宽度,并关闭由可视宽度引起的自动换行;未显式设置 width 时会采用父布局提供的可视宽度。文本中的显式换行符仍会保留:

<text
    width="300"
    height="40"
    marquee="horizontal"
    marquee-speed="40"
    marquee-pause="800"
    marquee-count="2"
>这是一段会在左右两端停留并往返滚动的横向长文本</text>

纵向滚动使用固定高度,文本仍按可用宽度正常换行:

<text
    width="300"
    height="120"
    marquee="vertical"
    marquee-speed="30"
    marquee-pause="800"
>第一行&#10;第二行&#10;第三行&#10;第四行&#10;第五行</text>

相关属性:

属性 取值 默认值 说明
marquee horizontalvertical 或空值 空值 选择滚动轴向;空值表示关闭
marquee-speed 正数 60 滚动速度,单位为像素/秒
marquee-pause 非负整数 1000 在两端停留的毫秒数;0 表示不停留
marquee-count 非负整数 0 完成“起点→末端→起点”的次数;0 表示不限次数

只有内容在所选轴向上溢出时才会启动动画;内容能够完整显示时不会产生动画帧。 修改文本、关闭 marquee 或关闭文档时,原动画会自动停止。滚动内容会被裁剪在 <text> 的内容区域内,不会覆盖边框或相邻元素。 达到 marquee-count 后,文本停在起点且不再申请动画帧;更新文本或重新启用 marquee 会从零重新计数。

布局模型

block 沿垂直方向依次排列子元素,inline 沿水平方向依次排列子元素。stack 将所有子元素放置在同一个内容区域。

这里的 blockinline 标签描述的是容器对其直接子节点采用的内部布局方式。布局类型由盒子类型决定,display 只控制显示/隐藏,并不等同于浏览器 CSS 的 display 属性:

  • block 约束子节点纵向排列;
  • inline 约束子节点横向排列;
  • 一个节点自身在父容器中使用多大空间,由父容器调用其 Calc 方法时传入的 Constraints 决定;
  • 当前节点自己的 blockinline 类型,只影响它如何排列下一层子节点。

例如,Inline 会横向放置一个 Block,但这个 Block 仍会纵向放置自己的子节点:

<inline>
    <block>
        <text>第一行</text>
        <text>第二行</text>
    </block>
</inline>

Go 布局接口中的尺寸偏好含义如下:

字段 true false
PrefersMaxWidth 当前节点优先使用父节点提供的全部可用宽度 当前节点按内容所需宽度收缩
PrefersMaxHeight 当前节点优先使用父节点提供的全部可用高度 当前节点按内容所需高度收缩
UnboundedWidth 可用宽度只作为百分比和排版参考,自然宽度不被它截断 自然宽度受可用宽度限制
UnboundedHeight 可用高度只作为百分比和排版参考,自然高度不被它截断 自然高度受可用高度限制

Constraints 约束的是正在执行 Calc 的节点自身,而不是它的子节点排列方向。尺寸优先级为 FixedWidth/FixedHeight > 样式 width/height > PrefersMax* 尺寸偏好或内容尺寸。无界轴会忽略 PrefersMax*,Spacer 和 flex-grow 也不会在该轴分配剩余空间;百分比仍相对于 ParentContentWidth/ParentContentHeightFixedWidth/FixedHeight 使用 NumberLength(n) 表示父布局分配的最终 border-box 尺寸,空值表示不强制;不得将分配结果写回样式。自定义 Calc 也应遵守这个约定。

<block height="300">
    <text>顶部</text>
    <spacer></spacer>
    <text>底部</text>
</block>

上面的 Spacer 会占据两个文本之间的剩余高度。在 inline 中使用 Spacer,则会占据剩余宽度:

<inline width="400">
    <text>左侧</text>
    <spacer></spacer>
    <text>右侧</text>
</inline>

align 当前支持:

效果
空值 水平靠左、垂直靠上
center 水平居中
middle 垂直居中
both 水平和垂直居中

block/inline 保持原来的布局规则;inline 不会自动换行,也没有通用 margin、min/max size、绝对定位或通用 overflow 裁剪。

单行 Flex

使用 <flex>(Go 构造函数为 NewFlex(doc))创建单行弹性布局容器,也可作为文档内容根节点。自身如何参与上一层布局仍由父容器决定。布局类型不能通过 display 改变;原来的 display="flex" 写法需要改为 <flex> 标签。

<flex width="300" height="80" gap="12" align-items="center">
    <block width="60" height="40" background-color="#3358d4"></block>
    <block width="0" height="40" flex-grow="1" background-color="#34c759"></block>
    <block width="0" height="40" flex-grow="2" background-color="#f5a623"></block>
</flex>

扣除固定宽度 60 和两个间距 24 后,剩余 216 按 1:2 分配,后两个元素宽度分别为 72、144。

属性 默认值 支持值
flex-direction row rowcolumn
flex-grow 0 非负有限数值,作用于 Flex 的直接子元素
gap 0 非负整数像素,仅在可见子元素之间留间距
justify-content start startendcenterspace-betweenspace-aroundspace-evenly
align-items stretch startendcenterstretch
align-self auto auto 或上述 align-items 值;auto 采用父容器设置

对齐属性同时接受 flex-start / flex-end 别名。这些属性不继承;align-selfflex-grow 由父 Flex 读取。Flex 容器使用新对齐属性,不使用旧的 align 排列子元素。

  • flex-grow 在基础尺寸上增加空间:基础尺寸来自显式宽高、百分比或内容测量。若想按权重分配全部主轴空间,横排设置 width="0",竖排设置 height="0"
  • 本项目按正权重比例分完剩余空间,即使权重之和小于 1;这是轻量布局规则,不是完整 CSS Flexbox 算法。
  • stretch 只拉伸未指定交叉轴尺寸的元素;显式尺寸(包括 0 和百分比)保持不变。
  • 文本作为一个 Flex item,可在分配后的宽度内多行断行;这不代表 Flex items 自身支持换行。
  • 未实现 flex-shrinkflex-basisflex shorthand、flex-wrap、反向排列、order、baseline 和 min/max 尺寸。空间不足时保持基础尺寸并溢出,grow 不会分配负尺寸。
  • <spacer>spacer 属性不会在 Flex 中自动启用增长,需要显式设置 flex-growgap 是 Flex 和 List 共用的非继承样式,分别表示子元素间距和行列槽位间距;Block/Inline 暂不使用它。List 同时支持 <list gap="4">list { gap: 4; },内联属性优先,动态修改会触发重新布局。
  • List 等自带内部布局的专用组件可作为 Flex item,但保持各自的内部布局规则。

系统覆盖层和安全区域

状态栏等系统界面可以作为独立覆盖层,始终绘制在所有普通文档之上。普通文档仍使用完整屏幕,因此背景图片可以延伸至屏幕边缘;需要避免被状态栏遮挡的内容放入 <safe-area>

覆盖层文档使用以下固定 ID 声明四边占用区域:

  • #top#bottom 的布局高度分别作为顶部、底部 inset;
  • #left#right 的布局宽度分别作为左侧、右侧 inset;
  • 缺少某个元素时,对应 inset 为零。

例如 status.html

<document>
<stack fill>
    <block>
        <inline id="top" height="48" background-color="#000000E8">
            <text>状态栏</text>
        </inline>
        <spacer></spacer>
    </block>
</stack>
</document>

创建并设置覆盖层:

overlay := app.NewOverlay(assets, "status.html")
app.SetOverlay(overlay)

普通文档可以把全屏背景和安全内容叠放:

<document>
<stack fill>
    <img src="background.png" fill="stretch">

    <safe-area fill>
        <block fill>
            <text>不会被系统覆盖层遮挡</text>
        </block>
    </safe-area>
</stack>
</document>

<safe-area> 会在布局时自动采用当前四边 inset。覆盖层尺寸改变、被替换或被移除时,显示中的普通文档会重新布局。调用下面任一种方式可以移除覆盖层:

app.SetOverlay(nil)
// 或
overlay.Close()

覆盖层仅负责顶层绘制,不会加入普通文档栈,也不会成为接收按键事件的活动文档。<safe-area> 的 padding 由系统占用区域管理,不应另外设置 padding;需要额外留白时,在其内部再放置带 padding 的容器。

完整示例见 demo/safe

样式

样式可以写在 <style> 中,也可以直接作为元素属性:

<block width="300" padding="12" background-color="#20242a">
    <text color="white" font-size="24">设置</text>
</block>

当前支持的样式属性:

  • align
  • background-color
  • background-image
  • border-color
  • border-width
  • border-radius
  • outline-color
  • outline-width
  • color
  • width
  • height
  • padding
  • font-family
  • font-size
  • bold / font-bold
  • italic / font-italic
  • spacer
  • display
  • fill
  • flex-directionflex-growgap
  • justify-contentalign-itemsalign-self

widthheight 可以解析整数或百分比。常规 Calc 布局每次根据父容器提供的完整内容区解析百分比,不受前序兄弟元素占用影响,也不会改写计算后的样式;根元素以文档尺寸为参考。文本的独立分段路径和内容自适应父容器的百分比规则仍有限制,参见 todo.md

display 是非继承的 bool 样式,默认 true。接受 true / false1 / 0,空属性 <block display> 表示 true。隐藏元素不参与父布局和绘制,隐藏祖先下的子元素也不会显示。noneblockinlineflex 等布局关键字不再接受;隐藏请使用 display="false",选择布局请使用相应的盒子标签。Go 中使用 Styles.SetDisplay(bool)DisplayMode 类型已移除。

padding 接受一至四个 0...65535 范围内的整数,展开顺序与 CSS shorthand 相同:

padding: 10;          /* 10 10 10 10 */
padding: 10 20;       /* 10 20 10 20 */
padding: 10 20 30;    /* 10 20 30 20 */
padding: 10 20 30 40; /* top right bottom left */

font-size 接受整数、百分比、非负 rem 和命名字号 xx-smallx-smallsmallmediumlargex-largexx-large。百分比相对于父元素的计算字号,rem 相对于 <document> 的计算字号:

document { font-size: 32; }
.title { font-size: 1.5rem; } /* 48 */
.hint { font-size: 75%; }    /* 父元素计算字号的 75% */

fill 当前可用值为 stretchcontainscale-downcovernone 尚未支持。

颜色支持预置颜色名,以及 #RGB#RGBA#RRGGBB#RRGGBBAA 十六进制形式。默认文本样式为:

document {
    color: black;
    font-family: system;
    font-size: 25;
}

选择器

当前支持:

block {}                 /* 标签 */
#main {}                 /* ID */
.selected {}             /* class */
block.item {}            /* 简单组合 */
list .selected {}        /* 后代 */
block > inline {}        /* 直接子元素 */
* {}                     /* 通配符 */
block, inline {}         /* 分组 */

选择器可以在规则内嵌套。未使用 & 时,嵌套选择器默认匹配父选择器的后代; & 表示父选择器本身,前导 > 则表示直接子元素:

.card {
    color: white;

    .title {                 /* .card .title */
        font-size: 32;
    }

    &.selected {             /* .card.selected */
        outline-width: 3;
    }

    > .icon {                /* .card > .icon */
        width: 24;
    }
}

支持多层嵌套和逗号分组;父子都是分组选择器时会展开为所有组合。 Nesting 仍只能使用上述选择器子集,不支持伪类、属性选择器、兄弟选择器或媒体规则。

主题颜色

App 默认按时间自动切换主题。同时按 Menu+Select可以按“日间 → 夜间 → 自动”循环切换; 切回自动时立即按当前时间选择主题。

也可通过 API 设置:

err := app.SetThemeMode(fbiw.ThemeModeLight) // 固定日间
err = app.SetThemeMode(fbiw.ThemeModeDark)   // 固定夜间
err = app.SetThemeMode(fbiw.ThemeModeAuto)   // 恢复自动

app.ThemeMode() 返回当前模式。手动模式不会被每分钟的时间检查覆盖, SetThemeLight / SetThemeDark 仍分别配置两个模式使用的主题。

样式表的 colorbackground-colorborder-coloroutline-color 可以引用主题中的语义颜色:

button {
    color: var(--color-on-primary);
    background-color: var(--color-primary);
}

App 内置浅色和深色主题;即使不传任何主题选项,也会随本地时间自动切换。 defaults.css 中的按钮、选择框等控件颜色均来自内置主题。内置主题提供以下常用变量:

--color-text              --color-background
--color-surface           --color-muted
--color-border            --color-focus
--color-selection-background
--color-primary           --color-on-primary
--color-destructive       --color-on-destructive

自定义主题会覆盖对应时段的内置主题,因此只需提供想修改的颜色;未提供的变量继续使用 系统浅色或深色主题中的值。 预置色值分别维护在 internal/assets/light.cssinternal/assets/dark.css 中,使用 :root { --color-name: value; } 格式;这里的 :root 是主题文件专用语法,不会扩展 普通样式表的选择器子集。

应用创建时可以设置初始主题,之后切换主题会自动重新计算所有文档样式并重绘:

app := fbiw.NewApp(
    fbiw.WithTheme("light", themeFS, "light.css"),
    fbiw.WithTheme("dark", themeFS, "dark.css"),
)
if err := app.SetThemeLight("light"); err != nil {
    panic(err)
}
if err := app.SetThemeDark("dark"); err != nil {
    panic(err)
}

可以在 App 层一键覆盖浅色和深色主题的强调色:

if err := app.SetThemeAccent("#7c3aed"); err != nil {
    panic(err)
}

// 恢复主题文件中定义的强调色。
if err := app.ClearThemeAccent(); err != nil {
    panic(err)
}

强调色会覆盖 --color-primary--color-primary-border--color-focus, 并根据对比度自动为 --color-on-primary 选择黑色或白色。该覆盖在昼夜主题切换后仍然有效。 --color-selection-background 会由当前 --color-surface 和强调色混合生成:浅色主题 使用 18% 强调色,深色主题使用 28% 强调色。 主题文件只覆盖 --color-primary 而未覆盖 --color-selection-background 时,也会执行同样的自动混合; 显式定义 --color-selection-background 则以主题文件为准。

组件可以注册自己的主题颜色和浅色、深色默认值:

var meterColor = fbiw.RegisterThemeColor(
    "--meter-color",
    "var(--color-primary)",
    "var(--color-primary)",
)

func (m *Meter) Draw(canvas *fbiw.Canvas) {
    color := m.Document().ResolveThemeColor(meterColor)
    // 使用 color 绘制组件……
}

主题颜色应在 NewApp 之前注册,通常声明为组件包的包级变量或在 init() 中注册。

主题文件可以通过同名变量覆盖组件默认值。toggle 预定义了 --toggle-track-color--toggle-checked-track-color--toggle-knob-colorcheck 预定义了 --check-box-color--check-checked-box-color--check-mark-colorprogress 预定义了 --progress-track-color--progress-value-color。 元素上的 track-color 等显式属性仍具有最高优先级。 注册默认值既可以是固定颜色,也可以引用另一个主题颜色;引用会在主题与强调色完成 合并后解析,并检查不存在的变量和循环引用。

主题由 App 统一管理。本地时间 06:00 至 18:00 使用浅色主题,18:00 至次日 06:00 使用深色主题。App 每分钟检查一次本地时间,系统休眠或时钟变更后也会在恢复运行后约一分钟内校正。 只配置一种时会始终使用该主题。 切换前会检查所有文档引用的颜色;缺失颜色时返回错误并保留旧主题。

var(...) 可用于 <style> 以及元素内联属性中的 colorbackground-colorborder-coloroutline-color

<block background-color="var(--color-selection-background)"></block>

当前不支持 fallback 或非颜色变量。除 --color-* 外,变量需要先通过 RegisterThemeColor 注册。

样式来源的覆盖顺序为:

默认样式 < 文档样式表 < 元素内联属性

同一来源内先比较选择器 specificity;specificity 相同时,源码中靠后的声明优先。

颜色和字体相关属性会从父元素继承。 Document.root 未显式设置背景时会采用 document--color-background;该背景 只应用到根节点,不会继承给后代。Popup 或 Overlay 需要透明根节点时可显式设置 background-color="none"

查询和绑定元素

可以使用 ID 或选择器查询 Box:

box := doc.GetBoxByID("panel")
first := doc.QuerySelector(".item")
all := doc.QuerySelectorAll("list .item")

Bind 可以根据结构体字段上的 css tag 自动绑定元素:

type View struct {
    root  fbiw.Box
    title *fbiw.Text `css:"#title"`
    items []fbiw.Box `css:".item"`
}

var view View
doc.Bind(&view)
view.title.SetText("新的标题")

字段可以是单个 Box、具体组件指针或切片。名为 root 且类型为 fbiw.Box 的字段会绑定文档根元素。

Text.SetRich 可以使用只包含 <b><i> 的富文本模板。{$n} 按从 1 开始的编号引用位置参数;参数会被自动转义,因此即使来自外部输入也不会被解释为标签:

err := view.title.SetRich(
    `你好,<b>{$1}</b>,还有 <i>{$2}</i> 条消息`,
    userName,
    messageCount,
)

参数可以重复或乱序引用,{{ 表示字面的 {。所有传入参数必须至少被引用 一次;模板错误、参数越界或出现其它标签时,方法返回错误且保留原内容。换行请在 模板中使用 \n

Document.Unmarshal 可用于动态创建一段组件树:

type Item struct {
    root fbiw.Box
    text *fbiw.Text `css:"text"`
}

func (item *Item) ListSelectionChanged(selected bool) {
    // 一个列表项包含多个文本时,可以只控制需要滚动的标题。
    item.text.SetMarqueeRunning(selected)
}

item := doc.Unmarshal[Item](`
    <block background-color="tan">
        <text></text>
    </block>
`)
item.text.SetText("项目内容")

事件系统

Box 同时也是事件目标。事件按照捕获、目标和冒泡三个阶段传播:

remove := box.Listen(fbiw.InputDownEvent, func(event *fbiw.Event) {
    if event.Input.Name == sticks.A {
        // 处理 A 键按下
        event.StopPropagation()
    }
})

defer remove()
box.Activate()

捕获阶段监听器:

doc.ListenOptions(fbiw.InputDownEvent, func(event *fbiw.Event) {
    // 从根元素开始捕获事件
}, fbiw.EventOptions{Capture: true})

目前公开的输入事件主要是:

  • InputDownEvent
  • InputUpEvent
  • QuitEvent

按键包括方向键、A/B/X/Y、Menu、Select、Start、Fn1/Fn2、音量、Home 和 L1/R1。同时按住 Menu 与 Start 会退出应用。

滚动容器

Scroll 是通用的像素级滚动视口。它接受一个直接子节点,子节点内部可以使用任意普通布局;视口会在滚动轴上无界测量内容,并将绘制裁剪到自身内容区。

<scroll id="article" height="240" direction="vertical" step="32" smooth>
    <block>
        <text>任意长度的内容</text>
        <img src="cover.png"></img>
    </block>
</scroll>
属性 默认值 说明
direction vertical verticalhorizontalboth
step 32 每次方向键滚动的正整数像素数
smooth false 使用 TransitionEaseOut 平滑过渡到目标偏移

激活后,Scroll 使用与方向匹配的方向键滚动;到达边界或按下未启用方向的按键时不会停止事件传播。代码可以直接控制和查询偏移:

scroll := doc.GetBoxByID[*fbiw.Scroll]("article")
scroll.Activate()
scroll.ScrollTo(0, 120)
scroll.ScrollBy(0, 32)
scroll.ScrollIntoView(doc.GetBoxByID("section-3"))
x, y := scroll.ScrollOffset()
maxX, maxY := scroll.ScrollRange()

ScrollIntoView 使用最小滚动距离让指定后代完整进入视口;目标已经可见、被隐藏或不属于当前 Scroll 时返回 false,大于视口的目标按起点对齐。它与 ScrollToScrollBy 一样遵守 directionsmooth

开启 smooth 后,连续的 ScrollBy 和方向键输入会累计目标偏移,并从当前显示位置平滑转向最新目标;ScrollOffset 返回当前帧实际显示的偏移。每次显示偏移变化都会派发 ScrollChange,事件数据为 ScrollChangeArgs{X, Y};最终目标完成后派发一次 ScrollEnd,事件数据为 ScrollEndArgs{X, Y}。连续重定向不会为中间目标派发 ScrollEnd,非平滑滚动则在位置更新后立即派发。内容或视口尺寸变化后,显示位置和动画目标都会自动限制在新的合法范围。当前不提供滚动条、鼠标/触摸或惯性滚动。

虚拟列表

List 只创建 rows × cols 个可视组件,并在滚动时复用这些组件:

<template id="item">
    <block><text></text></block>
</template>

<list
    id="list"
    rows="2"
    cols="3"
    gap="5"
    padding="10">
</list>
list := doc.GetBoxByID("list").(*fbiw.List)

type Item struct {
    root fbiw.Box
    text *fbiw.Text `css:"text"`
}

list.SetItems(
    100,
    func() (fbiw.Box, *Item) {
        item := doc.Instantiate[Item]("item")
        return item.root, item
    },
    func(item *Item, index int) {
        item.text.SetText(fmt.Sprintf("项目 %d", index))
    },
)

list.Activate()

SetItems 返回的 user 可以选择实现 ListSelectionAwareList 会在 列表项选中、取消选中以及虚拟槽位换绑数据时调用它;没有实现时静默忽略:

type ListSelectionAware interface {
    ListSelectionChanged(selected bool)
}

Text.SetMarqueeRunning(false) 会停止动画并让文本回到起点,再传入 true 时从 起点的停留阶段开始滚动。列表项可在回调中精确选择要控制的 <text>,不需要让 所有后代文本都响应 .selected

每个虚拟列表槽位也会裁剪其子内容;超宽文本、图片或其它子元素不会绘制到相邻 列表项之外。槽位自身的选中轮廓不受该裁剪影响。

被选中的可视槽位会自动获得 .selected class,可以通过样式显示选中状态:

list .selected {
    outline-width: 3;
    outline-color: var(--color-focus);
}

框架默认已从 internal/assets/focus.csslist .selectedbutton.selectedselect.active 提供上述焦点轮廓;应用只在需要不同宽度或交互样式时覆盖它。

List 支持读取和恢复选择状态,但当前所有槽位尺寸相同,不支持可变高度列表。

List 的行数与行高

rowsmax-rows 都表示可视槽位的行数,但语义不同:

  • rows:固定显示区域的行数。即使数据不足,List 也不因数据量而收缩。
  • max-rows:最多显示的行数。数据不足时按实际行数收缩,超过上限后滚动。
  • row-height:每行槽位的固定高度,不包含 gap

rowsmax-rows 互斥,一个 List 只能指定其中一个。两者分别与可选的 row-height 组合,共有以下四种布局方式:

配置 槽位高度 List 高度 数据不足时 数据超出时
rows 从可用高度均分 保持外部提供的高度 保持固定高度,留下空槽位 在固定槽位中滚动
rows + row-height 使用 row-height 仍由外部高度决定 保持固定高度,留下空槽位 在固定槽位中滚动
max-rows 按满额时的可用高度均分 按实际数据行数收缩,满额时等于外部提供的最大高度 收缩 保持最大高度并滚动
max-rows + row-height 使用 row-height 根据实际可见行数计算,最多为 max-rows 收缩 保持最大高度并滚动

其中,数据实际占用的行数为:

dataRows = ceil(count / cols)

1. rows:固定行数,自动均分行高

<list rows="5" height="196" gap="4"></list>

去掉 padding、border 和四个 gap 后,剩余高度平均分给五行。数据只有一项时, 容器仍然保持五行区域的高度;数据超过五行时开始滚动。

2. rows + row-height:固定行数和固定槽位高度

<list rows="5" row-height="36" height="196" gap="4"></list>

每个槽位固定为 36 像素,List 本身仍然使用外部提供的高度。调用者应保证容器 内容区的高度与所有槽位和间距匹配:

contentHeight = rows * rowHeight + (rows - 1) * gap

如果外部高度更大,会留下额外空间;如果更小,槽位可能超出容器。这个组合适用于 弹窗尺寸由外层统一控制、但列表项必须保持固定高度的场景。

3. max-rows:限定最大行数,自动均分行高

<list max-rows="5" height="196" gap="4"></list>

外部提供的高度表示五行满额时的最大高度。槽位高度按五行均分得到;只有两行数据时, List 收缩为两行槽位加一个 gap 的高度。达到或超过五行后保持最大高度并滚动。

4. max-rows + row-height:限定最大行数和固定槽位高度

弹出菜单等项目数量不固定的场景,推荐使用这个组合:

<list max-rows="5" row-height="36" gap="4"></list>

实际高度按可见数据行数计算:

visibleRows = min(dataRows, maxRows)
contentHeight = visibleRows * rowHeight + max(visibleRows - 1, 0) * gap
listHeight = border + padding + contentHeight

例如 max-rows="5" row-height="36" gap="4":一行数据的内容高度是 36,三行是 116,五行及更多数据是 196;第六行开始通过滚动访问。空列表不产生槽位和 gap, 高度只包含 border 和 padding。

图片和字体

相对图片路径从创建文档时传入的文件系统中读取:

<img src="icon.png" width="64" height="64">
<img src="loading.gif" width="64" height="64">
<img src="cover.png" width="160" aspect-ratio="16/9" fill="cover">
<block background-image="panel.png"></block>

GIF 在 <img> 中按文件的帧间隔自动循环播放;单帧 GIF 显示为静态图片。 播放使用 GIF 的完整画布尺寸,避免局部帧导致布局抖动。

<img>aspect-ratio 接受 16/916:9 或小数。只指定宽或高时, 另一边按比例计算;宽高都未指定时,在父布局的有界可用空间内取最大内接尺寸。 宽高都显式指定时忽略该比例。

添加字体:

app.AddFont("system", false, false, fontFS, "regular.ttf")
app.AddFont("system", true, false, fontFS, "bold.ttf")

或者在创建应用时使用:

app := fbiw.NewApp(
    fbiw.WithSystemFont(fontFS, "regular.ttf"),
    fbiw.WithFont("brand", false, false, fontFS, "brand.ttf"),
)

动画帧时钟

Document.RequestAnimationFrame(func(now time.Time)) 请求一次动画帧回调, 返回可重复调用的取消函数。持续动画需要在回调中再次申请:

var cancelFrame func()
var started time.Time
var frame func(time.Time)
frame = func(now time.Time) {
    if started.IsZero() {
        started = now
    }
    elapsed := now.Sub(started)
    // 根据 elapsed 更新组件状态;需要时调用 RequestPaint/RequestLayout。
    if elapsed < time.Second {
        cancelFrame = doc.RequestAnimationFrame(frame)
    }
}
cancelFrame = doc.RequestAnimationFrame(frame)

// 在需要提前停止时调用(UI 主线程):
// cancelFrame()

所有文档共享 App 的 60 FPS 目标时钟,同一帧回调按注册顺序执行,获得相同的 time.Time(保留单调时钟读数),然后统一检查布局和绘制需求。实际帧率取决于 绘制耗时及平台后端;卡顿只跳到当前时间,不补发历史帧。回调本身不会自动重绘。

注册、取消和回调均属于 UI 主线程;其他 goroutine 请通过 Document.Async 投递。 回调内注册的请求最早下一帧执行;取消也能阻止本帧中尚未开始的回调。 nil 回调、未绑定或已关闭文档的请求会 panic。

前台 Desktop 的所有文档与当前挂载的 Overlay 可以执行回调,不做遮挡或元素级 可见性判断。后台桌面、未挂载 Overlay 和 App Detach 期间保留请求但暂停回调, 时间仍继续流逝;恢复后收到当前时间。无可运行请求时,动画时钟不产生周期唤醒 (不改变 macOS 原有事件轮询方式)。关闭文档自动取消其请求,App 退出时清理全部请求。

进度动画与数值补间

Document.Animate 提供经过缓动的 0..1 时间进度,适合在一次回调中更新颜色、 位置等一个或多个值:

cancelAnimation := doc.Animate(fbiw.AnimationOptions{
    Duration: 300 * time.Millisecond,
    Easing:   fbiw.EaseOut,
    OnUpdate: func(progress float64) {
        // 使用 progress 插值所需状态,并按需请求布局或重绘。
        doc.RequestPaint()
    },
})

// cancelAnimation()

Animate 只负责时间和进度,不规定被更新的值类型。NumberAnimatorColorAnimator 根据起止值创建插值函数,因此多个值可以共享一次动画:

move := fbiw.NumberAnimator(oldX, newX)
fade := fbiw.ColorAnimator(oldColor, newColor)

cancelAnimation := doc.Animate(fbiw.AnimationOptions{
    Duration: 300 * time.Millisecond,
    Easing:   fbiw.EaseOut,
    OnUpdate: func(progress float64) {
        x = move(progress)
        color = fade(progress)
        doc.RequestPaint()
    },
    OnComplete: func() {
        log.Println("动画完成")
    },
})

// 需要提前停止时在 UI 主线程调用,可重复调用:
// cancelAnimation()

颜色逐 RGBA 通道在 sRGB 数值空间插值。ColorNoneColorClear 具有特殊 绘制语义,不能作为颜色插值器的端点。两个插值器都会把范围外的进度截到端点, 并拒绝 NaN 进度。

  • 从调用时开始计时,回调不会同步执行;首帧按实际经过时间计算,不保证恰好交付 From。需要立即显示起点时,应先设置组件状态。帧回调内创建的 Animate 使用本帧统一时间戳作为起点。
  • Iterations 为总次数:省略或设为 0 / 1 时单次,正整数指定次数,-1 无限循环。 Duration 是每次时长;每次循环独立应用缓动,中间循环边界进度为 0, 所有循环结束时交付 1 并只调用一次 OnComplete。后台恢复直接追上当前循环, 不补发错过的循环。无限循环的时长必须大于零;有限零时长动画下一帧直接完成。
  • 默认 EaseLinear 为匀速;EaseInEaseOutEaseInOut 使用二次曲线, 不等同于 CSS 同名关键字的三次贝塞尔曲线。
  • OnUpdate 必填,时长不能为负。零时长在下一帧直接交付进度 1。
  • 自然结束时先精确交付进度 1,再调用可选的 OnComplete,且只完成一次。 取消、关闭文档或退出 App 不触发完成回调;在最后一次 OnUpdate 中取消也会阻止它。
  • 后台和 Detach 行为沿用帧时钟:暂停回调,不暂停时间,恢复后追上当前进度或直接完成。
  • Animate 不自动修改样式或标记重绘。动画中途改变目标时,先取消旧动画, 再以当前显示值创建新的插值器,避免跳变。

文档 Timeline

每个使用 Animate 的文档会按需创建一个内部 Timeline,统一管理活动动画:

  • 同一文档的所有动画共享一个帧请求,按文档内的注册顺序推进。
  • 完成或取消的动画会被移除;没有活动动画时停止续订,后续可以复用该 Timeline。
  • 帧回调中新增的动画最早下一帧执行,即使它所属的 Timeline 在本帧还未执行。
  • 关闭文档会清理整个 Timeline;App 退出时也会清理后台文档的活动动画。

时钟只负责帧调度,Timeline 负责集合与续订,Animate 负责时间进度, Animator 负责具体值的插值。直接使用 RequestAnimationFrame 的回调仍是独立请求;与这些动画混用时,Timeline 中的动画按文档成批推进, 不保证它们与独立帧回调交错的注册顺序。

通用 Animate 目前不提供 CSS Transition、倍速或倒放;Toggle 使用一个进度同时完成 滑块位置和轨道颜色动画,其他组件尚未自动添加动效。

图片旋转

Image.SetRotation(degrees) 设置图片绕中心顺时针旋转的角度,并自动请求重绘。 旋转保持布局尺寸,默认裁剪在图片组件范围内,背景和边框不旋转。 在 RotationOptions 中设置 Overflow: true 可允许图片溢出组件范围,仍遵守 父容器和屏幕边界的裁剪;布局和命中区域不变,溢出的内容可能覆盖相邻组件。 每次 Rotate 应用新的溢出设置,停止或结束后保留;默认 false 恢复裁剪。

picture.SetRotation(30)
stop := picture.Rotate(fbiw.RotationOptions{
    Duration: 3 * time.Second, // 每圈时长,必须大于零
    Iterations: 0,            // 无限循环;正整数表示圈数
    Overflow: true,           // 允许溢出组件范围(默认 false)
    Reverse: true,            // 逆时针(默认顺时针)
})
// stop() // 停止并保持当前角度,可重复调用

动画从当前角度开始匀速旋转,默认顺时针,Reverse: true 为逆时针; 新旋转自动替换同一图片的旧旋转。 有限循环结束时回到起始朝向。注册、设置角度和停止均在 UI 主线程执行; 后台暂停回调但不暂停时间,恢复时追上进度,关闭文档时自动清理。 角度必须为有限数值,圈数不能为负,无效参数会 panic。 Image.Rotate 通过公共 Document.Animate 实现循环;自定义组件也可以使用 AnimationOptions.Iterations,无需访问文档内部 Timeline。注意旋转接口的 0 表示无限循环,而通用 Animate 的 0 保留默认单次语义。 运行示例(在 demo/rotation 目录):GOEXPERIMENT=simd go run .

通用值过渡

Document.NewTransition 创建可重复设置目标的泛型过渡对象。调用方管理交互状态, 过渡对象保存显示值、取消旧动画,并通过公共 Document.Animate 平滑更新:

scale := doc.NewTransition(1.0, fbiw.TransitionOptions[float64]{
    Duration: 200 * time.Millisecond,
    Easing: fbiw.EaseOut,
    Animator: fbiw.NumberAnimator,
    OnUpdate: picture.SetScale,
})
scale.SetTarget(1.1) // 选中时
scale.SetTarget(1)   // 取消选中时,从当前显示值恢复

SetTarget 对相同活动目标不重启动画,改变目标时使用新的完整过渡时长。 Value() 返回最近交付的显示值;Cancel() 停在当前值,之后可继续设置目标; SetValue(value) 取消动画并立即更新。构造时不调用更新回调,初始值由调用方呈现。 可选 OnComplete 只在自然结束后调用;零时长在下一帧完成。 所有操作在 UI 主线程执行,后台恢复和文档关闭行为沿用 Animate。 更新回调负责重绘或布局,时长不能为负,Animator 和 OnUpdate 不能为空。 Animator 负责验证端点,无效的新目标不会取消旧动画。

颜色使用相同的对象与现有颜色插值器:

color := doc.NewTransition(fromColor, fbiw.TransitionOptions[fbiw.Color]{
    Duration: 200 * time.Millisecond,
    Animator: fbiw.ColorAnimator,
    OnUpdate: func(value fbiw.Color) { /* 应用颜色并请求重绘 */ },
})
color.SetTarget(toColor)

类型必须可比较。自定义坐标等结构体可以提供自己的插值器:

type Point struct { X, Y float64 }
position := doc.NewTransition(Point{}, fbiw.TransitionOptions[Point]{
    Duration: 200 * time.Millisecond,
    Animator: func(from, to Point) func(float64) Point {
        x, y := fbiw.NumberAnimator(from.X, to.X), fbiw.NumberAnimator(from.Y, to.Y)
        return func(progress float64) Point { return Point{x(progress), y(progress)} }
    },
    OnUpdate: func(value Point) { /* 应用坐标并请求重绘或布局 */ },
})
position.SetTarget(Point{100, 200})

图片的 SetScale 支持中心缩放并可与旋转叠加,不改变布局和命中区域, 遵守旋转配置中的 Overflow。倍数必须是大于零的有限数值。 图片不会自动监听激活或按键事件;旋转 demo 的按键代码使用过渡对象设置目标。

异步更新

UI 修改应在主事件线程执行。从其他 goroutine 更新界面时,可使用:

app.Async(func() {
    text.SetText("加载完成")
})

文档相关任务优先使用 Document.Async。它会绑定提交时的文档生命周期:

go func() {
    result := loadData()
    doc.Async(func() {
        // 文档仍然有效时,才会在 UI 主线程执行。
        render(result)
    })
}()

文档在提交前未绑定、App 已退出,或者回调执行前已关闭或解绑时,回调会被忽略。 判断发生在 UI 回调真正执行前,而不是后台任务完成时。Document.Async 可以从其他 goroutine 调用,nil 回调会 panic。通用且不属于某个文档的任务继续使用 App.Async

仅需重绘时调用 RequestPaint,尺寸或结构变化时调用 RequestLayout。对应的 RequestPaintAsyncRequestLayoutAsync 复用生命周期绑定的投递,文档失效后自动忽略。

平台按键

macOS SDL2 开发窗口使用以下映射:

键盘 fbiw 按键
W / S / A / D 上 / 下 / 左 / 右
K / J A / B
I / U X / Y
R / T / Y Menu / Select / Start
Q / O L1 / R1

Linux 后端直接读取 evdev 按键码。当前设备选择和按键映射针对特定目标硬件编写,移植到其他设备时通常需要调整 internal/ports/port_linux.go

运行示例

仓库包含多个示例,其中安全区域示例可这样运行:

GOEXPERIMENT=simd go run ./demo
GOEXPERIMENT=simd go run ./demo/scroll
GOEXPERIMENT=simd go run ./demo/list
GOEXPERIMENT=simd go run ./demo/table
GOEXPERIMENT=simd go run ./demo/safe
GOEXPERIMENT=simd go run ./demo/theme

使用自己的 GIF 运行播放器示例,展示原始尺寸和缩放后的自动循环播放:

GOEXPERIMENT=simd go run ./demo/gif /path/to/your.gif

示例期望存在 demo/regular.ttf。该字体文件当前未包含在仓库中,运行前需要自行放置一个可用的 OpenType/TrueType 字体,并命名为 regular.ttf

Flex 交互示例(使用 <flex>,不使用 display 切换布局):

cd demo/flex
GOEXPERIMENT=simd go run .

它从 ../regular.ttf 加载字体,展示横向 grow 分配、竖向 1:2 分配、主轴/交叉轴对齐和文本自动折行。左右调整中间元素的 grow(1~5),上下切换主轴对齐,A 切换交叉轴对齐,B 隐藏/显示橙色元素。在 macOS 上对应 A/D、W/S、K、J 键。

测试

运行全部测试:

GOEXPERIMENT=simd go test ./...

测试目前覆盖样式解析、样式覆盖与继承、相对字号、选择器查询、布局计算以及结构体绑定。平台后端、完整事件循环和异步图片加载尚缺少集成测试。

项目结构

app.go        应用生命周期、文档堆叠和事件循环
event.go      输入别名与 UI 事件传播
dom.go        文档解析、DOM、查询、绑定和样式应用
style.go      CSS 子集、颜色和样式值
box.go        Box 组件与布局实现
canvas.go     BGRA 软件渲染和图片缓存
font.go       字体、字形缓存和文本测量
input/                 公开输入类型及手柄、键盘子包
internal/event/        平台后端与 UI 之间的输入消息
internal/ports/        Linux framebuffer/evdev 与 macOS SDL2 后端
demo/         示例程序
testdata/     布局、样式和查询测试数据
todo.md       已知问题与后续计划

已知限制

  • 仅提供 Linux 和 macOS 平台后端;
  • HTML 和 CSS 都是项目自定义的精简子集;
  • Inline 当前是单行布局,不支持自动换行;
  • 没有通用 Flex、Grid、margin、min/max size、绝对定位和裁剪;
  • 百分比尺寸、溢出和负尺寸传播仍有待修复;
  • List 仅支持固定行列和等尺寸槽位;
  • Linux 输入设备当前使用固定枚举位置,尚未按设备能力自动识别;
  • framebuffer 后端采用整帧复制,不是真正的原子双缓冲;
  • 公共 API 仍可能变化。

更完整的问题清单和优先级见 todo.md

About

[WIP] A cross-platform tiny GPU-Accelerated GUI library and framework that employs Web technologies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages