组件
组件层位于 components/。它不是另一套渲染系统,也不直接持有 OpenGL primitive;组件只负责把常用 DSL 结构封装成更短、更稳定的 builder 调用。
页面仍然由 core::dsl::Ui 里的 row、column、stack、rect、text、image、polygon 等图元组成。组件内部也只声明 DSL 树,事件、hover、pressed、transition、dirty rect 和 framebuffer cache 都继续交给 core::dsl::Runtime。
引入
通常只需要包含聚合头:
#include "components/components.h"
这个头当前导出:
| 组件 | 用途 |
|---|---|
button | 按钮,支持文字、图标、主题、禁用、右键回调 |
checkbox | 受控复选框 |
radio | 受控单选项 |
toggleSwitch | 受控开关 |
progress | 进度条,value 范围是 0.0f - 1.0f |
slider | 滑块,value 范围是 0.0f - 1.0f |
input | 文本输入框,支持单行/多行、选择、剪贴板快捷键 |
segmented | 分段选择 |
tabs | 标签页切换 |
scroll | 滚动条/offset 控制器 |
dropdown | 下拉选择 |
datepicker | dialog 式日期选择器,受控 open/date |
timepicker | dialog 式时间选择器,受控 open/time |
colorpicker | dialog 式颜色选择器,受控 open/value |
dataTable / datatable | 简单数据表格 |
dialog | 模态对话框 |
toast | 右上角 toast,支持自动消失 |
contextMenu | 右键菜单 |
linechart / lineChart | 折线图 |
barchart / barChart | 柱状图 |
piechart / pieChart | 饼图 |
panel | 套用主题的 RectBuilder |
text / label | 套用主题文本色的 TextBuilder |
image | 套用主题图片样式的 ImageBuilder |
theme | 颜色和视觉 token 辅助函数 |
基本约定
- 组件 id 必须稳定,内部子节点使用
id + ".name"。 - 组件 API 使用无下划线命名,例如
datepicker、timepicker、colorpicker。 - 大多数组件是受控组件:页面传入当前值,组件通过回调返回 next value。
- 组件不保存业务状态,不提供生命周期钩子。
- 需要动画时优先使用组件的
.transition(...),或者组件内部固定的 DSL transition。 - 弹层类组件一般提供
.zIndex(...)/.z(...),页面需要保证它们排在普通内容之上。 .style(...)用于完全覆盖样式结构,.theme(tokens)用于按当前主题重建样式。- 组件内部不应该
newprimitive、不绕过 Runtime、不自己读取原始鼠标状态。
Theme
主题定义在 components/theme.h,常用入口:
const auto dark = components::theme::DarkThemeColors();
const auto light = components::theme::LightThemeColors();
const auto page = components::theme::pageVisuals(dark);
const auto field = components::theme::fieldVisuals(dark);
当前提供:
| API | 说明 |
|---|---|
color(r, g, b, a) | 创建 core::Color |
withAlpha(color, alpha) | 替换 alpha |
withOpacity(color, opacity) | 按比例调整 alpha |
light() / dark() | 基础明暗主题 |
LightThemeColors() / DarkThemeColors() | 兼容参考项目命名 |
pageVisuals(tokens) | 页面标题、正文、卡片、间距等 token |
fieldVisuals(tokens) | 输入控件圆角、padding、focus、弹层阴影等 token |
CurrentPageVisuals(tokens) | pageVisuals 别名 |
CurrentFieldVisuals(tokens) | fieldVisuals 别名 |
resolveFieldFill(...) | 按 hover / active 计算字段背景 |
ResolveFieldFill(...) | resolveFieldFill 别名 |
buttonHover(...) | 按主题计算按钮 hover 色 |
buttonPressed(...) | 按主题计算按钮 pressed 色 |
border(...) | 常用边框 |
buttonBorder(...) | 按钮边框 |
shadow(...) | 通用阴影 |
buttonShadow(...) | 按钮阴影 |
panelShadow(...) | 面板阴影 |
popupShadow(...) | 弹层阴影 |
基础包装
panel、text、label、image 返回的仍然是 core DSL builder,所以可以继续链式调用底层能力。
components::panel(ui, "card", tokens)
.size(360.0f, 220.0f)
.transition(0.18f)
.build();
components::text(ui, "title", tokens)
.size(420.0f, 48.0f)
.text("EUI Gallery")
.fontSize(38.0f)
.horizontalAlign(core::HorizontalAlign::Center)
.verticalAlign(core::VerticalAlign::Center)
.build();
components::image(ui, "cover", tokens)
.size(320.0f, 180.0f)
.source("assets/icon.png")
.cover()
.build();
text.h 还提供 titleTextStyle(tokens)、subtitleTextStyle(tokens)、bodyTextStyle(tokens),用于快速生成 core::TextStyle。
控件
button
components::button(ui, "save")
.size(180.0f, 54.0f)
.theme(tokens)
.icon(0xF0C7)
.text("Save")
.transition(0.18f)
.onClick([] {
// update page state
})
.build();
常用 API:size、scale、text、icon、fontSize、iconSize、textColor、iconColor、style、theme、primaryTheme、secondaryTheme、radius、rounding、opacity、disabled、enabled、translate、translateX、translateY、pressScale、border、shadow、colors、transition、onClick、onContextMenu。
按钮内部是 Stack + Rect + Row + Text。按压缩放作用在外层 stack,背景、图标和文字会一起动。
checkbox / radio / toggleSwitch
这些都是受控组件,页面传当前值,回调里写回状态。
components::checkbox(ui, "remember")
.theme(tokens)
.checked(remember)
.text("Remember me")
.onChange([&](bool value) {
remember = value;
})
.build();
components::radio(ui, "mode.a")
.selected(mode == 0)
.text("Mode A")
.onSelect([&] {
mode = 0;
})
.build();
components::toggleSwitch(ui, "motion")
.checked(enableMotion)
.label("Motion")
.onChange([&](bool value) {
enableMotion = value;
})
.build();
checkbox 支持 size、checked、text、fontSize、boxSize、style、theme、transition、onChange。
radio 支持 size、selected / checked、text、fontSize、dotSize、style、theme、transition、onSelect、onChange。
toggleSwitch 支持 size、checked、label / text、fontSize、trackSize、style、theme、transition、onChange。
progress / slider
components::progress(ui, "loading")
.theme(tokens)
.size(320.0f, 14.0f)
.value(progress)
.build();
components::slider(ui, "volume")
.theme(tokens)
.size(320.0f, 34.0f)
.value(volume)
.onChange([&](float value) {
volume = value;
})
.build();
progress 支持 size、value、style、theme、transition。
slider 支持 size、value、style、theme、transition、onChange。value 会 clamp 到 0.0f - 1.0f。滑块用透明 hit rect 处理点击和拖拽,拖拽时根据缓存 bounds 计算 next value。
input
components::input(ui, "name")
.theme(tokens)
.size(280.0f, 42.0f)
.value(name)
.placeholder("Name")
.onChange([&](const std::string& value) {
name = value;
})
.onEnter([] {
// submit
})
.build();
支持 size、text / value、placeholder、multiline、fontSize、inset、style、theme、transition、onChange、onEnter、onFocus。
当前 input 支持 focus、点击定位光标、拖动选择、Shift+方向键选择、Ctrl+A/C/X/V、Backspace、Delete、Enter、单行和多行模式。暂时没有 IME 组合态显示和撤销栈。
segmented / tabs
components::segmented(ui, "density")
.items({"Compact", "Normal", "Comfort"})
.selected(density)
.onChange([&](int index) {
density = index;
})
.build();
components::tabs(ui, "main.tabs")
.items({"Style", "Bing", "Settings"})
.selected(tab)
.onChange([&](int index) {
tab = index;
})
.build();
两者都支持 size、items、selected、fontSize、style、theme、transition、onChange。
scroll
scroll 只是滚动条和 offset 控制器,不直接持有内容。内容裁剪由外层 .clip() 完成,内容位移由页面按 offset 写到子树。
ui.stack("list.viewport")
.size(360.0f, 260.0f)
.clip()
.content([&] {
ui.column("list.content")
.y(-scrollY)
.size(360.0f, contentHeight)
.build();
})
.build();
components::scroll(ui, "list.scroll")
.size(8.0f, 260.0f)
.viewport(260.0f)
.content(contentHeight)
.offset(scrollY)
.onChange([&](float value) {
scrollY = value;
})
.build();
支持 x、y、position、size、offset / value、viewport / viewportHeight、content / contentHeight、step、zIndex / z、style、theme、transition、onChange。
弹层和反馈
dialog
components::dialog(ui, "confirm")
.open(showDialog)
.screen(screen.width, screen.height)
.title("Confirm")
.message("Apply these changes?")
.primaryText("Apply")
.secondaryText("Cancel")
.onPrimary([&] {
showDialog = false;
})
.onSecondary([&] {
showDialog = false;
})
.onClose([&] {
showDialog = false;
})
.build();
支持 open、screen、size、title、message、primaryText、secondaryText、style、theme、transition、zIndex / z、onPrimary、onSecondary、onClose。
dialog 会绘制 backdrop,并用透明 hit rect 吃掉后面内容的 hover、click 和 scroll。打开/关闭动画作用在整体面板上,内容跟着外层一起动。
toast
components::toast(ui, "saved.toast")
.visible(showToast)
.screen(screen.width, screen.height)
.title("Saved")
.message("Your changes were saved.")
.icon(0xF00C)
.duration(2.4f)
.onAutoDismiss([&] {
showToast = false;
})
.onDismiss([&] {
showToast = false;
})
.build();
支持 visible、screen、size、title、message、icon、style、theme、transition、zIndex / z、duration / autoDismiss、onAutoDismiss、onDismiss。
duration(0.0f) 表示不自动消失。关闭按钮是内部命中区,点击触发 onDismiss。
contextMenu
components::contextMenu(ui, "item.menu")
.open(menuOpen)
.screen(screen.width, screen.height)
.position(menuX, menuY)
.items({"Rename", "Duplicate", "Delete"})
.onSelect([&](int index) {
menuOpen = false;
selectedAction = index;
})
.onDismiss([&] {
menuOpen = false;
})
.build();
支持 open、screen、position、size(width, itemHeight)、items、style、theme、transition、zIndex / z、onSelect、onDismiss。
右键菜单会根据 screen 把弹层限制在屏幕内,并绘制全屏透明 dismiss 层。菜单项 hover 使用即时状态,快速 hover 不会等待颜色动画拖尾。
dropdown
components::dropdown(ui, "theme.dropdown")
.items({"Dark", "Light", "System"})
.selected(themeIndex)
.open(dropdownOpen)
.placeholder("Theme")
.onOpenChange([&](bool value) {
dropdownOpen = value;
})
.onChange([&](int index) {
themeIndex = index;
})
.build();
支持 size、items、selected、placeholder、open、itemHeight、style、theme、transition、zIndex / z、onChange、onOpenChange。
下拉面板会随外层 stack 展开,选项 hover 和右键菜单一致,关闭时选项会禁用命中,避免点到隐藏面板。
datepicker / timepicker / colorpicker
这三个 picker 都是受控组件,命名不带下划线。页面传入当前值和 open,组件通过 onChange / onOpenChange 回写 next state。
picker 现在和 dialog 一样绘制全屏 overlay + 居中面板,适合放在页面根级 compose 末尾,避免被滚动容器或 .clip() 裁剪。触发入口可以用 button 或自定义字段单独绘制。
面板打开后会维护组件内部 draft:日期/时间轮盘、滚轮、颜色 slider、预设色块都只更新面板内预览,不会立即改变页面状态。点击 Done 才触发 onChange 提交 draft;点击遮罩只关闭,不提交。
components::datepicker(ui, "profile.date")
.theme(tokens)
.screen(screen.width, screen.height)
.size(420.0f, 270.0f)
.date(year, month, day)
.open(dateOpen)
.onOpenChange([&](bool open) {
dateOpen = open;
})
.onChange([&](int nextYear, int nextMonth, int nextDay) {
year = nextYear;
month = nextMonth;
day = nextDay;
})
.build();
components::timepicker(ui, "profile.time")
.theme(tokens)
.screen(screen.width, screen.height)
.size(330.0f, 264.0f)
.time(hour, minute)
.minuteStep(5)
.open(timeOpen)
.onOpenChange([&](bool open) {
timeOpen = open;
})
.onChange([&](int nextHour, int nextMinute) {
hour = nextHour;
minute = nextMinute;
})
.build();
components::colorpicker(ui, "profile.color")
.theme(tokens)
.screen(screen.width, screen.height)
.size(420.0f, 320.0f)
.value(accent)
.open(colorOpen)
.onOpenChange([&](bool open) {
colorOpen = open;
})
.onChange([&](core::Color value) {
accent = value;
})
.build();
datepicker 支持 open、screen、size(panelWidth, panelHeight)、date(year, month, day)、style、theme、transition、zIndex / z、onChange、onOpenChange。年份范围是 1900 - 2200,月份和日期会按闰年和当月天数修正,onChange 只在 Done 时触发。
timepicker 支持 open、screen、size(panelWidth, panelHeight)、time(hour, minute)、minuteStep、style、theme、transition、zIndex / z、onChange、onOpenChange。内部显示 12 小时轮盘,回调仍返回 0 - 23 的小时和 0 - 59 的分钟,onChange 只在 Done 时触发。
colorpicker 支持 open、screen、size(panelWidth, panelHeight)、value(core::Color)、colors(std::vector<core::Color>)、style、theme、transition、zIndex / z、onChange、onOpenChange。颜色 alpha 会按 1.0f 处理,面板里提供 RGB slider 和预设色块,onChange 只在 Done 时触发。
picker 的拖动、滚轮和 slider 交互只捕获静态 draft / drag state,不捕获临时 builder 指针;外部 onChange 只在 Done 的命中区触发。
数据展示
dataTable
components::dataTable(ui, "metrics.table")
.size(420.0f, 174.0f)
.columns({"Name", "Status", "Value"})
.rows({
{"Cache", "Ready", "94%"},
{"Render", "Idle", "12ms"},
{"Network", "Online", "32ms"},
})
.theme(tokens)
.build();
支持 size、columns、rows、style、theme、transition。datatable 是同一个 builder 的别名。
表格内部会裁剪内容区,表头和末行圆角用 patch rect 衔接,避免圆角和分割线断开。
图表
三个图表都是通用组件,使用 DSL 图元绘制,不使用 SVG 图片。tooltip 通过 hoverOpacityFrom(sourceId) 绑定到对应数据点、柱或扇区。
linechart
components::linechart(ui, "usage.line")
.size(206.0f, 236.0f)
.title("Usage")
.values({0.18f, 0.32f, 0.48f, 0.42f, 0.72f, 0.64f})
.labels({"Jan", "Feb", "Mar", "Apr", "May", "Jun"})
.theme(tokens)
.build();
支持 size、title、values、labels、style、theme、transition。lineChart 是别名。折线由旋转 rect 线段绘制,点是单层交互圆,hover 时显示对应 label 和百分比。
barchart
components::barchart(ui, "usage.bar")
.size(206.0f, 236.0f)
.title("Volume")
.values({0.92f, 0.36f, 0.68f, 0.52f})
.labels({"D1", "D2", "D3", "D4"})
.theme(tokens)
.build();
支持 size、title、values、labels、colors、style、theme、transition。barChart 是别名。每根柱子独立 hover,tooltip 锚点位于柱顶。
piechart
components::piechart(ui, "usage.pie")
.size(206.0f, 236.0f)
.title("Share")
.values({42.0f, 24.0f, 18.0f, 16.0f})
.labels({"Blue", "Green", "Orange", "Pink"})
.theme(tokens)
.build();
支持 size、title、values、labels、colors、style、theme、transition。pieChart 是别名。扇区使用 core 的 polygon 图元绘制,负数按 0 处理,显示值按总和归一化为百分比。
当前限制
- 组件没有生命周期钩子,状态应放在页面或业务层。
scroll只负责 offset 控制,不是完整 ScrollArea。input暂时没有 IME 组合态显示和撤销栈。- chart 是轻量展示组件,当前只显示归一化百分比,不提供坐标轴刻度配置、图例布局和数据格式化回调。
dialog、toast、contextMenu、dropdown、datepicker、timepicker、colorpicker需要页面传入 open/visible 状态,组件不会自己持久化开关状态。- transform 后的 hit-test 仍按布局矩形计算。
- 复杂嵌套滚动、圆角 clip、事件冒泡等能力仍属于 Runtime 后续增强范围。
写新组件时的底线
class ExampleBuilder {
public:
ExampleBuilder(core::dsl::Ui& ui, std::string id)
: ui_(ui), id_(std::move(id)) {}
ExampleBuilder& value(bool value) {
value_ = value;
return *this;
}
ExampleBuilder& onChange(std::function<void(bool)> callback) {
onChange_ = std::move(callback);
return *this;
}
void build() {
ui_.stack(id_)
.size(160.0f, 42.0f)
.visualStateFrom(id_ + ".hit", 0.965f)
.content([&] {
ui_.rect(id_ + ".hit")
.size(160.0f, 42.0f)
.states(normal_, hover_, pressed_)
.onClick([callback = onChange_, value = value_] {
if (callback) {
callback(!value);
}
})
.build();
ui_.text(id_ + ".label")
.size(160.0f, 42.0f)
.text(value_ ? "On" : "Off")
.horizontalAlign(core::HorizontalAlign::Center)
.verticalAlign(core::VerticalAlign::Center)
.build();
})
.build();
}
private:
core::dsl::Ui& ui_;
std::string id_;
bool value_ = false;
std::function<void(bool)> onChange_;
core::Color normal_{0.12f, 0.14f, 0.18f, 1.0f};
core::Color hover_{0.16f, 0.18f, 0.24f, 1.0f};
core::Color pressed_{0.10f, 0.12f, 0.16f, 1.0f};
};
重点:组件只是 DSL 组合。视觉、交互和动画都声明到 DSL 树上,由 Runtime 统一执行。