Contents
  1. 1. (三)Pulse GUI 语言设计理念与规范
    1. 1.1. 前言
    2. 1.2. 一、从哪里开始?
    3. 1.3. 二、GUI 的本质
    4. 1.4. 三、三元 × 三层
    5. 1.5. 四、自身:是什么 / 有什么
      1. 1.5.1. node(原子)
      2. 1.5.2. component(组合)
      3. 1.5.3. view(聚合)
    6. 1.6. 五、世界:在哪里 / 怎么动
      1. 1.6.1. space(原子)
      2. 1.6.2. viewport(组合)
      3. 1.6.3. scene(聚合)
      4. 1.6.4. 怎么动:无时间动画
    7. 1.7. 六、关系:与谁相关
      1. 1.7.1. relation(原子)
      2. 1.7.2. group(组合)
      3. 1.7.3. tree(聚合)
    8. 1.8. 七、模板语法
    9. 1.9. 八、主题
    10. 1.10. 九、和核心语言的关系
    11. 1.11. 十、刻意不做的东西
    12. 1.12. 十一、从第一篇到这里
    13. 1.13. 后记
    14. 1.14. 参考

(三)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"
/>

没有 divspanbuttoninput 的区分。一个 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 里出现了 handlesend——和 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: 节点树与组件式开发

附件:

Contents
  1. 1. (三)Pulse GUI 语言设计理念与规范
    1. 1.1. 前言
    2. 1.2. 一、从哪里开始?
    3. 1.3. 二、GUI 的本质
    4. 1.4. 三、三元 × 三层
    5. 1.5. 四、自身:是什么 / 有什么
      1. 1.5.1. node(原子)
      2. 1.5.2. component(组合)
      3. 1.5.3. view(聚合)
    6. 1.6. 五、世界:在哪里 / 怎么动
      1. 1.6.1. space(原子)
      2. 1.6.2. viewport(组合)
      3. 1.6.3. scene(聚合)
      4. 1.6.4. 怎么动:无时间动画
    7. 1.7. 六、关系:与谁相关
      1. 1.7.1. relation(原子)
      2. 1.7.2. group(组合)
      3. 1.7.3. tree(聚合)
    8. 1.8. 七、模板语法
    9. 1.9. 八、主题
    10. 1.10. 九、和核心语言的关系
    11. 1.11. 十、刻意不做的东西
    12. 1.12. 十一、从第一篇到这里
    13. 1.13. 后记
    14. 1.14. 参考