(三)Pulse GUI 语言设计理念与规范
Updated:
(三)Pulse GUI 语言设计理念与规范
前言
这篇记录 Pulse GUI 的设计思路。
一、从哪里开始?
前两篇设计的是逻辑层——事件怎么流转、模块怎么通信。但程序不只有逻辑,还有界面。
先想清楚一个问题:程序运行在哪?
前端(客户端):用户看到的、交互的
后端(服务端):数据处理、业务逻辑
单体应用(local):既是客户端,也是服务端
传统做法是前后端分开设计,各有一套技术栈。但 Pulse 想统一——事件驱动在后端管逻辑,在前端管界面,在单体应用里两者合一。
那前端这一层需要描述什么?
自身:我是什么 → 内容、外观
世界:我在哪里 → 位置、空间
关系:我和谁有关 → 层级、布局
这三个问题,就是 GUI 设计的三元概念。不管是客户端、服务端渲染、还是本地应用,GUI 要回答的都是这三件事。
二、GUI 的本质
传统 GUI 框架的做法:
Widget 树 + 状态管理 + 生命周期钩子 + 样式系统 + 动画引擎 + 布局系统 + ...
概念太多了。退一步想,一个 GUI 元素到底需要什么?
它是什么? → 一个红色的矩形,上面写着"确定"
它在哪里? → 屏幕左上角 100, 200 的位置
它和谁有关系? → 它是弹窗的子元素,排在取消按钮右边
三个问题,刚好对应三个维度:
GUI = 自身 × 世界 × 关系
自身:是什么 / 有什么(内容)
世界:在哪里 / 怎么动(时空)
关系:与谁相关(连接)
所有 GUI 问题都可以归到这三个维度里。
三、三元 × 三层
每个维度再分三层——原子、组合、聚合:
| 概念 | 原子 | 组合 | 聚合 |
|---|---|---|---|
| 自身 | node | component | view |
| 世界 | space | viewport | scene |
| 关系 | relation | group | tree |
3 × 3 = 9 个概念,覆盖了 GUI 的所有场景。
| 层级 | 含义 | 类比 |
|---|---|---|
| 原子 | 最小单元,不可再分 | 砖头 |
| 组合 | 原子的组合,可复用 | 墙 |
| 聚合 | 组合的组合,页面级 | 房间 |
四、自身:是什么 / 有什么
node(原子)
最小可见单元,不可再分。它就是一个可以看到的东西:
<Node
shape="rect"
size="100 50"
fill="red"
stroke="1 black"
text="Hello"
image="icon.png"
font="16 bold"
opacity="0.8"
/>
没有 div、span、button、input 的区分。一个 Node 可以是任何东西——矩形、圆形、文字、图片,取决于你给它什么属性。
component(组合)
node 的组合,可复用:
component Button
<props>
label: String
disabled: bool = false
</props>
<state>
mode: idle | pressed
</state>
<template>
<Node
shape="rect"
size="80 40"
fill="{{ mode == pressed ? theme.dark : theme.primary }}"
text="{{ label }}"
/>
</template>
注意 <state> 里的 mode: idle | pressed——这是枚举状态,按钮只有这两种状态,不多不少。
view(聚合)
component 的组合,页面级别:
view OrderPage
<state>
orders: List<Order> = []
loading: bool = true
</state>
<handle>
ViewLoad { } {
send Order.Fetch { }
}
Order.Fetched { orders } {
set orders = orders
set loading = false
}
</handle>
<template>
<Scene layout="column gap:16 padding:16">
<Node if="loading" text="加载中..." />
<OrderCard for="order in orders" :order="order" />
</Scene>
</template>
view 里出现了 handle 和 send——和 Pulse 核心语言一样的事件机制。GUI 不是独立的体系,它就是 Pulse 的一部分。
五、世界:在哪里 / 怎么动
space(原子)
节点在空间中的位置:
<Node
position="100 200"
rotation="45deg"
scale="1.5"
/>
viewport(组合)
可视窗口,决定用户看到什么:
<Viewport size="320 240" offset="0 100">
<Scene>...</Scene>
</Viewport>
场景可能很大,viewport 是用户看到的那个窗口。就像地图软件——地图是整个城市,你看到的是屏幕大小的一块。
scene(聚合)
场景空间,可以比视窗大:
<Scene size="1000 1000">
<Node position="500 500" />
</Scene>
怎么动:无时间动画
传统动画需要指定持续时间:
/* CSS */
transition: opacity 0.3s ease-in-out;
Pulse 不用时间,用三个要素:
| 要素 | 说明 | 值 |
|---|---|---|
| 频率 | 快慢(相对) | instant / fast / normal / slow |
| 程度 | 变化幅度 | 属性变化 |
| 循环 | 重复次数 | 1 / n / infinite |
<transition>
idle -> pressed {
frequency: fast
magnitude: {
scale: 1 -> 0.95
fill: theme.primary -> theme.dark
}
loop: 1
}
loading {
frequency: normal
magnitude: {
rotation: 0deg -> 360deg
}
loop: infinite
}
</transition>
为什么不用时间?因为语言层是桥梁,它描述的是「变化的意图」,不是「变化的执行」。具体多少毫秒完成,是渲染层(执行层)的事。不同设备、不同帧率、不同用户偏好,执行层自己决定。
这和 Pulse 核心语言的理念一致:语言层描述”要什么”,执行层决定”怎么做”。
六、关系:与谁相关
relation(原子)
父子连接、层级:
<Node parent="container" layer="1" />
group(组合)
组织分组、布局:
<Group layout="row gap:8 align:center">
<Button label="保存" />
<Button label="取消" />
</Group>
tree(聚合)
完整的层级结构:
<Tree>
<Node id="root">
<Node id="header" />
<Node id="content" />
<Node id="footer" />
</Node>
</Tree>
七、模板语法
GUI 需要把数据绑定到视图。Pulse 的模板语法:
| 语法 | 作用 | 示例 |
|---|---|---|
{{ value }} |
文本插值 | text="{{ name }}" |
:prop |
属性绑定 | :disabled="loading" |
::prop |
双向绑定 | ::value="input" |
@event |
事件绑定 | @click="HandleClick" |
if / else |
条件渲染 | <Node if="loading" /> |
for |
循环渲染 | <Node for="item in items" /> |
看着像 Vue?是的。声明式模板语法已经被验证过了,没必要重新发明。
八、主题
颜色、样式不应该散落在各处。Pulse 用 theme 统一管理:
theme Light {
primary: #007AFF
dark: #0056B3
background: #F2F2F7
text: #333333
border: #E5E5EA
}
theme Dark extends Light {
background: #1C1C1E
text: #FFFFFF
}
组件里用 theme.primary 引用,切换主题时自动生效。Dark extends Light 表示只覆盖需要改的部分,其余继承。
九、和核心语言的关系
Pulse GUI 不是独立的框架,它和核心语言共享同一套机制:
| 概念 | 核心语言 | GUI |
|---|---|---|
| 事件处理 | handle |
<handle> |
| 发送事件 | send |
send |
| 状态修改 | set |
set |
| 状态声明 | state |
<state> |
| 属性输入 | — | <props> |
GUI 组件里的 handle 可以直接处理业务事件:
<handle>
Order.Fetched { orders } {
set orders = orders
set loading = false
}
</handle>
不需要中间层转换,不需要额外的状态管理库。业务事件进来,直接更新视图状态。
十、刻意不做的东西
| 没有 | 为什么 |
|---|---|
| 虚拟 DOM | 声明式 + 事件驱动,渲染层自己优化 |
| CSS-in-JS | 属性直接写在节点上 |
| 组件生命周期钩子 | 用事件代替(ViewLoad 等) |
| 绝对时间动画 | 语言层只描述意图,执行层决定时间 |
| Widget 类型系统 | 只有 Node,属性决定外观 |
少一个概念,就少一类问题。
十一、从第一篇到这里
第一篇:编程范式的思考
→ 事件驱动消除并发
→ 各范式混合使用
第二篇:Pulse 语言设计
→ 15 个关键字
→ 三层架构
→ 一切皆事件
第三篇(本篇):Pulse GUI 设计
→ GUI = 自身 × 世界 × 关系
→ 3 × 3 = 9 个概念
→ 和核心语言共享事件机制
三篇文章走下来:范式思考 → 语言设计 → GUI 扩展。核心始终是同一个——事件来了,处理,发新事件。GUI 只是让这个模型有了一张脸。
后记
Pulse GUI 的设计原则:
| 原则 | 说明 |
|---|---|
| 三元 | 自身 × 世界 × 关系 |
| 三层 | 原子 → 组合 → 聚合 |
| 无时间 | 描述意图,不描述执行 |
| 事件驱动 | 和核心语言统一 |
| 声明式 | 描述”是什么”,不是”怎么做” |
自身定义内容,世界承载位置,关系组织结构,事件驱动变化。
参考
本系列:
架构模式:
- MVC: Model-View-Controller
- MVP: Model-View-Presenter
- MVVM: Model-View-ViewModel
- Elm Architecture: Model-Update-View
Web 前端:
- Vue: 声明式模板与响应式数据绑定
- React: 组件化与单向数据流
- CSS / Tailwind CSS: 样式与布局系统
桌面 GUI:
- WPF: XAML 声明式 UI 与数据绑定
- Qt / QML: 信号槽机制与属性绑定
- SwiftUI: 声明式 UI 框架
- Immediate Mode GUI (Dear ImGui)
游戏引擎:
- Unity: 场景树、GameObject 组件模型
- Cocos Creator: 节点树与组件式开发
附件:
