ratatui-style
一个面向 ratatui 的 CSS 级联引擎 —— 选择器、优先级、继承、伪状态、数据驱动样式。
输出原生 ratatui Style / Block / Constraint,不做并行渲染。
使用标准 CSS 属性名(color、background-color、font-weight、border、padding、margin、text-align、width …),
支持 serde(服务端驱动的 UI 可通过 JSON 传输样式),实现级联规则:
来源层 × 优先级 × 继承 × 伪状态。
截图展示
更多示例见下文 示例 一节。
快速开始
use ;
let mut sheet = new;
sheet.add?;
let node = new.with_classes;
let computed = sheet.compute;
// 投影到原生 ratatui 类型:
let _style = computed.to_style; // → ratatui::style::Style
let _block = computed.to_block; // → ratatui::widgets::Block
let _area = computed.apply_margin; // 缩小 Rect
let _layout = computed.constraints; // → (Constraint, Constraint)
let _align = computed.alignment; // → Alignment
盒模型 builder 的类型化输入
.padding() / .margin() / .border() 既能接受类型化值(零 panic),也能保留 CSS 简写字符串的便利:
use ;
// 类型化 —— 不会 panic
new.padding; // 四边各 1
new.padding; // 上下 0、左右 2
new.padding; // 上右下左
new.border;
new.border;
// 字符串简写仍可用(适合编译期已知的字面量,坏值会 panic)
new.padding.border;
CSS 文本样式表
use Stylesheet;
let sheet = parse?;
级联模型
级联按五个步骤为每个元素解析样式:
- 收集 所有选择器匹配该节点的规则。
- 排序 按
(来源层, 优先级, 源码顺序)升序。 - 叠加 声明 —— 后出现的规则逐字段覆盖前面的。
- 继承 —— 可继承属性(
color、font-weight、font-style、text-decoration、underline-color、text-align)从父元素的计算样式流入子元素的None字段。 - 解析
var()引用,对照令牌表替换为具体值。
来源层
规则按来源分层;同等优先级下,高来源层覆盖低来源层:
| 来源 | 优先级 | 用途 |
|---|---|---|
UserAgent |
最低 | 内置默认值 |
Theme |
应用级主题 | |
User |
用户配置 / CSS 文本 | |
Inline |
最高 | 行内样式 |
优先级(Specificity)
(id数, class数+伪类数, 类型数) —— 标准 CSS 优先级,以元组形式比较。
*(通用选择器)为 (0, 0, 0)。
支持的 CSS 属性
| 属性 | 值类型 | 映射到 |
|---|---|---|
color |
颜色 | Style::fg |
background / background-color |
颜色 | Style::bg / Block::style |
font-weight |
bold / normal / 100–900 |
Modifier::BOLD(见下方说明) |
font-style |
italic / normal |
Modifier::ITALIC |
text-decoration |
underline / line-through / 两者组合 |
Modifier::UNDERLINED / CROSSED_OUT |
underline-color |
颜色 | Style::underline_color |
border |
none / single / rounded / double / thick [颜色] |
Block::borders + border_type |
border-top / border-right / border-bottom / border-left / border-x / border-y |
同 border 简写(<样式> [颜色]),只作用于指定边 |
Block::borders(按边组合) |
padding |
1 / 1 2 / 1 2 3 / 1 2 3 4 |
Block::padding |
margin |
同 padding 简写 | Rect 缩小 |
text-align |
left / center / right |
Alignment |
width / height |
auto / 10 / 50% / min(3) / max(5) |
Constraint |
边框:单边与组合
border 简写画出全部四条边。要只画某一条(或几条)边,用单边声明:
} /* 只画底边 */
}
支持的属性:
| 属性 | 作用的边 |
|---|---|
border-top |
顶 |
border-right |
右 |
border-bottom |
底 |
border-left |
左 |
border-x |
左 + 右 |
border-y |
顶 + 底 |
值格式与 border 一致:<样式> [颜色],如 border-bottom: rounded red。
组合语义:单边声明在级联中以「按位 OR」累加,而非互相覆盖。所以两个原子类可以组合出顶 + 底:
}
}
/* <div class="bt bb"> → 顶 + 底两条边 */
这与 Tailwind 式的 .rounded(设样式)+ .border-slate-700(设颜色)组合在同一元素上拼成一条完整边框是同一套机制。border 简写(全四边)的优先级更高:它声明的是「全部边」,与单边声明组合后仍是全部边(不会收窄)。
font-weight 的限制
ratatui(以及终端本身)只支持单一的加粗修饰位(Modifier::BOLD),没有真正的字重(100–900)细分。因此:
font-weight: bold/bolder/700–900→ 加粗。font-weight: normal/lighter/100–500→ 不加粗。500与normal等价,600与900等价。
这是终端字体能力的固有限制,不是解析器缺陷。
颜色语法
所有颜色属性支持:
| 语法 | 示例 |
|---|---|
| 十六进制 3/4/6/8 位 | #fff #fff0 #ff8800 #ff8800ff |
rgb() / rgba() |
rgb(255, 128, 0) rgba(0,0,0,0.5) |
| CSS 命名颜色 | red blue cyan orange gold … |
transparent / none / reset |
重置为终端默认 |
inherit |
强制从父元素继承 |
var(--name) |
CSS 自定义属性,可带回退值:var(--accent, #fff) |
选择器与伪类
复合选择器格式 Type.class#id:pseudo…,支持逗号列表和 * 通配:
Button /* 类型 */
.primary /* 类 */
#save /* id */
Button.primary:focus /* 复合 */
Text, .muted, #title /* 逗号列表 */
* /* 通配 */
伪类::focus :hover :disabled :checked :active
继承与 var()
color、font-weight、font-style、text-decoration、underline-color、text-align
可从父元素的计算样式继承。var(--name) 从 :root 令牌表解析
(也可通过编程方式构建 ThemeTokens,或从 themekit 桥接)。
var() 当前支持颜色与长度(width / height)两类自定义属性:
}
}
}
颜色与长度的字面量语法互不重叠(#fff/rgb()/命名色 vs 10/50%/auto/min(n)),
因此 :root 会自动判定每个 --name 的类型。var() 引用可链式(--w: var(--w2)),
类型由链的终点决定;未定义或类型不符的 var() 在宽松解析时降级(颜色 → Reset,长度 → Auto),
在严格模式(Stylesheet::parse_strict)下报 UndefinedVariable。
暂不支持:
padding/margin/border的var()—— 这些属性的BoxEdges/BorderSpec表示尚无Var变体,改动较大,留待后续。暂不支持:
Block标题样式映射(title-color/title-align)。ratatui 的Block::title_style/title_alignment已就绪,但本 crate 目前无法设置标题文本本身, 仅映射标题样式意义有限,留待后续与标题内容一起支持。
use ;
let mut sheet = new;
sheet.tokens_mut.insert;
sheet.add?;
sheet.add?;
sheet.add?;
// Panel 解析自身样式
let panel = sheet.compute;
// Text 从 Panel 继承 color + italic
let text = sheet.compute;
// 禁用按钮::disabled 规则生效,color=gray
let btn = sheet.compute;
遍历组件树:CascadeContext
真实组件树里要给每个子节点手动传 Some(&parent) 既啰嗦又易错。CascadeContext
是一个级联遍历器:它持有一个 Stylesheet 引用 + 可复用 scratch + 一个 parent
计算样式栈。enter(node) 自动用栈顶(若有)作为 parent 计算节点样式、压栈并返回
owned 副本;leave() 弹栈。这样遍历组件树时完全无需手写 parent 穿线。
use ;
let sheet: Stylesheet = /* … */;
let mut ctx = new;
// Root
let root = ctx.enter;
// …渲染 root…
// Panel(Root 的子节点)
let panel = ctx.enter;
// …渲染 panel…
// Text(Panel 的子节点)—— 自动继承 Panel 的 color
let text = ctx.enter;
// …渲染 text…
ctx.leave; // 回到 Panel 上下文
ctx.leave; // 回到 Root 上下文
ctx.leave; // 完成
enter返回 owned 副本,调用者无需操心借用顺序(避免返回&self借用 导致无法嵌套enter的问题)。压栈时的一次clone也极廉价:解析后的ComputedStyle只含Literal/Reset字段(var()已解析),无String/Box/Vec堆字段,是纯栈上 memcpy。
严格模式与带位置的错误
默认的 Stylesheet::parse 是宽松的:未知属性被静默忽略(向前兼容),
未定义的 var() 在级联时降级为 Reset。这对生产渲染很稳健,但对"手写 CSS"
的诊断体验不佳——拼写错误悄悄消失。
为此提供两个改进:
1. 解析错误带行:列
所有由解析产生的 CssError 现在携带一个 1-based 的 Loc { line, column }。
注释剥离被改为位置保持(把注释字符替换为空格、但保留其中的 \n),所以
清洗后的文本与原输入等长,字节偏移可直接映射回原文行:列。
use Stylesheet;
let css = "Button {\n color: red;\n background: #zzz;\n}\n";
let err = parse.unwrap_err;
let loc = err.loc.unwrap;
assert_eq!; // 指向写错的 #zzz 那一行
2. parse_strict 严格解析
Stylesheet::parse_strict 在 parse 的基础上把两种情况升级为硬错误:
- 未知属性:不在已知属性集里、且不是
--前缀自定义属性的声明。错误类型为CssErrorKind::UnknownProperty,loc 精确指向该属性名。 - 未定义变量:没有 fallback 的
var(--name),且name不在本样式表的令牌表 中。错误类型为CssErrorKind::UndefinedVariable(loc 目前为None,见下文注记)。 带 fallback 的var(--nope, #fff)不会报错。
use ;
// 拼写错误:colr → UnknownProperty,loc 指向第 1 行
let err = parse_strict.unwrap_err;
assert!;
// 未定义变量 → UndefinedVariable
let err = parse_strict.unwrap_err;
assert!;
// 先声明再引用 → 正常
parse_strict.unwrap;
// 带 fallback → 正常
parse_strict.unwrap;
注记:未定义变量错误目前不带精确
loc(为None)。属性错误的 loc 是精确的。 这是有意取舍:属性名在解析阶段即可定位,而var()出现的位置需要额外的解析期 记录才能回溯,成本较高,故先以不带 loc 的形式报出 kind。
框架集成
在你的节点类型上实现 StyledNode —— 引擎不依赖任何特定框架:
use ;
每帧零分配的热路径
draw 循环里反复调用 compute 会成为分配热点。用借用的 [NodeRef](构造零分配)
- 复用的 [
ComputeScratch](匹配缓冲跨帧复用)来消除每帧分配:
use ;
// 在 draw 之外(或 main 中)持有一个 scratch,跨帧复用容量:
let mut scratch = new;
// draw 循环内:NodeRef 全是 &'static str 借用,零 String/Vec 分配。
let node = new.classes.state;
let computed = sheet.compute_with;
OwnedNode 仍保留作为便利的拥有型节点(测试、一次性查询);它的 classes() 仍会
产生一次 Vec 分配,但热点路径已迁到 NodeRef。
运行时主题与文件热重载
主题不一定来自编译期 css! 宏。RuntimeStyle 支持两种构造基表(base stylesheet):
- 静态基表(
RuntimeStyle::new(&'static Stylesheet)):css!宏工作流,零成本。 - 拥有型基表(
RuntimeStyle::from_owned(Arc<Stylesheet>)):从磁盘/配置/网络在运行时 解析的主题,无需Box::leak:
use Arc;
use ;
let css = read_to_string?;
let style = from_owned;
加载后可用 load_override(&path) 一次性叠加用户层 CSS(Origin::User 覆盖 Origin::Theme)。
更实用的是基于 mtime 的轻量热重载 —— 在 app 的 tick 里调用,文件没改就不重解析:
// 在事件循环的 tick / poll 里:
if style.reload_if_changed?
reload_if_changed 只在文件 mtime 变化时返回 true;文件被删除视为"移除 override"
(与 load_override 的 NotFound 语义一致)。文件系统无法提供 mtime 时降级为"每次都重载",
确保不会静默丢失更新。
Feature 标志
| Feature | 默认 | 说明 |
|---|---|---|
serde |
✅ | 为所有值类型提供 Serialize/Deserialize —— JSON 属性映射、配置文件、传输格式 |
themekit |
❌ | ThemeTokens::from_themekit —— 将 ratatui-themekit 语义颜色槽桥接为 CSS var() 令牌 |
禁用默认 feature 可获得零依赖的纯样式引擎:
[]
= { = "0.1", = false }
示例
# Hello, World! —— 最小示例:一条 CSS 规则 → 渲染 "Hello, World!"
# 交互式仪表盘 —— 纯 CSS 驱动,单一样式表
# 级联演示 —— 继承、var()、优先级、伪状态
# CSS 文本样式表解析
# 颜色与值解析
# css! 宏:编译期嵌入 + 运行时覆盖
# scss! 宏:编译期嵌入 SCSS(需要 scss feature)
# themekit 桥接(需要 themekit feature)
# CSS 驱动布局 —— width/height 声明 → ratatui Constraint
# 服务端驱动 UI —— 通过 serde 加载 JSON 样式
# 严格模式 —— 捕获 CSS 属性名拼写错误和未定义变量
预设库:ratatui-style-presets
不想从零写 CSS?配套 crate ratatui-style-presets 提供开箱即用的主题与样式,按 feature flag 按需启用:
| 预设 | 说明 |
|---|---|
default(始终可用) |
neutral 默认主题 + 基础组件类(Button/Panel/Text/List/Badge …) |
tailwind |
Tailwind 式原子工具类(.bg-*/.text-*/.p-*/.rounded …) |
widgets |
ratatui widget 类型默认样式(Table/List/Tabs/Gauge/Scrollbar …) |
catppuccin / nord / dracula |
官方调色板,填同一套语义 token |
所有主题填同一套规范语义 token(--bg/--text/--accent/--success/…),换基表即换肤:
use ;
// 默认主题 + widget 默认样式 + Catppuccin 调色板,叠成一张表。
let sheet = merge;
let computed = sheet.compute;
let _block = computed.to_block;
# 预设画廊:侧栏浏览全部预设,`c` 切换「整框随主题重渲染」
生态定位
| Crate | 定位 | ratatui-style |
|---|---|---|
ratatui-themekit |
15 个语义颜色槽 + 调色板 | 组合使用 —— ThemeTokens::from_themekit 填充 CSS 变量 |
tui-theme-builder |
编译期 Style 宏 |
ratatui-style 覆盖 运行时/配置驱动 场景 |
lipgloss |
"终端 CSS"(自有渲染栈) | 同类 DX,基于 ratatui 的 buffer 模型 |
当前状态
已实现:CSS 文本解析器、复合选择器、优先级、级联层(UserAgent < Theme < User < Inline)、
伪状态、var()(含回退值)、继承、盒模型(padding / margin / border)、
尺寸(width / height → Constraint)、serde 集成、themekit 桥接。
计划中:后代/子代组合器(A B、A > B)、:nth-child、@media、ComputedStyle 缓存。
许可证
MIT