Oct 30, 2025
2 min read
GUI,
Rust,

现代的 Rust GUI 开发框架: GPUI 与 Tailwindcss 的相似性

如果你熟悉 tailwindcss ,那么你一定对 GPUI 的 API 感到似曾相识。因为 GPUI 关于样式的 API 就是直接抄的 tailwindcss。

是的,GPUI 对 UI 的样式做了原子化的设计:

    div()
        .flex()
        .size_full()
        .p_4()
        .bg(rgb(0xf0f0f0))
        .text_color(black())
        .child(nav())

ElementId

ElementId 是 GPUI 元素的唯一标识符。它用于在 GPUI 组件树中引用元素。因此保持 id 的唯一性很重要,这个和前端的 DOM 元素的 id 类似。通过唯一的 id,我们可以很方便的对某个元素绑定事件。为元素添加 id 之后,类型也从 E 类型变成 Stateful<E> 类型。

div().id("my-element").child("Hello, World!")

Render vs RenderOnce

所有的元素,要么实现 RenderOnce,要么实现 Render。不过,RenderOnce一般用于定义组件,而 Render 一般用于定义视图(可以绘制到屏幕上的对象)。RenderOnce 会消费 self 的所有权,而 Render 则对自身可变引用。

// Render - 视图持续存在,多次渲染
impl Render for MyView {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        // 可以访问和修改 self 的状态
        div().child(Label::new("Hello"))
    }
}

// RenderOnce - 一次性组件
impl RenderOnce for MyComponent {
    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
        // 消费 self,通常用于构建可重用的组件模式
        div().child(Label::new("Component content"))
    }
}

主要区别:

  • Render实现了Render的类型通常是具有持久状态的实体,在每一帧渲染时都会调用render方法,可以访问和修改组件的内部状态。
  • RenderOnce 通常用于构建无状态或简单状态的UI片段,通过消费self来创建一次性的UI元素。

在 GPUI 中,可以简单粗暴地认为,RenderOnce 用于定义组件,而 Render 用于定义视图。

上下文(Context)

从上面的RenderRenderOnce 可知,Render 的 render 方法的签名里带有 cx: &mut Context<Self> 上下文参数。这是一个实体上下文实例,用于处理上下文级别的事务。这些上下文是传递给函数的引用,使得可以与全局状态、窗口、实体和系统服务进行交互。

UI

整体上,只要你对 tailwindcss 熟悉,那么你对 GPUI 的 API 就不会陌生。但是有一些样式和效果, GPUI 未必有对应的 API。

动画和过渡效果

GPUI 只支持简单的动画,可以通过.with_animation(id, animation, animator) 来实现。 目前未找到对应的过渡效果相关的 API,当然,理论上,我们也可以自己实现过滤效果的,只是这个实现成本很大,这意味着我们需要自己维护状态管理,以及对组件更细节的控制。 如果组件需要状态管理,单纯地实现RenderOnce 无法满足要求,我们必须实现Render

官方的示例中,旋转动画效果作用在 svg 上的,原因是 svg 元素实现了with_transformation 方法,原因是,在底层 api 上,只有 paint_svg 方法提供了过渡效果的参数:

pub fn paint_svg(
        &mut self,
        bounds: Bounds<Pixels>,
        path: SharedString,
        mut data: Option<&[u8]>,
        transformation: TransformationMatrix, //提供了过渡效果的参数
        color: Hsla,
        cx: &App,
    ) -> Result<()>

普通的组件并不是通过 paint_svg 方法绘制的,所以其实有一些很常见的动画效果,可能也需要我们从底部组件上实现。

父子组件的样式继承

在前端中,多数框架或者样式库都支持在父组件上添加样式,子组件会继承父组件的样式。这在做效果时非常的高效有用。然而 GPUI 中,暂时还没找到控制的方法。 一个简单的例子,当鼠标移入 button 组件时,button 的背景应该改变颜色,子元素 span 的颜色也应该一并改变。在前端里,特别是 tailwindcss 中,我们可以这样做:

<button class="bg-blue-500 hover:bg-blue-700 text-white hover:text-slate-300 ">
 <span> primary </span>
 </button>

这里的 span 会继承 button 的 text 样式。但是在 gpui 中, 我暂时未找到在 hover 事件中控制子元素的 API。

其他

当然,gpui 只实现了部分 tailwindcss 常见的样式 api,其他不太常用的效果,只能自己自行实现了。

比如,前端组件里比较常见的环形 ⭕️ 进度条, 在 gpui 里如果只使用顶层 API,就很难实现。