一个面向 ratatui 的 CSS 级联引擎 —— 选择器、优先级、继承、伪状态、数据驱动样式。
输出原生 ratatui Style / Block / Constraint,不做并行渲染。
使用标准 CSS 属性名(color、background-color、font-weight、border、padding、margin、text-align、width …),
支持 serde(服务端驱动的 UI 可通过 JSON 传输样式),实现级联规则:
来源层 × 优先级 × 继承 × 伪状态。
更多示例见下文 示例 一节。
use ratatui_style::{CssStyle, Origin, OwnedNode, Stylesheet};
let mut sheet = Stylesheet::new();
sheet.add(
"Button.primary",
CssStyle::new().color("#fff").background("blue").bold(),
Origin::User,
)?;
let node = OwnedNode::new("Button").with_classes(["primary"]);
let computed = sheet.compute(&node, None);
// 投影到原生 ratatui 类型:
let _style = computed.to_style(); // → ratatui::style::Style
let _block = computed.to_block(); // → ratatui::widgets::Block
let _area = computed.apply_margin(area); // 缩小 Rect
let _layout = computed.constraints(); // → (Constraint, Constraint)
let _align = computed.alignment(); // → Alignment.padding() / .margin() / .border() 既能接受类型化值(零 panic),也能保留 CSS 简写字符串的便利:
use ratatui_style::{BorderStyle, CssStyle};
// 类型化 —— 不会 panic
CssStyle::new().padding(1u16); // 四边各 1
CssStyle::new().padding((0u16, 2u16)); // 上下 0、左右 2
CssStyle::new().padding((1u16, 2u16, 3u16, 4u16)); // 上右下左
CssStyle::new().border(BorderStyle::Rounded);
CssStyle::new().border((BorderStyle::Rounded, "#00d4ff"));
// 字符串简写仍可用(适合编译期已知的字面量,坏值会 panic)
CssStyle::new().padding("0 2").border("rounded #00d4ff");use ratatui_style::Stylesheet;
let sheet = Stylesheet::parse(r#"
:root { --accent: #00d4ff; }
Button.primary {
color: var(--accent);
background: blue;
font-weight: bold;
border: rounded;
padding: 0 2;
}
Button:focus { background: green; }
#save:disabled { color: gray; }
"#)?;级联按五个步骤为每个元素解析样式:
- 收集 所有选择器匹配该节点的规则。
- 排序 按
(来源层, 优先级, 源码顺序)升序。 - 叠加 声明 —— 后出现的规则逐字段覆盖前面的。
- 继承 —— 可继承属性(
color、font-weight、font-style、text-decoration、underline-color、text-align)从父元素的计算样式流入子元素的None字段。 - 解析
var()引用,对照令牌表替换为具体值。
规则按来源分层;同等优先级下,高来源层覆盖低来源层:
| 来源 | 优先级 | 用途 |
|---|---|---|
UserAgent |
最低 | 内置默认值 |
Theme |
应用级主题 | |
User |
用户配置 / CSS 文本 | |
Inline |
最高 | 行内样式 |
(id数, class数+伪类数, 类型数) —— 标准 CSS 优先级,以元组形式比较。
*(通用选择器)为 (0, 0, 0)。
| 属性 | 值类型 | 映射到 |
|---|---|---|
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 |
opacity |
1 / 0.5 / 50% / normal |
Modifier::DIM(< 1 时暗淡,不继承) |
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 简写画出全部四条边。要只画某一条(或几条)边,用单边声明:
.card { border-bottom: rounded #00d4ff; } /* 只画底边 */
.tabs { border-bottom: single; }支持的属性:
| 属性 | 作用的边 |
|---|---|
border-top |
顶 |
border-right |
右 |
border-bottom |
底 |
border-left |
左 |
border-x |
左 + 右 |
border-y |
顶 + 底 |
值格式与 border 一致:<样式> [颜色],如 border-bottom: rounded red。
组合语义:单边声明在级联中以「按位 OR」累加,而非互相覆盖。所以两个原子类可以组合出顶 + 底:
.bt { border-top: single; }
.bb { border-bottom: single; }
/* <div class="bt bb"> → 顶 + 底两条边 */这与 Tailwind 式的 .rounded(设样式)+ .border-slate-700(设颜色)组合在同一元素上拼成一条完整边框是同一套机制。border 简写(全四边)的优先级更高:它声明的是「全部边」,与单边声明组合后仍是全部边(不会收窄)。
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
color、font-weight、font-style、text-decoration、underline-color、text-align
可从父元素的计算样式继承。var(--name) 从 :root 令牌表解析
(也可通过编程方式构建 ThemeTokens,或从 themekit 桥接)。
var() 当前支持颜色与长度(width / height)两类自定义属性:
:root {
--accent: #00d4ff; /* 颜色 */
--sidebar-w: 22; /* 长度:等价于 22 个 cell */
--half: 50%; /* 长度:百分比 */
}
.side { width: var(--sidebar-w); }
.col { width: var(--half); }颜色与长度的字面量语法互不重叠(#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 ratatui_style::{CssStyle, Origin, OwnedNode, Stylesheet};
let mut sheet = Stylesheet::new();
sheet.tokens_mut().insert("accent", "#00d4ff");
sheet.add("Panel", CssStyle::new().color("#cdd6f4").italic(), Origin::Theme)?;
sheet.add("Button", CssStyle::new().background("var(--accent)").bold(), Origin::User)?;
sheet.add("Button:disabled", CssStyle::new().color("gray"), Origin::User)?;
// Panel 解析自身样式
let panel = sheet.compute(&OwnedNode::new("Panel"), None);
// Text 从 Panel 继承 color + italic
let text = sheet.compute(&OwnedNode::new("Text"), Some(&panel));
// 禁用按钮::disabled 规则生效,color=gray
let btn = sheet.compute(
&OwnedNode::new("Button").with_state(ratatui_style::State::disabled()),
Some(&panel),
);真实组件树里要给每个子节点手动传 Some(&parent) 既啰嗦又易错。CascadeContext
是一个级联遍历器:它持有一个 Stylesheet 引用 + 可复用 scratch + 一个 parent
计算样式栈。enter(node) 自动用栈顶(若有)作为 parent 计算节点样式、压栈并返回
owned 副本;leave() 弹栈。这样遍历组件树时完全无需手写 parent 穿线。
use ratatui_style::{CascadeContext, OwnedNode, Stylesheet};
let sheet: Stylesheet = /* … */;
let mut ctx = CascadeContext::new(&sheet);
// Root
let root = ctx.enter(&OwnedNode::new("Root"));
// …渲染 root…
// Panel(Root 的子节点)
let panel = ctx.enter(&OwnedNode::new("Panel"));
// …渲染 panel…
// Text(Panel 的子节点)—— 自动继承 Panel 的 color
let text = ctx.enter(&OwnedNode::new("Text"));
// …渲染 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"
的诊断体验不佳——拼写错误悄悄消失。
为此提供两个改进:
所有由解析产生的 CssError 现在携带一个 1-based 的 Loc { line, column }。
注释剥离被改为位置保持(把注释字符替换为空格、但保留其中的 \n),所以
清洗后的文本与原输入等长,字节偏移可直接映射回原文行:列。
use ratatui_style::Stylesheet;
let css = "Button {\n color: red;\n background: #zzz;\n}\n";
let err = Stylesheet::parse(css).unwrap_err();
let loc = err.loc.unwrap();
assert_eq!(loc.line, 3); // 指向写错的 #zzz 那一行Stylesheet::parse_strict 在 parse 的基础上把两种情况升级为硬错误:
- 未知属性:不在已知属性集里、且不是
--前缀自定义属性的声明。错误类型为CssErrorKind::UnknownProperty,loc 精确指向该属性名。 - 未定义变量:没有 fallback 的
var(--name),且name不在本样式表的令牌表 中。错误类型为CssErrorKind::UndefinedVariable(loc 目前为None,见下文注记)。 带 fallback 的var(--nope, #fff)不会报错。
use ratatui_style::{Stylesheet, CssErrorKind};
// 拼写错误:colr → UnknownProperty,loc 指向第 1 行
let err = Stylesheet::parse_strict("Foo { colr: red; }").unwrap_err();
assert!(matches!(err.kind, CssErrorKind::UnknownProperty(ref p) if p == "colr"));
// 未定义变量 → UndefinedVariable
let err = Stylesheet::parse_strict("Foo { color: var(--nope); }").unwrap_err();
assert!(matches!(err.kind, CssErrorKind::UndefinedVariable(_)));
// 先声明再引用 → 正常
Stylesheet::parse_strict(":root{--x:red;}\nFoo{color:var(--x);}").unwrap();
// 带 fallback → 正常
Stylesheet::parse_strict("Foo { color: var(--nope, #fff); }").unwrap();注记:未定义变量错误目前不带精确
loc(为None)。属性错误的 loc 是精确的。 这是有意取舍:属性名在解析阶段即可定位,而var()出现的位置需要额外的解析期 记录才能回溯,成本较高,故先以不带 loc 的形式报出 kind。
在你的节点类型上实现 StyledNode —— 引擎不依赖任何特定框架:
use ratatui_style::{Classes, StyledNode, State, Position};
impl StyledNode for MyNode {
fn type_name(&self) -> &str { &self.kind }
fn id(&self) -> Option<&str> { self.id.as_deref() }
// classes() 现在返回零分配视图 Classes<'_>,而非 Vec<&str>。
fn classes(&self) -> Classes<'_> {
Classes::from_vec(self.classes.iter().map(String::as_str).collect())
}
fn state(&self) -> State { self.state }
fn position(&self) -> Position { self.position.clone() }
}draw 循环里反复调用 compute 会成为分配热点。用借用的 [NodeRef](构造零分配)
- 复用的 [
ComputeScratch](匹配缓冲跨帧复用)来消除每帧分配:
use ratatui_style::{NodeRef, ComputeScratch};
// 在 draw 之外(或 main 中)持有一个 scratch,跨帧复用容量:
let mut scratch = ComputeScratch::new();
// draw 循环内:NodeRef 全是 &'static str 借用,零 String/Vec 分配。
let node = NodeRef::new("Button").classes(&["primary"]).state(State::focus());
let computed = sheet.compute_with(&node, None, &mut scratch);OwnedNode 仍保留作为便利的拥有型节点(测试、一次性查询);它的 classes() 仍会
产生一次 Vec 分配,但热点路径已迁到 NodeRef。
主题不一定来自编译期 css! 宏。RuntimeStyle 支持两种构造基表(base stylesheet):
- 静态基表(
RuntimeStyle::new(&'static Stylesheet)):css!宏工作流,零成本。 - 拥有型基表(
RuntimeStyle::from_owned(Arc<Stylesheet>)):从磁盘/配置/网络在运行时 解析的主题,无需Box::leak:
use std::sync::Arc;
use ratatui_style::{RuntimeStyle, Stylesheet};
let css = std::fs::read_to_string("theme.css")?;
let style = RuntimeStyle::from_owned(Arc::new(Stylesheet::parse(&css)?));加载后可用 load_override(&path) 一次性叠加用户层 CSS(Origin::User 覆盖 Origin::Theme)。
更实用的是基于 mtime 的轻量热重载 —— 在 app 的 tick 里调用,文件没改就不重解析:
// 在事件循环的 tick / poll 里:
if style.reload_if_changed(path.as_ref())? {
// 主题文件变了 —— 已重新解析合并,下一次 compute() 即生效
}reload_if_changed 只在文件 mtime 变化时返回 true;文件被删除视为"移除 override"
(与 load_override 的 NotFound 语义一致)。文件系统无法提供 mtime 时降级为"每次都重载",
确保不会静默丢失更新。
| Feature | 默认 | 说明 |
|---|---|---|
serde |
✅ | 为所有值类型提供 Serialize/Deserialize —— JSON 属性映射、配置文件、传输格式 |
themekit |
❌ | ThemeTokens::from_themekit —— 将 ratatui-themekit 语义颜色槽桥接为 CSS var() 令牌 |
禁用默认 feature 可获得零依赖的纯样式引擎:
[dependencies]
ratatui-style = { version = "0.1", default-features = false }# Hello, World! —— 最小示例:一条 CSS 规则 → 渲染 "Hello, World!"
cargo run --example 00_hello_world
# 交互式仪表盘 —— 纯 CSS 驱动,单一样式表
cargo run --example 05_dashboard
# 级联演示 —— 继承、var()、优先级、伪状态
cargo run --example 03_cascade
# CSS 文本样式表解析
cargo run --example 02_stylesheet
# 颜色与值解析
cargo run --example 01_values
# css! 宏:编译期嵌入 + 运行时覆盖
cargo run --example 09_runtime_override
# scss! 宏:编译期嵌入 SCSS(需要 scss feature)
cargo run --example 10_scss_embed --features scss
# themekit 桥接(需要 themekit feature)
cargo run --example 11_themekit_bridge --features themekit
# CSS 驱动布局 —— width/height 声明 → ratatui Constraint
cargo run --example 13_sizing
# 服务端驱动 UI —— 通过 serde 加载 JSON 样式
cargo run --example 14_data_driven
# 严格模式 —— 捕获 CSS 属性名拼写错误和未定义变量
cargo run --example 15_strict
# 响应式 @media —— 终端尺寸/色彩能力驱动的断点(P3–P6)
cargo run --example 16_media
# 组合选择器 —— 后代/子/相邻/兄弟,经 CascadeContext 树遍历(P3–P5)
cargo run --example 17_combinators
# 结构伪类 —— :nth-child / first/last/only / nth-of-type(P3/P5/P6)
cargo run --example 18_structural
# @supports —— 色彩能力特性检测(P6)
cargo run --example 19_supports
# 计算缓存 —— 访问序 LRU + 变更自动失效(P4/P5)
cargo run --example 20_cache
# var() 用于盒模型 —— padding/border-style/border-color 走 token(P6)
cargo run --example 21_box_var
# opacity → DIM —— 终端无 alpha,< 1 即暗淡(不继承)
cargo run --example 22_opacity不想从零写 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 ratatui_style_presets::{merge, Preset};
// 默认主题 + widget 默认样式 + Catppuccin 调色板,叠成一张表。
let sheet = merge(&[Preset::Default, Preset::Widgets, Preset::Catppuccin]);
let computed = sheet.compute(&ratatui_style::NodeRef::new("Button").classes(&["primary"]), None);
let _block = computed.to_block();# 预设画廊:侧栏浏览全部预设,`c` 切换「整框随主题重渲染」
cargo run -p ratatui-style-presets --example 02_gallery --all-features跨平台的终端磁盘占用分析与清理工具(WinDirStat 风格目录树 + Kondo 风格构建产物清理)。
其科幻 HUD 风格 TUI —— 目录树、详情面板、进度条、直方图 —— 的样式来自
src/tui/theme.css,在编译期通过 css! 宏加载;改 CSS 变量与选择器即可换肤,
无需改动 Rust 代码。
自动监控并杀死高内存占用 rust-analyzer 进程的 TUI 工具。其交互式终端界面
(--tui 模式)—— 实时内存进度条、进程列表(PID / CPU / 内存)、阈值告警配色 ——
全部由 ratatui-style 的 CSS 样式表驱动。
两者均采用链接时优化等尺寸优化构建(disk-cleaner 用
lto = "fat",ra-killer 用opt-level = "z"+lto+strip+panic = "abort"),ratatui-style在这些 精简 release 二进制中运行良好。
| 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


