Tabs
概述
`Tabs` 标签路由到 `rml_ui::Tabs`,是 **WPF TabControl 风格的标签容器**:header(标签行)+ body(内容面板)一体化切换。对应 WPF `<TabControl><TabItem Header="...">Content</TabItem></TabControl>` 模式。
与 [`TabBar`](./tab-bar.md) 的关键差异:
| 特性 | `Tabs` | `TabBar` | |------|--------|----------| | header 标签行 | ✅ | ✅ | | body 内容面板 | ✅ | ❌ | | `bordered` 边框包裹整体 | ✅ | ❌ | | `on-close` / `on-close-all` / `on-close-others` | ✅ | ❌ | | `on-promote`(双击 promote) | ✅ | ❌ | | 右键菜单(Close/Close All/Close Others) | ✅ | ❌ |
`TabBar` 是 `Tabs` 的 header-only 子集,内部委托 `Tabs` 渲染。当所有子项 body 均为 `None` 时,`Tabs` 自动退化为 header-only 模式。
RML **推荐使用 PascalCase** `<Tabs>`,与 gpui-component `Tabs` 类型名一致。kebab-case `<tabs>` 完全兼容。
标签别名表
| 写法 | 规范化结果 | 推荐度 | 说明 | |------|-----------|--------|------| | `<Tabs>` | `Tabs` | ✅ 推荐 | PascalCase,与 gpui-component 类型名一致 | | `<tabs>` | `Tabs` | 兼容 | 小写,`canonical_tag` 映射到 `Tabs` | | `<Tab>` | `Tab` | ✅ 推荐 | 子项标签,统一底层为 `TabItem` | | `<tab>` | `Tab` | 兼容 | 小写 |
> `<Tab>` 是 `Tabs` 与 `TabBar` 共用的子项标签。codegen 统一生成 `TabItem::new()...`,在 `<Tabs>` 内可带 body 子节点,在 `<TabBar>` 内仅作 header 渲染。
基本用法(body 模式)
`<Tab>` 的子节点作为 body 内容(对应 WPF `TabItem.Content`),仅选中 tab 的 body 被渲染:
<Tabs selected-index={active_tab} on-click={on_tab_select}>
<Tab label="Account">
<div class="tab-body">
<p>Account settings panel</p>
</div>
</Tab>
<Tab label="Profile">
<div class="tab-body">
<p>User profile panel</p>
</div>
</Tab>
<Tab label="Settings">
<div class="tab-body">
<p>System settings panel</p>
</div>
</Tab>
</Tabs>
- `selected-index={active_tab}` 绑定当前选中索引 - `on-click={on_tab_select}` 事件回调,签名 `fn(index: usize, &mut Window, &mut App)` - `<Tab label="...">` 的 `label` 属性对应 WPF `TabItem.Header` - `<Tab>` 的子节点(`<div class="tab-body">`)对应 WPF `TabItem.Content`,选中时惰性渲染
bordered 属性
`bordered` 启用 1px 边框,**包裹 header + body 整体**(而非仅 header):
<Tabs bordered="" selected-index={active_tab} on-click={on_tab_select}>
<Tab label="Tab 1">
<p>Content 1</p>
</Tab>
<Tab label="Tab 2">
<p>Content 2</p>
</Tab>
</Tabs>
运行时渲染结构([tabs.rs:820-829](../../../crates/ui/src/components/tab/tabs.rs)):
v_flex (size_full, border_1, border_color)
├── header(Tabs 横向标签行)
└── div (flex_1, min_h_0)
└── body(选中 tab 的 body 闭包渲染结果)
> `bordered` 是 `Tabs` 专属属性,`TabBar` 不支持。
5 种 variant
与 `TabBar` 一致,通过同名布尔标志切换:
<Tabs underline=""> <!-- 下划线(iOS 风格) -->
<Tabs pill=""> <!-- 药丸(圆角背景) -->
<Tabs flat=""> <!-- 扁平(无边框,背景选中) -->
<Tabs outline=""> <!-- 描边(带边框) -->
<Tabs segmented=""> <!-- 分段(iOS SegmentedControl 风格) -->
默认 variant 为 `Tab`(标准标签栏)。
关闭按钮与事件
`<Tab closable>` 在 header 末尾渲染关闭按钮(`IconName::Close`,`.xsmall()` 尺寸),按钮**仅在鼠标 hover 到该 tab 时显示**。关闭按钮点击触发 `stop_propagation()`,避免误触发 tab 的 `on_click`。
`Tabs` 容器级关闭事件:
| 属性 | 签名 | 说明 | |------|------|------| | `on-close` | `fn(index: usize, &mut Window, &mut App)` | 关闭按钮点击回调,参数为被关闭 tab 的索引 | | `on-close-all` | `fn(&mut Window, &mut App)` | "关闭全部"回调(无索引参数) | | `on-close-others` | `fn(index: usize, &mut Window, &mut App)` | "关闭其他"回调,参数为保留 tab 的索引 |
<Tabs
selected-index={selected_index}
on-click={on_tab_select}
on-close={on_tab_close}
on-close-all={on_tab_close_all}
on-close-others={on_tab_close_others}>
<Tab each={tab in tabs}
label={tab.title}
closable={tab.closable} />
</Tabs>
右键菜单(内置)
当 `on-close` / `on-close-all` / `on-close-others` 任一存在时,`Tabs` 自动为每个 tab 注入右键菜单,包含框架内置三个标准菜单项:
| 菜单项 | i18n key | 触发条件 | |--------|----------|----------| | Close | `rml.tab.close` | `on-close` 存在 | | Close Others | `rml.tab.close_others` | `on-close-others` 存在 | | Close All | `rml.tab.close_all` | `on-close-all` 存在 |
菜单项文本走 i18n(`rml_core::i18n::t_or_default`),可通过 i18n 资源覆盖。
预览模式与 promote
`<Tab preview>` 以 italic 标题渲染(VSCode 预览 tab 风格)。双击 tab 触发 `on-promote` 回调(250ms 时间窗口内两次点击视为双击)。
| 属性 | 签名 | 说明 | |------|------|------| | `on-promote` | `fn(index: usize, &mut Window, &mut App)` | 双击 tab 触发,参数为被双击 tab 的索引 |
<Tabs selected-index={selected_index}
on-click={on_tab_select}
on-promote={on_tab_promote}>
<Tab each={tab in tabs}
label={tab.title}
preview={tab.preview} />
</Tabs>
> **VSCode 预览 tab 模式**:单击打开为 preview(italic 标题),双击 promote 为持久 tab。业务通过 `preview` 标志 + `on-promote` 回调驱动数据层变更。
溢出压缩(自适应紧凑模式)
当 `menu="true"` 且 Tab 数量超出可视宽度时,`Tabs` 自动从**滚动模式**切换为**压缩模式**(类似浏览器 Tab 行为):
| 模式 | 触发条件 | Tab 行为 | 容器行为 | |------|----------|----------|----------| | 滚动模式 | 内容宽度 ≤ 视口宽度 | `flex_shrink_0`(固定宽度)+ 完整 label | `overflow_x_scroll`,可横向滚动 | | 压缩模式 | 内容宽度 > 视口宽度 | `flex_1` + `min_w_0` + label `truncate()`(省略号) | `overflow_x_hidden`,等分容器宽度 |
**实现要点**([tabs.rs:494-657](../../../crates/ui/src/components/tab/tabs.rs)):
1. `Tabs` 在 `on_prepaint` 阶段测量 `content_width` 与 `viewport_width`,结果存入 `Entity<bool>` 溢出标志 2. 溢出时把 `compress=true` 传播给每个 `Tab`,并切换容器为 `overflow_x_hidden` 3. `Tab` 收到 `compress=true` 后:自身 `flex_1().min_w_0()`,label 包裹 `div().min_w_0().truncate()` 实现等分 + 省略号 4. 首帧无 bounds(测量延迟一帧),第二帧拾取后自动 re-render
> **设计意图**:浏览器式"会呼吸"的 Tab 行——少时宽松、多时紧凑、关闭后自动恢复。无需业务侧配置。
Tabs 属性
| 属性 | 类型 | 绑定 | 说明 | |------|------|------|------| | `selected-index` | `usize` | `{expr}` | 当前选中索引 | | `on-click` | 事件 | `="method"` | 点击回调,签名 `fn(index: usize, &mut Window, &mut App)` | | `on-close` | 事件 | `="method"` | 关闭按钮回调,签名 `fn(index: usize, &mut Window, &mut App)` | | `on-close-all` | 事件 | `="method"` | 关闭全部回调,签名 `fn(&mut Window, &mut App)` | | `on-close-others` | 事件 | `="method"` | 关闭其他回调,签名 `fn(index: usize, &mut Window, &mut App)` | | `on-promote` | 事件 | `="method"` | 双击 promote 回调,签名 `fn(index: usize, &mut Window, &mut App)` | | `bordered` | 布尔标志 | — | 1px 边框包裹 header + body 整体 | | `underline` / `pill` / `flat` / `outline` / `segmented` | 布尔标志 | — | 5 种 variant | | `size` | `xsmall` / `small` / `large` | — | 尺寸 | | `menu` | 布尔 | — | 启用下拉菜单 + 溢出压缩(标签过多时) | | `prefix` / `suffix` | 元素 | `{expr}` | 首尾注入元素 | | `last-empty-space` | 元素 | `{expr}` | 尾部占位元素 | | `track-scroll` | `ScrollHandle` 引用 | `{expr}` | 滚动控制 |
Tab 子项属性
| 属性 | 类型 | 绑定 | 说明 | |------|------|------|------| | `label` | 字符串 | `{expr}` | 标签标题(对应 WPF `TabItem.Header`) | | `icon` | `IconName::...` | `{expr}` | 标签图标(与 `label` 互斥,优先级:children > icon > label) | | `disabled` | 布尔 | — | 禁用标签 | | `selected` | 布尔 | — | 选中状态 | | `closable` | 布尔 | — | 是否渲染关闭按钮(hover 时显示,触发 `on-close`) | | `preview` | 布尔 | — | 预览模式(italic 标题,VSCode 风格) | | `on-click` | 事件 | `="method"` | 单个标签点击回调(ClickEvent) | | `<template slot="header">` | 插槽 | — | header 自定义内容(覆盖 label/icon) | | 子节点 | element | — | body 内容(对应 WPF `TabItem.Content`,选中时渲染) |
> **body 与 header 插槽的区别**:直接子节点作为 body 渲染(选中时显示);`<template slot="header">` 内的子节点作为 header 自定义内容(始终显示在标签行内)。
运行时渲染结构
body 模式(有子节点)
v_flex (size_full)
├── [border_1, border_color] ← 仅 bordered=true 时
├── header(Tabs 横向标签行)
│ ├── prefix
│ ├── Tab[0] ... Tab[N]
│ ├── last_empty_space
│ └── suffix
│ └── [menu button] ← 仅 menu=true 且溢出时
└── div (flex_1, min_h_0)
└── body(选中 tab 的 body 闭包渲染结果)
header-only 模式(无子节点)
当所有 `<Tab>` 均无 body 子节点时,`Tabs` 退化为仅渲染 header(与 `TabBar` 等效)。
完整示例
WPF TabControl 模式 + bordered
<Tabs bordered="" selected-index={active_tab} on-click={on_tab_select}>
<Tab label="Account">
<div class="tab-body">
<p>Account settings panel</p>
</div>
</Tab>
<Tab label="Profile">
<div class="tab-body">
<p>User profile panel</p>
</div>
</Tab>
<Tab label="Settings">
<div class="tab-body">
<p>System settings panel</p>
</div>
</Tab>
</Tabs>
VSCode 风格(关闭 + promote + 溢出压缩)
<Tabs
selected-index={selected_index}
on-click={on_tab_select}
on-close={on_tab_close}
on-close-all={on_tab_close_all}
on-close-others={on_tab_close_others}
on-promote={on_tab_promote}
menu="true">
<Tab each={tab in tabs}
label={tab.title}
closable={tab.closable}
preview={tab.preview} />
</Tabs>
代码示例切换(.rml / .rml.rs)
<Tabs selected-index={code_tab} on-click={on_code_tab_change}>
<Tab label=".rml">
<CodeEditor ref="rml_editor" value={rml_sample} language="rml" />
</Tab>
<Tab label=".rml.rs">
<CodeEditor ref="rust_editor" value={rust_sample} language="rust" />
</Tab>
</Tabs>
与 TabBar 的选择指南
| 场景 | 推荐组件 | |------|----------| | 标签栏 + 内容面板一体化(WPF TabControl) | `<Tabs>` | | 需要关闭按钮 + on_close 事件 | `<Tabs>` | | 需要 bordered 边框包裹整体 | `<Tabs>` | | 需要 promote(双击) | `<Tabs>` | | 纯 header 标签切换,无 body | `<TabBar>` | | tab_window 标题栏内的标签栏 | `<TabBar>`(header-only)+ 外部 `<component>` 注入 body |
相关文档
- [tab-bar.md](./tab-bar.md) — 原生 header-only 标签栏 - [window-roots.md](./window-roots.md) — tab_window 根节点与插槽分区 - [slots.md](../slots.md) — `<template slot="...">` 插槽机制 - [composition.md](../composition.md) — 组件组合模式