GPUI 框架完整学习教程

GPUI 框架完整学习教程

基于 Zed 编辑器 GPUI 框架示例代码编写的系统性教程

源码路径:G:/开源代码/zed/crates/gpui/examples/

目录

  • 教程概览
  • [01. Hello World](#01. Hello World)
  • [02. 核心概念](#02. 核心概念)
  • [03. 布局系统](#03. 布局系统)
  • [04. 样式与外观](#04. 样式与外观)
  • [05. 文本与字体](#05. 文本与字体)
  • [06. 图像与图标](#06. 图像与图标)
  • [07. 交互与事件](#07. 交互与事件)
  • [08. 焦点与导航](#08. 焦点与导航)
  • [09. 菜单与命令](#09. 菜单与命令)
  • [10. 输入框与表单](#10. 输入框与表单)
  • [11. 拖放系统](#11. 拖放系统)
  • [12. 状态管理](#12. 状态管理)
  • [13. 事件发射](#13. 事件发射)
  • [14. 全局状态](#14. 全局状态)
  • [15. 列表与虚拟滚动](#15. 列表与虚拟滚动)
  • [16. 弹窗与浮层](#16. 弹窗与浮层)
  • [17. 动画系统](#17. 动画系统)
  • [18. Canvas 绘图](#18. Canvas 绘图)
  • [19. 渐变效果](#19. 渐变效果)
  • [20. 多窗口管理](#20. 多窗口管理)
  • [21. 窗口定位](#21. 窗口定位)
  • [22. 窗口阴影](#22. 窗口阴影)
  • [API 参考](#API 参考)
  • 示例代码

GPUI 学习教程 --- 教程概览

开始之前: 本教程将带你从零开始掌握 GPUI 框架,构建高性能原生 GUI 应用。

GPUI 是一个用 Rust 编写的、面向高性能场景的即时模式(immediate-mode)GUI 框架。 它诞生于 Zed 编辑器 的开发需求,充分利用 GPU 加速渲染,为复杂的桌面应用提供流畅的交互体验。 本教程共 25 章,系统覆盖从"Hello World"到复杂组件架构的完整知识体系。

什么是 GPUI?

GPUI 是 Zed 编辑器团队为构建 Zed 编辑器 而开发的 GUI 框架。 与传统的保留模式(retained-mode)GUI 框架不同,GPUI 采用即时模式范式: 每一帧都重新描述整个 UI 树,由框架负责高效的差异比对和 GPU 批量渲染。

这种设计带来了几个关键优势:UI 代码更直观易读、状态管理更可预测、 并且因为直接对接 GPU,可以轻松实现 60fps 甚至 144fps 的流畅动画与交互。

💡 提示: 即时模式 vs 保留模式

保留模式(如 Qt、GTK)需要你手动维护 UI 组件的生命周期;即时模式(如 GPUI、Dear ImGui)每帧重新构建 UI 描述,框架自动处理差异。前者更灵活但更复杂,后者更简单但要求渲染效率高------GPUI 的 GPU 加速正是为解决这个矛盾而生。

核心特性

GPU 加速渲染

基于 wgpu 的跨平台 GPU 渲染后端,充分利用现代显卡的并行计算能力,支持高性能动画与大规模 UI 元素。

响应式编程

内置响应式和派生状态系统,类似 React 的 useState/useMemo,但完全类型安全,编译期即可捕获大部分错误。

Rust 类型安全

整个框架的 API 设计充分利用 Rust 的类型系统,Entity 绑定的 Context 类型在编译期防止状态误用。

声明式 API

采用链式调用风格的声明式 API,用 div().flex().child(...) 的方式描述 UI,代码即文档,可读性极强。

学习路线图

本教程共 25 章,按难度递进排列。下表列出了所有章节的编号、标题、核心内容和难度等级。

编号 标题 核心内容 难度
--- 教程概览 GPUI 简介、学习路线、前置知识
01 Hello World 第一个 GPUI 程序、App 生命周期
02 核心概念 Render/Entity/Context、状态管理基础 ⭐⭐
03 布局系统 Flexbox、CSS Grid、定位与间距 ⭐⭐
04 样式与外观 颜色、边框、阴影、透明度 ⭐⭐
05 文本与字体 文本渲染、字体配置、国际化 ⭐⭐
06 交互与事件 鼠标/键盘事件、点击/拖拽处理 ⭐⭐⭐
07 组件基础 自定义组件、props 传递、children ⭐⭐⭐
08 状态管理 useState、use_derived、全局状态 ⭐⭐⭐
09 列表与虚拟滚动 List、uniform_list、虚拟滚动原理 ⭐⭐⭐⭐
10 动画系统 transition、keyframe、缓动函数 ⭐⭐⭐
11 弹窗与浮层 Popover、Modal、Tooltip ⭐⭐⭐
12 输入框与表单 TextInput、Checkbox、Select ⭐⭐⭐
13 焦点与导航 Focus management、键盘导航 ⭐⭐⭐⭐
14 拖放系统 Drag & Drop、拖放数据源与目标 ⭐⭐⭐⭐
15 菜单与命令 ContextMenu、CommandPalette ⭐⭐⭐
16 主题系统 全局主题、暗色/亮色模式切换 ⭐⭐⭐
17 图标与图片 SVG 图标、图片加载、纹理管理 ⭐⭐
18 多窗口管理 多窗口创建、窗口间通信 ⭐⭐⭐⭐
19 异步与任务 async/await、后台任务、进度指示 ⭐⭐⭐⭐
20 剪贴板与拖放 Clipboard API、系统拖放集成 ⭐⭐⭐
21 性能优化 渲染优化、内存管理、性能分析 ⭐⭐⭐⭐⭐
22 测试与调试 单元测试、快照测试、调试工具 ⭐⭐⭐
23 打包与发布 Cargo bundle、安装包制作 ⭐⭐⭐
24 实战项目:文本编辑器 综合应用所有知识点 ⭐⭐⭐⭐⭐
25 进阶主题 自定义渲染器、插件系统、WebAssembly ⭐⭐⭐⭐⭐

如何使用本教程

建议按照章节顺序依次学习,每一章都包含可运行的代码示例和详细解释。你可以:

  • 📖 顺序阅读:从第一章开始,跟随教程逐步构建知识体系
  • 💻 动手实践:每章的代码示例都可以在本地运行,建议亲手输入一遍
  • 🔍 查阅参考:使用侧边栏快速跳转到任意章节
  • 📌 标记进度:顶部进度条会自动记录你的学习进度

⚠️ 注意: 编译时间提醒

GPUI 依赖较多,首次编译可能需要 5--15 分钟,具体取决于你的机器配置。建议使用 cargo watch 或 cargo check 进行快速迭代开发,避免频繁完整编译。

前置知识

本教程假设你已经具备以下 Rust 基础知识。如果某些概念还不够熟悉,建议先复习相关内容再继续。

Rust 基础

  • 基本语法:变量绑定、函数定义、控制流(if/loop/for)
  • 复合类型:struct、enum、tuple
  • 泛型:impl、trait bound(where 子句)
  • 错误处理:Result<T, E>、Option、? 操作符

所有权与借用

  • 所有权规则:每个值有唯一所有者,离开作用域自动释放
  • 借用:&T(共享引用)与 &mut T(可变引用)
  • 生命周期:'a 标注、生命周期省略规则
  • Clone 与 Copy trait 的区别

Trait 系统

  • 定义 trait:trait MyTrait { fn method(&self); }
  • 为类型实现 trait:impl MyTrait for MyStruct
  • 常用标准库 trait:Debug、Clone、Default、Iterator
  • 关联类型与泛型 trait

💡 提示: 推荐的 Rust 学习资源

如果你需要复习 Rust 知识,推荐阅读 The Rust Programming Language(中文版:《Rust 程序设计语言》)。重点关注第 4 章(所有权)、第 5 章(结构体)、第 10 章(trait)和第 13 章(生命周期)。


01 Hello World --- 你的第一个 GPUI 程序

从零开始: 本章将带你编写并运行第一个 GPUI 程序,理解应用的基本生命周期。

每个 GPUI 应用都遵循相同的骨架:创建 App 实例、打开窗口、运行事件循环。 在这一章中,我们将逐行分析最小可运行程序,并介绍 GPUI 渲染模型的核心概念。

最小 GPUI 程序结构

一个 GPUI 程序包含三个核心步骤:创建 App打开窗口启动事件循环。 下面是最简化的完整程序(hello_world.rs),它会在窗口中显示 "Hello, World!"。

hello_world.rs

rust 复制代码
use gpui::*;

// 定义应用状态结构体
struct HelloWorld {
text: SharedString,
}

impl HelloWorld {
fn new(text: &str, _cx: &mut Context<Self>) -> Self {
Self {
text: SharedString::from(text),
}
}
}

// 实现 Render trait ------ GPUI 的渲染入口
impl Render for HelloWorld {
fn render(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> impl IntoElement {
div()
.flex()
.bg(rgb(0x505050))
.size(px(500.0))
.items_center()
.justify_center()
.text_xl()
.text_white()
.child(format!("Hello, {}!", &self.text))
}
}

// 程序入口
fn main() {
App::new()
.run(|mut cx| {
cx.open_window(
WindowOptions {
titlebar: Some(WindowTitlebarOptions::default()),
..Default::default()
},
|cx| HelloWorld::new("World", cx),
);
});
}

💡 提示: SharedString 是什么?

SharedString 是 GPUI 提供的零拷贝共享字符串类型,基于 Arc<u8> 实现。与 String 相比,SharedString 在 Clone 时只增加引用计数,不复制内存,非常适合在 UI 渲染中频繁传递文本。

关键概念解释

下面是上面代码中出现的每个核心概念的详细说明:

概念 说明
App GPUI 应用的顶层上下文。负责全局资源管理、事件循环和窗口管理。App::new() 创建实例,.run() 启动事件循环。
Window 代表一个操作系统窗口。cx.open_window(options, constructor) 创建新窗口并绑定根组件。
Context 与特定 Entity 绑定的上下文。提供状态读写、事件监听、通知重渲染等能力。类型参数 T 确保只能访问对应实体的状态。
Entity GPUI 的状态容器,类似 React 的 state。通过 cx.new(
Render trait 有状态组件的渲染 trait。要求实现 render(&mut self, window, cx) 方法,返回 impl IntoElement。每次状态变化后自动调用。
div() 创建一个 Div 元素,是 GPUI 中最基础的布局容器。支持链式调用大量修饰方法(class)来设置样式和布局。
Element GPUI 中所有可渲染元素的统称。IntoElement trait 允许任何类型转换为最终的渲染描述。

div() 链式 API 说明

GPUI 的 UI 描述采用链式调用风格,每一个修饰方法都返回 self(或 Self), 因此可以连续调用。这种方式类似于 CSS 的写法,但完全类型安全。

以示例代码中的链式调用为例:

链式 API 解析

rust 复制代码
div()                          // 创建一个 Div 元素
.flex()                      // 设置 display: flex
.bg(rgb(0x505050))       // 设置背景色为深灰色
.size(px(500.0))        // 设置宽高为 500px
.items_center()              // align-items: center(交叉轴居中)
.justify_center()            // justify-content: center(主轴居中)
.text_xl()                   // 设置字体大小为 xl
.text_white()                 // 设置文字颜色为白色
.child(                       // 添加一个子元素
format!("Hello, {}!", &self.text)
)

常用的链式修饰方法可分为以下几类:

分类 方法示例 效果
布局 .flex() .flex_row() .flex_col() Flexbox 布局方向
对齐 .items_center() .justify_between() 子元素对齐方式
尺寸 .size() .w() .h() .flex_1() 宽高与弹性
间距 .p() .px() .py() .m() .gap() 内外边距
颜色 .bg() .text_color() .border_color() 背景、文字、边框颜色
圆角 .rounded() .rounded_lg() .rounded_full() 边框圆角
子元素 .child() .children() 添加单个/多个子元素

⚠️ 注意: 链式调用的顺序无关性

大多数修饰方法的调用顺序不影响最终渲染结果(因为它们设置的是独立的 CSS 属性)。但 .child() 和 .children() 的调用顺序决定子元素的排列顺序,需要特别注意。

运行你的第一个程序

创建一个新的 Rust 项目,并在 Cargo.toml 中添加 GPUI 依赖:

Cargo.toml

toml 复制代码
[dependencies]
gpui = { git = "https://github.com/zed-industries/gpui" }
gpui_macros = { git = "https://github.com/zed-industries/gpui" }

将上面的 "hello_world.rs" 代码保存到 src/main.rs,然后运行:

终端

bash 复制代码
cargo run

如果一切正常,你会看到一个 500×500 的窗口,背景为深灰色,中央显示白色的 "Hello, World!" 文字。


02 核心概念 --- Render、Entity 与上下文

理解核心抽象: 本章深入讲解 GPUI 的三大核心抽象:Render trait、Entity 状态容器和 Context 上下文体系。

GPUI 的设计哲学是状态驱动渲染:当状态变化时,框架自动调用渲染方法更新 UI。 理解 Render trait、Entity 和 Context 体系是掌握 GPUI 的关键。

Render trait vs RenderOnce trait

GPUI 提供了两个渲染 trait:Render 和 RenderOnce。 选择哪一个取决于组件是否需要持有状态

Trait 适用场景 render 方法签名 性能特点
Render 有状态组件,持续存在 fn render(&mut self, window, cx) → impl IntoElement 可重渲染,调用 cx.notify() 触发更新
RenderOnce 无状态组件,一次性渲染 fn render(self, window, cx) → impl IntoElement 渲染后丢弃,适用于轻量级展示组件

下面是一个 RenderOnce 的示例------一个无状态的"标签"组件:

label_component.rs

rust 复制代码
use gpui::*;

// 无状态组件:拥有数据所有权,渲染后丢弃
struct Label {
text: SharedString,
color: Rgba,
}

impl RenderOnce for Label {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
div()
.text_color(self.color)
.text_sm()
.child(self.text)
}
}

// 使用:在有状态组件的 render 方法中
// .child(Label { text: "Hello".into(), color: rgb(0xffffff) })

💡 提示: 如何选择 Render 还是 RenderOnce?

如果你的组件需要在多次渲染之间保持状态(例如:计数器、输入框、展开/折叠面板),使用 Render。

如果组件只是展示传入的数据,不需要维护自己的状态,使用 RenderOnce 更轻量。

Entity 详解 --- GPUI 的状态容器

Entity 是 GPUI 中状态管理的核心抽象,类似于 React 中的 useState, 但它是类型安全的,并且与 Rust 的所有权系统深度集成。

创建 Entity

使用 cx.new(|cx| T { ... }) 在 Context 中创建一个 Entity:

创建 Entity 示例

rust 复制代码
use gpui::*;

struct Counter {
count: i32,
}

impl Counter {
fn new(cx: &mut Context<Self>) -> Self {
Self { count: 0 }
}

fn increment(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.count += 1;
cx.notify();  // 通知 GPUI 状态已变化,需要重渲染
}
}

impl Render for Counter {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.items_center()
.gap(px(8.0))
.child(format!("Count: {}", self.count))
.child(
button("+1")
.on_click(|_, window, cx| cx.entity().update(cx, |this, cx| {
this.increment(window, cx);
}))
)
}
}

fn main() {
App::new().run(|mut cx| {
let counter = cx.new(|_cx| Counter::new(_cx));
cx.open_window(WindowOptions::default(), |cx| counter.clone());
});
}

读取与更新 Entity

Entity 提供两种访问方式:

方法 用途 示例
entity.read(cx) 获取不可变引用(&T) let count = entity.read(cx).count;
entity.update(cx, this, cx { ... })
cx.notify() 在 update 闭包中调用,触发重渲染 在 increment 方法中调用

Context 层次:Context、Window 与 App

GPUI 中有三个层级的上下文,各自管理不同范围的状态:

  • Context --- 组件级上下文,绑定到特定 Entity。提供该组件的状态读写、事件监听和通知重渲染。每个有状态组件都有自己的 Context。

  • Window --- 窗口级上下文,管理单个窗口内的全局状态:焦点、拖放、弹窗等。所有在同一窗口中的组件共享同一个 Window 实例。

  • App --- 应用级上下文,管理跨窗口的全局状态:全局快捷键、应用设置、多窗口协调。是整个应用的顶层上下文。

在回调函数中,你经常会看到 mut Window 和不同层级的 Context 同时出现:

上下文层级示例

rust 复制代码
// 在事件回调中,通常有三个参数:
// _event: 触发的事件(如 ClickEvent)
// window: &mut Window  ------ 窗口级上下文
// cx:      &mut App     ------ 应用级上下文(全局)
// 或在组件方法中:
// window: &mut Window        ------ 窗口级上下文
// cx:      &mut Context<T>   ------ 组件级上下文

fn on_click(
_event: &ClickEvent,
window: &mut Window,
cx: &mut App,  // 注意:这里是 &mut App,不是 Context<T>
) {
// 通过 cx 访问全局状态
}

cx.notify() 与 cx.listener()

cx.notify() --- 触发重新渲染

当你修改了组件的 State 后,必须调用 cx.notify() 通知 GPUI 该组件需要重新渲染。 这与 React 的 setState() 自动触发重渲染不同------GPUI 采用显式通知机制,带来更细粒度的控制。

notify 示例

rust 复制代码
fn set_text(&mut self, new_text: String, cx: &mut Context<Self>) {
self.text = SharedString::from(new_text);
cx.notify();  // 必须调用!否则 UI 不会更新
}

cx.listener() --- 创建回调

cx.listener() 用于在当前组件上注册一个事件监听器, 当被监听的 Entity 发出指定事件时,回调函数会被执行。

listener 示例

rust 复制代码
// 假设有一个 Counter 组件,它会在 increment 时发出 CounterIncremented 事件
use gpui::*;

struct CounterIncremented;  // 自定义事件类型

impl Counter {
fn increment(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.count += 1;
cx.emit(CounterIncremented);  // 发出事件
cx.notify();
}
}

// 在另一个组件中监听这个事件
impl Render for Display {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
cx.listener(self.counter.clone(), |this, _ev: &CounterIncremented, window, cx| {
// 当 Counter 发出 CounterIncremented 时执行
// 可以在这里更新 Display 组件的状态
});

div().child("Display")
}
}

SharedString --- 零拷贝共享字符串

SharedString 是 GPUI 中推荐的字符串类型,用于 UI 文本。 它基于 Arc 实现,Clone 时只增加引用计数,不复制底层数据。

SharedString 用法

rust 复制代码
use gpui::*;

let s1: SharedString = "Hello".into();
let s2 = s1.clone();  // 零成本!只增加 Arc 引用计数

// 从 String 转换
let s3 = SharedString::from("Hello".to_string());

// 在 format! 中使用
format!("Hello, {}!", &SharedString::from("World"))

⚠️ 注意: SharedString 与 String 的区别

String 拥有堆分配的缓冲区,Clone 时会真正复制内存(O(n))。SharedString 是引用计数的,Clone 是 O(1) 的。但在创建 SharedString 时(从 String 转换),仍需要一次堆分配。建议:组件字段用 SharedString,局部临时字符串仍可用 String。

核心概念速查

  • Render trait --- 有状态组件的渲染入口。render() 方法返回 UI 描述。状态变化后调用 cx.notify() 触发重渲染。

  • RenderOnce trait --- 无状态组件的渲染入口。render() 消耗 self,适用于纯展示组件。性能更优但无法重渲染。

  • Entity --- GPUI 的状态容器。用 cx.new() 创建,用 read()/update() 访问。类似 React 的 useState + 组件实例。

  • cx.notify() --- 显式通知框架当前组件需要重渲染。必须在状态修改后调用,否则 UI 不会更新。

  • cx.listener() --- 注册事件监听器,监听其他 Entity 发出的事件。是实现组件间通信的核心机制。

  • SharedString --- 零拷贝共享字符串。Clone 成本极低,适合作为组件字段类型和跨组件传递文本。

💡 提示: 为什么 GPUI 使用显式 notify 而不是自动追踪?

显式 cx.notify() 让开发者精确控制重渲染时机,避免不必要的渲染开销。在 Zed 编辑器中,文档可能非常大,自动追踪所有状态变化会带来显著的性能损耗。显式通知机制是 GPUI 能做到高性能的关键设计之一。


03 布局系统 --- Flexbox 与 CSS Grid

掌握布局: 本章介绍 GPUI 的 Flexbox 和 CSS Grid 布局系统,以及定位、间距和尺寸控制。

GPUI 的布局系统完全对应 Web 的 Flexbox 和 CSS Grid 规范。 如果你有 Web 前端经验,会发现 API 几乎是一比一映射的。 这使得布局代码直观且易于预测。

Flexbox 布局

Flexbox 是 GPUI 中最常用的布局方式。通过 .flex() 启用弹性布局, 然后用方向、对齐、间距等修饰方法控制子元素的排列。

方向与主轴

Flexbox 方向 API

rust 复制代码
div()
.flex()              // display: flex(默认 row 方向)
.flex_row()          // flex-direction: row(水平排列,默认值)
.flex_col()          // flex-direction: column(垂直排列)
.flex_row_reverse()  // flex-direction: row-reverse
.flex_col_reverse()  // flex-direction: column-reverse

对齐方式

Flexbox 对齐 API

rust 复制代码
// 交叉轴对齐(align-items)
.items_start()     // align-items: flex-start
.items_center()   // align-items: center
.items_end()       // align-items: flex-end
.items_stretch()   // align-items: stretch

// 主轴对齐(justify-content)
.justify_start()    // justify-content: flex-start
.justify_center()  // justify-content: center
.justify_end()      // justify-content: flex-end
.justify_between()  // justify-content: space-between
.justify_around()   // justify-content: space-around
.justify_evenly()   // justify-content: space-evenly

// 多行对齐(align-content)
.content_start()    // align-content: flex-start
.content_center()  // align-content: center
.content_end()      // align-content: flex-end

间距与弹性

Flexbox 间距与弹性 API

rust 复制代码
// 子元素间距
.gap(px(8.0))       // gap: 8px(行和列)
.gap_x(px(8.0))      // column-gap: 8px
.gap_y(px(8.0))      // row-gap: 8px

// 弹性控制(子元素上使用)
.flex_none()          // flex: none(不伸缩)
.flex_1()             // flex: 1(等分剩余空间)
.flex_shrink_0()      // flex-shrink: 0(不收缩)

💡 提示: Flexbox 使用技巧

在 GPUI 中,.flex() 必须和 .flex_row() 或 .flex_col() 配合使用才能正确工作。如果只调用 .flex() 而不指定方向,默认为 row 方向。推荐始终显式指定方向,提高代码可读性。

CSS Grid 布局

CSS Grid 适用于二维布局(同时控制行和列)。 GPUI 的 Grid API 与 CSS Grid 规范完全一致。

Grid 基础 API

CSS Grid API

rust 复制代码
div()
.grid()               // display: grid
.grid_rows(repeat(3, minmax(px(0.0), fr(1.0))))  // grid-template-rows
.grid_cols(repeat(2, fr(1.0)))                 // grid-template-columns
.gap(px(16.0))      // grid-gap

示例:圣杯布局(grid_layout.rs)

以下是一个完整的"圣杯布局"示例,包含顶栏、侧边栏、主内容区和底栏:

grid_layout.rs

rust 复制代码
use gpui::*;

struct HolyGrailLayout;

impl Render for HolyGrailLayout {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.w_full()
.h_full()
.grid()
// 三行:顶栏(auto)、主区域(1fr)、底栏(auto)
.grid_rows(vec![px(48.0), fr(1.0), px(32.0)])
// 两列:侧边栏(240px)、主内容(1fr)
.grid_cols(vec![px(240.0), fr(1.0)])
.gap(px(0.0))
// 顶栏:横跨两列
.child(
div()
.col_span(2)
.h(px(48.0))
.bg(rgb(0x1e1e1e))
.flex()
.items_center()
.px(px(16.0))
.child("顶栏")
)
// 侧边栏
.child(
div()
.w(px(240.0))
.h_full()
.bg(rgb(0x2a2a2a))
.p(px(12.0))
.child("侧边栏")
)
// 主内容区
.child(
div()
.h_full()
.bg(rgb(0x3a3a3a))
.p(px(16.0))
.child("主内容区")
)
// 底栏:横跨两列
.child(
div()
.col_span(2)
.h(px(32.0))
.bg(rgb(0x1e1e1e))
.flex()
.items_center()
.px(px(16.0))
.child("底栏")
)
}
}

fn main() {
App::new().run(|mut cx| {
cx.open_window(WindowOptions::default(), |_cx| HolyGrailLayout);
});
}

⚠️ 注意: grid_rows / grid_cols 的参数类型

这两个方法接受 Vec 类型参数。px(n) 表示固定像素,fr(n) 表示比例分配(类似 CSS 的 fr 单位)。也可以使用 repeat(count, track) 辅助函数简化代码。

定位系统

GPUI 支持 CSS 的 position 模型: .relative()(相对定位)和 .absolute()(绝对定位)。 绝对定位的元素相对于最近的 .relative() 祖先定位。

定位 API

rust 复制代码
// 定位模式
.relative()            // position: relative
.absolute()            // position: absolute
.fixed()                // position: fixed

// 偏移量(配合 absolute/relative 使用)
.top(px(0.0))         // top: 0
.left(px(0.0))        // left: 0
.right(px(0.0))       // right: 0
.bottom(px(0.0))      // bottom: 0

// 典型用法:全屏覆盖层
div()
.relative()             // 作为定位参考
.w_full().h_full()
.child(
div()
.absolute()         // 绝对定位
.top(px(0.0))
.left(px(0.0))
.right(px(0.0))
.bottom(px(0.0))
.bg(rgba(0x000000, 0.5))  // 半透明黑色遮罩
)

间距系统

GPUI 提供完整的 padding(内边距)和 margin(外边距)控制,与 CSS 的简写规则一致。

间距 API

rust 复制代码
// Padding(内边距)
.p(px(16.0))     // padding: 16px(四边)
.px(px(16.0))    // padding-left + padding-right: 16px
.py(px(8.0))     // padding-top + padding-bottom: 8px
.pt(px(8.0))     // padding-top: 8px
.pr(px(8.0))     // padding-right: 8px
.pb(px(8.0))     // padding-bottom: 8px
.pl(px(8.0))     // padding-left: 8px

// Margin(外边距)------ 注意:GPUI 中 margin 方法名与 padding 类似
.m(px(16.0))     // margin: 16px
.mx(px(16.0))    // margin-left + margin-right
.my(px(8.0))     // margin-top + margin-bottom
.mt(px(8.0))     // margin-top
.mb(px(8.0))     // margin-bottom

// Gap(子元素间距,Flex/Grid 容器上使用)
.gap(px(8.0))     // gap: 8px

尺寸控制

尺寸 API

rust 复制代码
// 同时设置宽高
.size(px(500.0))   // width: 500px; height: 500px
.size(fr(1.0))    // width: 1fr; height: 1fr(在 Grid 中)

// 单独设置宽/高
.w(px(500.0))     // width: 500px
.h(px(300.0))     // height: 300px
.min_w(px(100.0))  // min-width: 100px
.min_h(px(100.0))  // min-height: 100px
.max_w(px(800.0))  // max-width: 800px
.max_h(px(600.0))  // max-height: 600px

// 百分比/全屏
.w_full()              // width: 100%
.h_full()              // height: 100%
.min_w_full()          // min-width: 100%
.min_h_full()          // min-height: 100%

💡 提示: px() vs fr() 的区别

px(n) 表示固定像素值,在任何布局中都生效。fr(n) 表示比例单位,只在 Flexbox(.flex_1())和 CSS Grid(grid_rows/grid_cols)中生效。fr(1.0) 表示"占据剩余可用空间的 1 份"。

布局方法速查表

类别 方法 对应 CSS
Flex 方向 .flex_row() .flex_col() flex-direction
对齐 .items_center() .justify_between() align-items, justify-content
Grid .grid() .grid_rows() .grid_cols() display:grid, grid-template-rows/cols
定位 .absolute() .relative() .top() .left() position, top, left
间距 .p() .px() .py() .pt() .m() .gap() padding, margin, gap
尺寸 .size() .w() .h() .w_full() .flex_1() width, height, flex

04 样式与外观 --- 颜色、阴影与透明度

美化你的 UI: 本章介绍 GPUI 的颜色系统、边框与圆角、阴影效果、透明度和溢出控制。

一个好的 UI 离不开精致的视觉样式。GPUI 提供了完整的样式 API, 涵盖颜色、边框、阴影、透明度等各个方面,全部采用与 CSS 一致的命名约定。

颜色系统

GPUI 使用 Rgba 类型表示颜色,提供三个辅助函数创建颜色值: rgb()、rgba() 和 hsla()。

rgb() --- 不透明颜色

rgb 函数用法

rust 复制代码
// rgb(u32) --- 接受 0xRRGGBB 格式的十六进制数
rgb(0xff5733)  // 红色 0xff, 绿色 0x57, 蓝色 0x33
rgb(0x505050)  // 深灰色
rgb(0xffffff)  // 白色
rgb(0x000000)  // 黑色

// 在链式调用中使用
div()
.bg(rgb(0x7c3aed))     // 紫色背景
.text_color(rgb(0xffffff)) // 白色文字

rgba() --- 带透明度的颜色

rgba 函数用法

rust 复制代码
// rgba(u32, f32) --- 第二个参数是 alpha 通道(0.0~1.0)
rgba(0x000000, 0.5)  // 半透明黑色(遮罩常用)
rgba(0xff0000, 0.2)  // 20% 不透明度的红色
rgba(0xffffff, 0.1)  // 10% 白色(轻微高光)

// 示例:半透明遮罩层
div()
.absolute()
.top(px(0.0)).left(px(0.0))
.right(px(0.0)).bottom(px(0.0))
.bg(rgba(0x000000, 0.5))

hsla() --- HSL 颜色模型

hsla 函数用法

rust 复制代码
// hsla(h: f32, s: f32, l: f32, a: f32)
// h: 色相 (0~360), s: 饱和度 (0~1), l: 亮度 (0~1), a: 透明度 (0~1)
hsla(240.0, 0.8, 0.5, 1.0)  // 不透明蓝色
hsla(120.0, 0.6, 0.4, 0.8)  // 80% 不透明绿色

// HSL 比 RGB 更直观:调整亮度即可获得同色系的深浅变体
let base_hue = 240.0;  // 蓝色色相
let light_blue  = hsla(base_hue, 0.8, 0.7, 1.0);  // 浅蓝
let dark_blue   = hsla(base_hue, 0.8, 0.3, 1.0);  // 深蓝

💡 提示: 什么时候用 rgb,什么时候用 hsla?

rgb() 适合直接使用设计稿中的十六进制色值,简单直观。hsla() 适合需要程序化生成颜色变体的场景(如:hover 时变亮、disabled 时降低饱和度)。Zed 编辑器的主题系统大量使用 HSL 颜色空间。

边框与圆角

边框 API

边框 API 示例

rust 复制代码
div()
.border(px(1.0))             // border-width: 1px(四边)
.border_color(rgb(0x555555))   // border-color
// 也可以单边设置:
.border_top(px(1.0))
.border_left(px(1.0))
.border_bottom(px(0.0))
.border_right(px(0.0))

圆角 API

圆角 API 示例

rust 复制代码
div()
.rounded(px(4.0))         // border-radius: 4px(四角统一)
.rounded_lg(px(8.0))     // 大圆角(常用 8px)
.rounded_full()              // 完全圆形(border-radius: 9999px)
// 单边圆角:
.rounded_t(px(4.0))       // 左上 + 右上
.rounded_b(px(4.0))       // 左下 + 右下
.rounded_l(px(4.0))       // 左上 + 左下
.rounded_r(px(4.0))       // 右上 + 右下

边框 + 圆角组合使用的典型示例:

卡片样式示例(shadow.rs 风格)

rust 复制代码
div()
.p(px(16.0))
.bg(rgb(0x2a2a2a))
.border(px(1.0))
.border_color(rgb(0x444444))
.rounded_lg(px(8.0))
.shadow_md()  // 中等阴影(见下一节)
.child("这是一个卡片")

BoxShadow 阴影系统

GPUI 的阴影通过 BoxShadow 结构体定义,并提供了多个预设级别供快速使用。 阴影是提升 UI 层次感的关键手段。

BoxShadow 结构体

BoxShadow 结构体定义

rust 复制代码
pub struct BoxShadow {
pub color: Rgba,           // 阴影颜色(通常带透明度)
pub offset: Point<Px>,  // 阴影偏移量 (x, y)
pub blur_radius: Px,      // 模糊半径
pub spread_radius: Px,   // 扩散半径(可选,通常为 0)
}

预设阴影级别

GPUI 内置了 6 个阴影预设,从极轻微到非常明显:

预设方法 典型用途 模糊半径
.shadow_xs() 细分隔线、轻微浮起 ~2px
.shadow_sm() 按钮悬停、小卡片 ~4px
.shadow_md() 卡片、下拉菜单 ~8px
.shadow_lg() 弹出面板、对话框 ~16px
.shadow_xl() 模态框、重要提示 ~24px
.shadow_2xl() 全局拖拽预览、最高层级 ~40px

自定义阴影

shadow.rs --- 自定义阴影完整示例

rust 复制代码
use gpui::*;

struct ShadowDemo;

impl Render for ShadowDemo {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
// 使用预设阴影
let with_preset = div()
.w(px(200.0))
.h(px(120.0))
.bg(rgb(0xffffff))
.rounded_lg(px(8.0))
.shadow_md()  // 使用预设中等阴影
.flex().items_center().justify_center()
.child("预设阴影");

// 使用自定义阴影
let custom_shadow = BoxShadow {
color:         rgba(0x000000, 0.15),
offset:        Point::new(px(0.0), px(4.0)),
blur_radius:   px(12.0),
spread_radius: px(0.0),
};

let with_custom = div()
.w(px(200.0))
.h(px(120.0))
.bg(rgb(0xffffff))
.rounded_lg(px(8.0))
.shadow(custom_shadow)  // 应用自定义阴影
.flex().items_center().justify_center()
.child("自定义阴影");

// 多个阴影(数组)
let multi_shadow = BoxShadow {
color:        rgba(0x7c3aed, 0.3),
offset:       Point::new(px(0.0), px(0.0)),
blur_radius:  px(20.0),
spread_radius: px(2.0),
};

let with_multi = div()
.w(px(200.0))
.h(px(120.0))
.bg(rgb(0x1e1e1e))
.rounded_lg(px(8.0))
.shadows(vec![BoxShadow { /* ... */ }, multi_shadow])
.flex().items_center().justify_center()
.child("多层阴影");

div()
.flex().flex_wrap()
.gap(px(24.0))
.p(px(32.0))
.children(vec![with_preset, with_custom, with_multi])
}
}

⚠️ 注意: 阴影性能提示

模糊阴影(blur_radius 较大时)需要进行 GPU 高斯模糊计算,在大量元素上同时使用大阴影会影响帧率。建议:卡片列表中的阴影使用 .shadow_sm() 或 .shadow_md(),仅对重点元素使用 .shadow_lg() 及以上。

透明度

.opacity() 修饰符控制整个元素的透明度(包括其子元素)。 取值范围为 0.0(完全透明)到 1.0(完全不透明)。

opacity.rs --- 透明度示例

rust 复制代码
use gpui::*;

struct OpacityDemo;

impl Render for OpacityDemo {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex().flex_col().gap(px(12.0)).p(px(24.0))
.children(vec![
// 完全不透明
div()
.w(px(200.0)).h(px(40.0))
.bg(rgb(0x7c3aed))
.opacity(1.0)
.flex().items_center().justify_center()
.child("opacity: 1.0(完全不透明)"),

// 50% 透明
div()
.w(px(200.0)).h(px(40.0))
.bg(rgb(0x7c3aed))
.opacity(0.5)
.flex().items_center().justify_center()
.child("opacity: 0.5(半透明)"),

// 20% 透明(几乎不可见)
div()
.w(px(200.0)).h(px(40.0))
.bg(rgb(0x7c3aed))
.opacity(0.2)
.flex().items_center().justify_center()
.child("opacity: 0.2(几乎透明)"),
])
}
}

与 rgba() 的区别:

  • rgba(0x000, a) 只影响背景色的透明度,文字和子元素不受影响
  • .opacity(a) 影响整个元素及其所有子元素的透明度

溢出控制

当元素内容超出其限定尺寸时,溢出控制决定如何显示多余内容。

溢出控制 API

rust 复制代码
// 完全隐藏溢出内容
.overflow_hidden()         // overflow: hidden(x 和 y 方向)

// 滚动
.overflow_scroll()          // overflow: scroll(x 和 y 都显示滚动条)
.overflow_x_scroll()        // overflow-x: scroll
.overflow_y_scroll()        // overflow-y: scroll

// 自动(内容溢出时才显示滚动条)
.overflow_auto()            // overflow: auto
.overflow_y_auto()          // overflow-y: auto

// 示例:固定高度的可滚动列表
div()
.h(px(300.0))
.overflow_y_scroll()  // 内容超过 300px 时出现纵向滚动条
.children(/* 列表项 */)

💡 提示: overflow_hidden 的常见用途

.overflow_hidden() 不仅用于隐藏溢出内容,还常用于裁剪圆角内的子元素。当一个元素设置了 .rounded_lg() 但子元素有背景色时,需要给父元素加 .overflow_hidden() 才能让子元素不溢出圆角。

样式方法速查表

类别 方法 说明
颜色函数 rgb() rgba() hsla() 创建 Rgba 颜色值
背景 .bg(color) 设置背景颜色
文字颜色 .text_color(color) 设置文字颜色
边框 .border() .border_color() 边框宽度与颜色
圆角 .rounded() .rounded_lg() .rounded_full() 边框圆角
阴影 .shadow_md() .shadow(color, offset, blur, spread) 盒子阴影
透明度 .opacity(f32) 元素整体透明度
溢出 .overflow_hidden() .overflow_y_scroll() 溢出控制

05 · 文字与排版

文字是 GUI 应用最基础的元素。GPUI 提供了丰富的文本渲染能力,从简单的样式设置到高级的文本布局与字体管理,本章将全面介绍 GPUI 的文字与排版系统。

基础文字属性

GPUI 提供了一组简洁的修饰符来控制文本的外观:

修饰符 说明 示例
.text_size() 设置字号 .text_size(px(14.0))
.text_color() 设置文字颜色 .text_color(Color::blue())
.font_weight() 设置字重 .font_weight(FontWeight::BOLD)
.font_style() 设置字体样式 .font_style(FontStyle::Italic)

以下示例展示了如何创建带样式的文本元素:

text.rs

rust 复制代码
use gpui::*;

fn render_styled_text(cx: &mut App) ->  impl Element {
div()
.child(
Label::new("大号加粗文字")
.text_size(px(24.0))
.font_weight(FontWeight::BOLD)
.text_color(Color::from_hex("#7c3aed"))
)
.child(
Label::new("斜体文字")
.font_style(FontStyle::Italic)
.text_size(px(16.0))
)
.child(
Label::new("普通文字")
.text_size(px(14.0))
.text_color(rgb(0x666666))
)
}

文本溢出处理

当文本超出容器边界时,GPUI 提供了多种溢出处理方式:

方法 说明
text_ellipsis() 单行省略,超出部分显示为 ...
line_clamp(n) 多行省略,限制最多显示 n 行
truncate() 截断文本,不显示省略号
whitespace_nowrap() 禁止换行,强制单行显示

以下示例来自 text_wrapper.rs,展示了各种文本溢出处理方式:

text_wrapper.rs

rust 复制代码
use gpui::*;

fn render_overflow_examples(cx: &mut App) ->  impl Element {
div()
.size_full()
.flex_col()
.gap(px(12.0))
.p(px(16.0))
// 单行省略
.child(
div()
.w(px(200.0))
.text_ellipsis()
.child("这是一段很长的文本,超出容器宽度后会被省略号截断")
)
// 多行省略(最多2行)
.child(
div()
.w(px(200.0))
.line_clamp(2)
.child("这是一段很长的文本内容,"
"当容器宽度有限且设置了行数限制时,"
"超出行数的部分将被省略号替换。")
)
// 强制不换行
.child(
div()
.w(px(200.0))
.whitespace_nowrap()
.child("这段文字不会换行,超出部分溢出")
)
}

文本布局

对于更复杂的文本渲染需求,GPUI 提供了底层的文本布局 API。text_layout.rs 展示了高级文本渲染能力,包括 ShapedLine(形状化行)和 TextRun(文本运行)等核心概念。

text_layout.rs

rust 复制代码
use gpui::*;

/// ShapedLine 表示一行已经完成形状化处理的文本
/// 包含字距调整、连字等排版信息
pub struct ShapedLine {
/// 该行的所有文本片段
runs: Vec<TextRun>,
/// 行宽度
width: f32,
/// 基线偏移
baseline: f32,
}

/// TextRun 表示一段具有相同样式的连续文本
pub struct TextRun {
/// 文本内容
text: SharedString,
/// 字体样式
font: Font,
/// 字号
font_size: Pixels,
/// 颜色
color: Hsla,
/// 起始偏移
offset: f32,
}

这些底层 API 通常在编辑器、终端模拟器等需要精确控制文本布局的场景中使用。一般应用场景直接使用 Label 即可满足需求。

自定义字体

GPUI 允许通过文本系统加载自定义字体:

text.rs

rust 复制代码
use gpui::*;

fn load_custom_fonts(cx: &mut App) {
// 从文件加载自定义字体
cx.text_system().add_fonts([FontSource::File(
PathBuf::from("assets/fonts/CustomFont.ttf"),
)]);

// 加载多个字体文件
cx.text_system().add_fonts([
FontSource::File(PathBuf::from("assets/fonts/Regular.woff2")),
FontSource::File(PathBuf::from("assets/fonts/Bold.woff2")),
FontSource::File(PathBuf::from("assets/fonts/Italic.woff2")),
]);
}

fn use_custom_font(cx: &mut App) ->  impl Element {
div()
.child(
Label::new("使用自定义字体")
.font("CustomFont")
.text_size(px(20.0))
)
.child(
Label::new("Bold CustomFont")
.font("CustomFont")
.font_weight(FontWeight::BOLD)
)
}

字符网格

CharacterGrid 是一个展示字体渲染能力的组件,它在网格中渲染单个字符,常用于字体预览和调试:

text.rs

rust 复制代码
use gpui::*;

/// CharacterGrid 在网格中展示字符渲染效果
pub struct CharacterGrid {
/// 要展示的字符列表
characters: Vec<char>,
/// 网格列数
columns: usize,
/// 单元格大小
cell_size: Pixels,
/// 字体
font: Font,
}

impl Render for CharacterGrid {
fn render(&mut self, cx: &mut ViewContext<Self>) ->  impl Element {
div()
.flex()
.flex_wrap()
.gap(px(4.0))
.map(|this| {
self.characters.iter().fold(this, |el, &ch| {
el.child(
div()
.size(self.cell_size)
.border(px(1.0))
.border_color(rgb(0xdddddd))
.flex()
.items_center()
.justify_center()
.child(
Label::new(ch.to_string())
.font(self.font.clone())
)
)
})
})
}
}

💡 提示: SharedString 优化

在 GPUI 中,Label::new() 接受 SharedString 而非普通 &str。SharedString 是引用计数的不可变字符串,在组件树中传递时可以避免克隆开销。普通字符串会被自动转换为 SharedString,但在高频更新场景中,直接使用 SharedString 可以减少内存分配。


06 · 图像与媒体

图像是丰富界面表达的重要元素。GPUI 提供了对静态图片、SVG、GIF 动图等多种媒体格式的支持,并通过 AssetSource trait 提供灵活的资源加载机制。

静态图片

使用 img() 函数加载静态图片,通过 source 参数指定图片路径或 URL:

image_loading.rs

rust 复制代码
use gpui::*;

fn render_images(cx: &mut App) ->  impl Element {
div()
.flex()
.gap(px(16.0))
.p(px(16.0))
// 从文件路径加载
.child(
img("assets/logo.png")
.size(px(100.0))
.rounded(px(8.0))
)
// 设置宽高
.child(
img("assets/banner.jpg")
.w(px(200.0))
.h(px(150.0))
)
// 从 URL 加载
.child(
img("https://example.com/image.png")
.size(px(80.0))
.rounded_full()
)
}

SVG 图像

GPUI 支持通过 svg() 函数渲染 SVG 图像,支持内联 SVG 和外部 SVG 文件:

image_loading.rs

rust 复制代码
use gpui::*;

fn render_svg(cx: &mut App) ->  impl Element {
div()
.flex()
.gap(px(16.0))
// 从文件加载外部 SVG
.child(
svg()
.path("assets/icons/settings.svg")
.size(px(24.0))
.text_color(Color::from_hex("#333333"))
)
// 内联 SVG(使用 svg() 构建)
.child(
svg()
.with_contents("<svg viewBox=\"0 0 24 24\" "
"xmlns=\"http://www.w3.org/2000/svg\">"
"<circle cx=\"12\" cy=\"12\" r=\"10\" "
"fill=\"#7c3aed\"/></svg>")
.size(px(32.0))
)
}

GIF 动图

GPUI 原生支持 GIF 动图渲染。gif_viewer.rs 展示了如何在应用中显示 GIF 动画:

gif_viewer.rs

rust 复制代码
use gpui::*;

pub struct GifViewer {
/// GIF 图片源
source: String,
}

impl GifViewer {
pub fn new(source: SharedString) -> Self {
Self {
source: source.into(),
}
}
}

impl Render for GifViewer {
fn render(&mut self, _cx: &mut ViewContext<Self>) ->  impl Element {
// GPUI 会自动处理 GIF 帧动画
img(self.source.as_str())
.w(px(300.0))
.rounded(px(8.0))
}
}

AssetSource trait

AssetSource trait 允许自定义资源加载策略,例如从文件系统、内存或网络加载资源。以下示例来自 animation.rs,展示了如何实现自定义的 AssetSource

animation.rs

rust 复制代码
use gpui::{App, AssetSource, SharedString};
use std::path::PathBuf;

/// 从文件系统加载资源的 AssetSource 实现
pub struct FsAssetSource {
/// 资源根目录
root: PathBuf,
}

impl FsAssetSource {
pub fn new(root: PathBuf) -> Self {
Self { root }
}
}

impl AssetSource for FsAssetSource {
fn load(&self, path: &str, cx: &mut App) -> Option<SharedString> {
let full_path = self.root.join(path);
std::fs::read_to_string(&full_path).ok().map(SharedString::from)
}
}

图片画廊

image_gallery.rs 展示了如何使用网格布局创建缩略图画廊:

image_gallery.rs

rust 复制代码
use gpui::*;

pub struct ImageGallery {
images: Vec<SharedString>,
}

impl Render for ImageGallery {
fn render(&mut self, cx: &mut ViewContext<Self>) ->  impl Element {
div()
.size_full()
.overflow_y_scroll()
.p(px(16.0))
.flex()
.flex_wrap()
.gap(px(8.0))
.map(|this| {
self.images.iter().fold(this, |el, path| {
el.child(
img(path.as_ref())
.w(px(150.0))
.h(px(150.0))
.rounded(px(8.0))
.object_fit(ObjectFit::Cover)
)
})
})
}
}

图片缓存

GPUI 内部使用 ImageCache 来管理已加载的图片,避免重复解码。图片加载是异步的,首次加载后会被缓存,后续使用同一路径加载时直接从缓存读取。

特性 说明
自动缓存 通过 img() 加载的图片会自动缓存
异步加载 大图片异步解码,不阻塞 UI 线程
内存管理 未使用的缓存图片会被自动回收

ObjectFit:图片适配模式

ObjectFit 控制图片在容器内的适配方式:

模式 说明
ObjectFit::Cover 等比缩放并裁剪,填满容器(可能有裁剪)
ObjectFit::Contain 等比缩放,完整显示图片(可能有留白)
ObjectFit::Fill 拉伸填满容器(可能变形)
ObjectFit::ScaleDown 缩小到可容纳范围内(不放大)

image_loading.rs

rust 复制代码
// Cover 模式:填满容器,超出部分裁剪
img("photo.jpg")
.w(px(200.0))
.h(px(200.0))
.object_fit(ObjectFit::Cover)

// Contain 模式:完整显示,保持比例
img("photo.jpg")
.w(px(200.0))
.h(px(200.0))
.object_fit(ObjectFit::Contain)

⚠️ 注意: 大图加载性能注意事项

加载大尺寸图片时需要注意以下几点:

  • 异步解码:GPUI 会异步解码图片,但超大图片(>10MB)仍可能导致内存峰值。
  • 缩略图优先:在列表或网格中显示时,优先使用缩略图而非原图。
  • 延迟加载:对于不可见区域的图片,考虑延迟加载以减少启动时的内存开销。
  • GIF 优化:GIF 动图会持续占用内存和 CPU 资源,避免在同一视图中加载过多 GIF。

07 · 事件处理

事件处理是构建交互式界面的核心。GPUI 提供了完整的事件系统,涵盖鼠标、键盘等交互,并支持事件传播控制。本章将介绍 GPUI 中各种事件的监听与处理方式。

点击事件

使用 .on_click() 修饰符来监听点击事件,通常搭配 cx.listener() 创建回调:

painting.rs

rust 复制代码
use gpui::*;

pub struct Counter {
count: i32,
}

impl Counter {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self { count: 0 }
}
}

impl Render for Counter {
fn render(&mut self, cx: &mut ViewContext<Self>) ->  impl Element {
div()
.flex()
.gap(px(8.0))
.child(
div()
.child(format!("计数: {}", self.count))
)
.child(
div()
.child("+1")
.px(px(12.0))
.py(px(6.0))
.bg(rgb(0x7c3aed))
.text_color(rgb(0xffffff))
.rounded(px(6.0))
.cursor_pointer()
.on_click(cx.listener(|self, _event, cx| {
self.count += 1;
cx.notify();
}))
)
}
}

鼠标事件

GPUI 提供了完整的鼠标事件 API:

事件 说明
.on_mouse_down() 鼠标按下
.on_mouse_up() 鼠标释放
.on_mouse_move() 鼠标移动
.on_mouse_enter() 鼠标进入元素区域
.on_mouse_leave() 鼠标离开元素区域
.on_scroll_wheel() 滚轮滚动

painting.rs

rust 复制代码
use gpui::*;

pub struct MouseTracker {
position: Point<Pixels>,
}

impl Render for MouseTracker {
fn render(&mut self, cx: &mut ViewContext<Self>) ->  impl Element {
div()
.size_full()
.bg(rgb(0x1a1a2e))
.relative()
.on_mouse_move(cx.listener(|self, event, cx| {
// event.position 包含鼠标当前位置
self.position = event.position;
cx.notify();
}))
.child(
div()
.absolute()
.top(px(0.0))
.left(px(0.0))
.w(px(8.0))
.h(px(8.0))
.rounded_full()
.bg(rgb(0xff6b6b))
.ml(self.position.x.0)
.mt(self.position.y.0)
)
.child(
div()
.child(format!(
"鼠标位置: ({:.0}, {:.0})",
self.position.x.0,
self.position.y.0
))
)
}
}

悬停效果

使用 .on_hover()hoverable() 修饰符来创建悬停效果:

mouse_pressure.rs

rust 复制代码
use gpui::*;

pub struct HoverButton {
hovered: bool,
}

impl Render for HoverButton {
fn render(&mut self, cx: &mut ViewContext<Self>) ->  impl Element {
let bg_color = if self.hovered {
rgb(0x9333ea)  // 悬停时更深的紫色
} else {
rgb(0x7c3aed)  // 默认紫色
};

div()
.px(px(16.0))
.py(px(8.0))
.bg(bg_color)
.text_color(rgb(0xffffff))
.rounded(px(8.0))
.cursor_pointer()
.hoverable()  // 启用悬停检测
.on_hover(cx.listener(|self, hovered, cx| {
self.hovered = hovered.hovered();
cx.notify();
}))
.child("悬停我试试")
}
}

鼠标按下外部检测

.on_mouse_down_out() 用于检测鼠标在元素外部按下,常用于关闭弹出层、下拉菜单等场景:

mouse_pressure.rs

rust 复制代码
use gpui::*;

pub struct Popover {
open: bool,
}

impl Render for Popover {
fn render(&mut self, cx: &mut ViewContext<Self>) ->  impl Element {
div()
.relative()
.on_mouse_down_out(cx.listener(|self, _event, cx| {
// 点击弹出层外部时关闭
self.open = false;
cx.notify();
}))
.child(
div()
.child(if self.open { "关闭" } else { "打开" })
.on_click(cx.listener(|self, _event, cx| {
self.open = !self.open;
cx.notify();
}))
)
}
}

事件传播

GPUI 的事件系统支持冒泡传播。默认情况下,事件会从目标元素向上冒泡到父元素。调用 cx.stop_propagation() 可以阻止事件继续向上传播:

painting.rs

rust 复制代码
div()
.// 外层容器
.on_click(cx.listener(|self, _event, _cx| {
println!("外层容器被点击");
}))
.child(
div()
// 内层按钮:阻止事件冒泡
.on_click(cx.listener(|self, event, _cx| {
println!("按钮被点击(事件不会冒泡)");
event.stop_propagation();
}))
.child("点击我")
)

鼠标压力

mouse_pressure.rs 展示了高级鼠标事件处理,包括检测鼠标压力级别(适用于压感设备如数位板):

mouse_pressure.rs

rust 复制代码
use gpui::*;

pub struct PressureCanvas {
pressure: f32,
}

impl Render for PressureCanvas {
fn render(&mut self, cx: &mut ViewContext<Self>) ->  impl Element {
// 根据压力值调整圆点大小
let size = px(4.0 + self.pressure * px(40.0).0);

div()
.size_full()
.bg(rgb(0x0f0f23))
.on_mouse_down(cx.listener(|self, event, cx| {
// event.pressure 包含压力值 (0.0 ~ 1.0)
self.pressure = event.pressure.unwrap_or(0.5);
cx.notify();
}))
.child(
div()
.absolute()
.top(px(50.0))
.left(px(50.0))
.size(size)
.rounded_full()
.bg(rgba(0x7c3aed, self.pressure))
)
}
}

💡 提示: cx.listener vs 闭包的选择

cx.listener() 创建的回调带有类型信息,可以正确处理组件的 &mut self。在组件内部的事件处理器中,始终使用 cx.listener() 而非普通闭包。只有在不涉及组件状态修改的简单场景(如纯 UI 回调)中,才考虑使用闭包。cx.listener() 还会自动管理视图的生命周期,避免悬垂引用。


08 · 焦点管理

焦点管理是构建可访问 GUI 应用的关键。GPUI 提供了完善的焦点系统,支持键盘导航、Tab 切换和程序化聚焦,本章将详细介绍 GPUI 的焦点管理 API。

FocusHandle:创建和管理焦点

FocusHandle 是 GPUI 焦点系统的核心,每个可聚焦的元素都通过 FocusHandle 来管理焦点状态:

方法 说明
cx.focus_handle() 为组件创建一个焦点句柄
window.focus(&handle) 将焦点设置到指定句柄
handle.is_focused(cx) 检查句柄是否持有焦点

focus_visible.rs

rust 复制代码
use gpui::*;

pub struct FocusableInput {
/// 焦点句柄
focus_handle: FocusHandle,
text: String,
}

impl FocusableInput {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
// 创建焦点句柄
focus_handle: cx.focus_handle(),
text: String::new(),
}
}
}

impl Render for FocusableInput {
fn render(&mut self, cx: &mut ViewContext<Self>) ->  impl Element {
let is_focused = self.focus_handle.is_focused(cx);

div()
.id("focusable-input")
.track_focus(&self.focus_handle)
.on_click(cx.listener(|self, _event, cx| {
// 点击时聚焦到该组件
cx.focus(&self.focus_handle);
}))
.border(px(2.0))
.border_color(if is_focused {
rgb(0x7c3aed)  // 聚焦时高亮边框
} else {
rgb(0xcccccc)  // 默认边框
})
.rounded(px(8.0))
.p(px(12.0))
.child(format!("输入框内容: {}", self.text))
}
}

focus() vs focus_visible()

GPUI 区分两种焦点状态:

焦点类型 说明 场景
focus() 程序聚焦,不显示焦点指示器 通过代码调用 window.focus()
focus_visible() 键盘导航聚焦,显示焦点指示器 通过 Tab 键导航

这种区分对于无障碍访问至关重要------键盘用户需要看到焦点指示器来定位当前位置,而鼠标用户不需要。

focus_visible.rs

rust 复制代码
use gpui::*;

impl Render for FocusableInput {
fn render(&mut self, cx: &mut ViewContext<Self>) ->  impl Element {
// focus_visible() 仅在键盘导航时为 true
let is_focus_visible = self.focus_handle.is_focus_visible(cx);
let is_focused = self.focus_handle.is_focused(cx);

div()
.track_focus(&self.focus_handle)
.border_color(if is_focus_visible {
// 键盘导航:显示明显的焦点环
rgb(0x7c3aed)
} else if is_focused {
// 鼠标点击:显示轻微的焦点样式
rgb(0x9ca3af)
} else {
// 无焦点
rgb(0xe5e7eb)
})
.border_1()
.rounded(px(6.0))
.p(px(8.0))
.child("区分 focus 和 focus_visible")
}
}

Tab 导航

GPUI 内置支持 Tab 键导航,可以使用 window.focus_next()window.focus_prev() 来手动控制:

focus_visible.rs

rust 复制代码
use gpui::*;

pub struct Form {
name_handle: FocusHandle,
email_handle: FocusHandle,
submit_handle: FocusHandle,
}

impl Form {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
name_handle: cx.focus_handle(),
email_handle: cx.focus_handle(),
submit_handle: cx.focus_handle(),
}
}
}

impl Render for Form {
fn render(&mut self, cx: &mut ViewContext<Self>) ->  impl Element {
div()
.flex_col()
.gap(px(8.0))
.child(
div()
.track_focus(&self.name_handle)
.child("姓名")
)
.child(
div()
.track_focus(&self.email_handle)
.child("邮箱")
)
.child(
div()
.track_focus(&self.submit_handle)
.on_key_down(cx.listener(|self, event, cx| {
if event.keystroke == "enter" {
// 提交表单
println!("表单已提交");
}
}))
.child("提交")
)
}
}

tab_index 和 tab_stop

可以通过 tab_indextab_stop 自定义 Tab 导航的顺序和行为:

tab_stop.rs

rust 复制代码
use gpui::*;

pub struct CustomTabOrder {
handle_a: FocusHandle,
handle_b: FocusHandle,
handle_c: FocusHandle,
}

impl Render for CustomTabOrder {
fn render(&mut self, cx: &mut ViewContext<Self>) ->  impl Element {
div()
.flex()
.gap(px(8.0))
.child(
div()
.track_focus(&self.handle_a)
.tab_index(1)  // 第一个 Tab 停靠点
.child("A (tab_index=1)")
)
.child(
div()
.track_focus(&self.handle_b)
.tab_index(3)  // 第三个 Tab 停靠点
.child("B (tab_index=3)")
)
.child(
div()
.track_focus(&self.handle_c)
.tab_index(2)  // 第二个 Tab 停靠点
.child("C (tab_index=2)")
)
.child(
div()
// tab_stop(false) 跳过此元素的 Tab 导航
.tab_stop(false)
.child("不可 Tab 到达")
)
}
}

Tab 导航顺序按 tab_index 值从小到大排列,值相同时按 DOM 顺序。设置 tab_stop(false) 可以让元素从 Tab 导航中移除。

focus_within()

focus_within() 用于检测焦点是否在某个容器或其子元素上:

focus_visible.rs

rust 复制代码
use gpui::*;

pub struct FocusContainer {
focus_handle: FocusHandle,
}

impl Render for FocusContainer {
fn render(&mut self, cx: &mut ViewContext<Self>) ->  impl Element {
// focus_within() 在任意子元素获得焦点时为 true
let has_focus_within = self.focus_handle.focus_within(cx);

div()
.track_focus(&self.focus_handle)
.border_color(if has_focus_within {
rgb(0x7c3aed)
} else {
rgb(0xe5e7eb)
})
.border(px(2.0))
.rounded(px(8.0))
.p(px(16.0))
.child("当子元素获得焦点时,边框高亮")
}
}

💡 提示: focus_visible 用于无障碍访问

focus_visible 遵循 :focus-visible CSS 伪类的理念:只有在键盘操作(Tab、方向键等)触发焦点切换时才显示焦点指示器,而鼠标点击触发的聚焦则不显示。这避免了鼠标用户看到不必要的焦点环,同时保证键盘用户始终能看到当前位置,是无障碍设计的重要实践。


09 · 动作与快捷键

动作(Actions)是 GPUI 中解耦组件间通信的核心机制。通过动作系统,可以将键盘快捷键、菜单项和按钮点击统一关联到同一个处理逻辑。本章将介绍如何定义、绑定和分发动作。

actions! 宏

使用 actions! 宏定义动作类型。每个动作是一个独立的零大小类型(ZST),可携带任意数据:

input.rs

rust 复制代码
use gpui::*;

// 定义无参数动作
actions!(
Save,
Open,
Copy,
Paste,
Undo,
Redo,
);

// 定义带参数的动作
actions!(
OpenFile,
GoToLine,
);

// 带参数的动作自动生成 new() 构造函数
let action = OpenFile::new("src/main.rs".into());

actions! 宏会为每个动作生成以下内容:

  • 动作结构体本身(实现了 CloneDebug 等基础 trait)
  • new() 构造函数(仅带参数的动作)
  • 每个字段自动成为公开的 getter 方法

键盘绑定

使用 cx.bind_keys()KeyBinding::new() 将快捷键绑定到动作:

window.rs

rust 复制代码
use gpui::*;

fn bind_keybindings(cx: &mut App) {
// 绑定单个快捷键
cx.bind_keys([
KeyBinding::new("ctrl-s", Save::new(), None),
KeyBinding::new("ctrl-c", Copy::new(), None),
KeyBinding::new("ctrl-v", Paste::new(), None),
KeyBinding::new("ctrl-z", Undo::new(), None),
KeyBinding::new("ctrl-shift-z", Redo::new(), None),
]);

// 为特定平台绑定不同的快捷键
if cfg!(target_os = "macos") {
cx.bind_keys([
KeyBinding::new("cmd-s", Save::new(), None),
KeyBinding::new("cmd-c", Copy::new(), None),
]);
}
}

快捷键语法遵循以下格式:

修饰键 格式 说明
Ctrl ctrl- Control 键
Shift shift- Shift 键
Alt alt- Alt 键
Cmd cmd- Command 键(macOS)

监听动作

使用 .on_action() 在组件内监听动作。动作会沿着视图树向上冒泡,直到被处理:

input.rs

rust 复制代码
use gpui::*;

pub struct Editor {
content: String,
}

impl Render for Editor {
fn render(&mut self, cx: &mut ViewContext<Self>) ->  impl Element {
div()
.size_full()
// 监听 Save 动作
.on_action(cx.listener(|self: &mut Editor, action: &Save, cx| {
println!("保存文件: {}", self.content);
}))
// 监听 Copy 动作
.on_action(cx.listener(|self: &mut Editor, _action: &Copy, cx| {
cx.copy_to_clipboard(self.content.clone());
}))
// 监听 Undo 动作
.on_action(cx.listener(|self: &mut Editor, _action: &Undo, cx| {
// 执行撤销逻辑
}))
.child(Label::new(&self.content))
}
}

全局动作分发

使用 cx.dispatch_action() 可以手动分发动作到当前焦点元素,动作会沿着视图树向上冒泡:

window.rs

rust 复制代码
use gpui::*;

// 从代码触发 Save 动作(等同于按下 Ctrl+S)
cx.dispatch_action(Save::new());

// 触发带参数的动作
cx.dispatch_action(GoToLine::new(42));

// 在特定窗口中分发动作
window.dispatch_action(Save::new(), cx);

dispatch_action() 的分发机制:

  • 动作从当前焦点元素开始
  • 沿着视图树向上冒泡到父元素
  • 第一个处理该动作的 .on_action() 会消费它
  • 未被处理的动作会被丢弃

菜单栏

GPUI 支持原生菜单栏,通过 cx.set_menus() 配置菜单项,每个菜单项可以关联到动作:

set_menus.rs

rust 复制代码
use gpui::*;

fn setup_menu_bar(cx: &mut App) {
cx.set_menus([
Menu::new("文件", Menu::items(cx, [
// 关联到 Open 动作,显示快捷键提示
MenuItem::action("打开", Open::new(), Some("ctrl-o")),
MenuItem::action("保存", Save::new(), Some("ctrl-s")),
MenuItem::separator(),
MenuItem::action("退出", Quit::new(), Some("ctrl-q")),
])),
Menu::new("编辑", Menu::items(cx, [
MenuItem::action("撤销", Undo::new(), Some("ctrl-z")),
MenuItem::action("重做", Redo::new(), Some("ctrl-shift-z")),
MenuItem::separator(),
MenuItem::action("复制", Copy::new(), Some("ctrl-c")),
MenuItem::action("粘贴", Paste::new(), Some("ctrl-v")),
])),
Menu::new("帮助", Menu::items(cx, [
MenuItem::action("关于", About::new(), None),
])),
]);
}

菜单项相关 API:

方法 说明
MenuItem::action() 关联动作的菜单项
MenuItem::separator() 菜单分隔线
MenuItem::checkbox() 可勾选的菜单项
MenuItem::custom() 自定义渲染的菜单项

window.prompt():系统对话框

GPUI 提供了系统级别的对话框 API,用于与用户进行简单交互:

window.rs

rust 复制代码
use gpui::*;

// 显示确认对话框
let answer = window.prompt(
PromptLevel::Warning,
"未保存的更改",
&["保存", "不保存", "取消"],
cx,
);

// answer 是用户选择按钮的索引
match answer {
0 => { // "保存"
cx.dispatch_action(Save::new());
}
1 => { // "不保存"
println!("放弃更改");
}
_ => { // "取消" 或关闭
println!("操作已取消");
}
}

PromptLevel 控制对话框的视觉风格:

级别 说明
PromptLevel::Info 信息提示(默认)
PromptLevel::Warning 警告提示
PromptLevel::Critical 严重错误提示
动作与事件的选择策略

在 GPUI 中,动作(Actions)和事件(Events)有不同的适用场景:

  • 使用动作:当交互逻辑需要在多个组件间共享时(如复制、粘贴、保存),或需要与快捷键、菜单栏关联时。动作会沿视图树冒泡,父组件可以统一处理。
  • 使用事件:当交互逻辑是组件私有的,仅在当前元素上处理时(如按钮点击、悬停效果)。事件不会冒泡(除非手动停止传播)。

简而言之:跨组件通信用动作,组件内部交互用事件


高级

第10章 文本输入

来源示例:input.rs

概述

GPUI 没有内置的文本输入组件,需要开发者自行实现。本章将介绍如何基于 EntityInputHandler trait 和自定义 Element 构建完整的文本输入功能。

EntityInputHandler trait

EntityInputHandler 是 GPUI 处理键盘输入和 IME(输入法)的核心 trait。实现该 trait 需要提供以下方法:

方法 说明
text_for_range() 获取指定范围的文本内容
selected_text_range() 返回当前选中的文本范围
mark_text() 标记 IME 复合文本(输入法中间状态)
unmark_text() 取消 IME 复合文本标记
replace_text_in_range() 替换指定范围内的文本

TextInput 结构体实现 EntityInputHandler

以下示例展示如何为自定义的 TextInput 结构体实现 EntityInputHandler:

text_input.rs

rust 复制代码
pub struct TextInput {
pub text: SharedString,
pub placeholder: SharedString,
pub selected_range: Range<usize>,
pub marked_range: Option<Range<usize>>,
focus_handle: FocusHandle,
}

impl EntityInputHandler for TextInput {
fn text_for_range(&mut self, range: Range<usize>, cx: &mut AppContext) -> Option<SharedString> {
if range.end <= self.text.len() {
Some(self.text[range].into())
} else {
None
}
}

fn selected_text_range(&mut self, cx: &mut AppContext) -> Option<Range<usize>> {
Some(self.selected_range.clone())
}

fn mark_text(&mut self, range: Range<usize>, cx: &mut AppContext) {
self.marked_range = Some(range);
cx.notify();
}

fn unmark_text(&mut self, cx: &mut AppContext) {
self.marked_range = None;
cx.notify();
}

fn replace_text_in_range(&mut self, range: Option<Range<usize>>, text: &str, cx: &mut AppContext) {
let range = range.unwrap_or(self.selected_range.clone());
self.text = self.text[..range.start].to_string() + text + &self.text[range.end..];
self.selected_range = range.start + text.len()..range.start + text.len();
cx.notify();
}
}

自定义 Element 三阶段渲染

GPUI 的渲染流程分为三个阶段,每个阶段有明确职责:

  • request_layout():计算元素所需的尺寸
  • prepaint():计算元素在窗口中的最终位置
  • paint():将内容绘制到屏幕上

TextElement 的 request_layout / prepaint / paint 实现

text_element.rs

rust 复制代码
pub struct TextElement {
pub entity: Entity<TextInput>,
}

impl Element for TextElement {
type RequestLayoutState = ();
type PrepaintState = Option<Hitbox>;

fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, ()) {
let mut style = Style::default();
style.size.width = relative(1.0).into();
style.size.height = relative(1.0).into();
(cx.request_layout(style, []), ())
}

fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
request_layout_state: &mut (),
cx: &mut WindowContext,
) -> Option<Hitbox> {
let hitbox = cx.insert_hitbox(bounds, false);
Some(hitbox)
}

fn paint(
&mut self,
id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
request_layout_state: &mut (),
prepaint_state: &mut Option<Hitbox>,
cx: &mut WindowContext,
) {
let input = self.entity.read(cx);
// 绘制文本内容、光标和选区
let display_text = if input.text.is_empty() {
input.placeholder.clone()
} else {
input.text.clone()
};
cx.paint_text(bounds, &display_text, input.text_style.clone());
}
}

注册输入处理器

要使文本输入生效,需要在窗口创建时将 Entity 注册为输入处理器:

main.rs

rust 复制代码
let text_input = cx.new(|cx| TextInput {
text: "".into(),
placeholder: "输入文本...".into(),
selected_range: 0..0,
marked_range: None,
focus_handle: cx.focus_handle(),
});

cx.on_window_opened(|workspace, cx| {
workspace
.handle_input(cx.entity().downgrade())
.detach();
});

焦点与光标

FocusHandle 是 GPUI 中管理输入焦点的核心类型,包含以下关键概念:

  • FocusHandle :标识可聚焦元素,通过 cx.focus_handle() 创建
  • 光标位置 :由 selected_range 的起始位置决定
  • 选区selected_range 表示当前选中的文本范围,当 start == end 时表示光标

剪贴板操作

GPUI 提供了剪贴板读写能力:

clipboard.rs

rust 复制代码
// 读取剪贴板
let text = cx.clipboard_read().unwrap_or_default();

// 写入剪贴板
cx.clipboard_write("复制的内容");

鼠标选择文本的实现

通过监听鼠标事件来更新文本选区:

text_input.rs

rust 复制代码
impl TextInput {
fn handle_mouse_down(&mut self, position: Point<Pixels>, cx: &mut ViewContext<Self>) {
let char_index = self.pixel_to_char_index(position);
self.selected_range = char_index..char_index;
cx.notify();
}

fn handle_mouse_drag(&mut self, position: Point<Pixels>, cx: &mut ViewContext<Self>) {
let end = self.pixel_to_char_index(position);
self.selected_range = self.selected_range.start..end;
cx.notify();
}
}

⚠️ 注意: 自定义文本输入的复杂性和注意事项

  • 中文、日文、韩文(CJK)输入法需要正确处理 IME mark/unmark 流程
  • Emoji 组合字符和多字节字符需要精确的索引管理
  • 光标位置与字体渲染的对齐需要精确计算
  • 建议参考 GPUI 官方示例 input.rs 作为实现模板
  • 文本渲染性能优化:长文本需考虑虚拟滚动或截断策略

中级

第11章 拖拽功能

来源示例:drag_drop.rs

拖拽源:.on_drag() 修饰符

.on_drag() 方法用于将元素标记为可拖拽源。当用户在该元素上按下鼠标并移动时,GPUI 会启动拖拽流程。

设置可拖拽元素

drag_source.rs

rust 复制代码
div()
.child("拖拽我")
.on_drag(
cx.listener(|view, drag_event, cx| {
let drag_data = DragInfo {
text: view.data.read(cx).clone(),
offset: drag_event.position,
};
cx.start_drag(drag_data);
})
)

放置目标:.on_drop() 修饰符

.on_drop() 方法用于将元素标记为可接受拖放的放置目标。当拖拽的数据被释放到该区域时,回调函数将被触发。

设置放置目标

drop_target.rs

rust 复制代码
div()
.child("放在这里")
.on_drop(
cx.listener(|view, drop_event, cx| {
if let Some(data) = drop_event.data.downcast_ref::<DragInfo>() {
view.data.update(cx, |state, cx| {
state.items.push(data.text.clone());
cx.notify();
});
}
})
)

DragInfo:拖拽数据载体

DragInfo 是承载拖拽数据的结构体。它通过实现 Render trait 来提供拖拽时的视觉预览效果。

DragInfo 结构体和 Render 实现

drag_info.rs

rust 复制代码
pub struct DragInfo {
pub text: SharedString,
pub offset: Point<Pixels>,
}

impl Render for DragInfo {
fn render(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.px_2()
.py_1()
.bg(gpui::black())
.text_color(gpui::white())
.rounded_md()
.child(self.text.clone())
}
}

drag_over 样式:拖拽悬停时的视觉反馈

当拖拽内容悬停在放置目标上时,可以设置特殊的视觉样式来提供反馈:

drag_over.rs

rust 复制代码
div()
.child("拖放到这里")
.on_drop(cx.listener(DropTarget::handle_drop))
.on_drag_over::<DragInfo>(|style, data, bounds, cx| {
style.bg(gpui::blue())
.border_color(gpui::cyan())
})

drag_drop.rs 完整示例

drag_drop.rs

rust 复制代码
use gpui::*;

struct DragDrop {
left_items: Vec<SharedString>,
right_items: Vec<SharedString>,
}

impl Render for DragDrop {
fn render(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.size_full()
.children([
// 左侧列表 - 拖拽源
div()
.w_1_2()
.p_4()
.border_r_1()
.children(self.left_items.iter().map(|item| {
div()
.p_2()
.mb_2()
.bg(gpui::gray(7))
.rounded_md()
.child(item.clone())
.on_drag(cx.listener(move |view, e, cx| {
cx.start_drag(DragInfo {
text: item.clone(),
offset: e.position,
});
}))
})),
// 右侧列表 - 放置目标
div()
.w_1_2()
.p_4()
.on_drop(cx.listener(|view, e, cx| {
if let Some(info) = e.data.downcast_ref::<DragInfo>() {
view.right_items.push(info.text.clone());
cx.notify();
}
}))
.children(self.right_items.iter().map(|item| {
div()
.p_2()
.mb_2()
.bg(gpui::gray(7))
.rounded_md()
.child(item.clone())
})),
])
}
}

拖拽生命周期

GPUI 中的拖拽操作遵循以下生命周期:

  1. 开始拖拽 :用户按下鼠标并移动 → on_drag 触发 → 调用 cx.start_drag()
  2. 拖拽移动 :鼠标移动 → GPUI 渲染 DragInfo 的预览跟随光标
  3. 进入目标 :光标进入放置目标区域 → on_drag_over 触发 → 应用视觉反馈
  4. 离开目标:光标离开放置目标区域 → 视觉反馈恢复
  5. 放置 :用户释放鼠标 → on_drop 触发 → 处理拖拽数据
  6. 取消:用户按 Escape 键或释放鼠标在无效区域 → 拖拽取消,数据丢弃

💡 提示: 跨窗口拖拽的支持

GPUI 支持同一应用内不同窗口之间的拖拽操作。由于 DragInfo 数据在进程内部传递,跨窗口拖拽的数据共享是天然支持的。对于跨应用拖拽(如文件拖放),需要依赖平台原生支持,GPUI 提供了相应的扩展接口。


核心

第12章 Entity 状态管理

来源示例:ownership_post.rs

Entity:GPUI 的核心状态容器

Entity 是 GPUI 中管理状态的基石。它提供了一种类型安全的方式来创建、读取和更新应用状态。

操作 方法 说明
创建 cx.new( cx
读取 entity.read(cx) 不可变地读取 Entity 中的数据
更新 entity.update(cx, data, cx

创建和更新 Entity

entity_example.rs

rust 复制代码
// 定义数据结构
pub struct Counter {
pub count: i32,
}

// 创建 Entity
let entity: Entity<Counter> = cx.new(|cx| Counter {
count: 0,
});

// 读取数据
let current_count = entity.read(cx).count;

// 更新数据
entity.update(cx, |counter, cx| {
counter.count += 1;
cx.notify(); // 通知 GPUI 重新渲染
});

cx.observe():观察状态变化

cx.observe() 用于监听另一个 Entity 的变化。当被观察的 Entity 调用 cx.notify() 时,观察者的回调函数将被触发。

监听 Entity 变化触发回调

observe.rs

rust 复制代码
pub struct Parent {
child: Entity<Child>,
child_data: String,
}

impl Parent {
fn init(cx: &mut WindowContext) -> Entity<Self> {
let child = cx.new(|cx| Child { value: "初始值".into() });
cx.new(|cx| {
let child_handle = child.clone();
cx.observe(&child, |parent: &mut Parent, child_entity, cx| {
// 当 Child Entity 发生变化时,同步更新 Parent 的状态
parent.child_data = child_entity.read(cx).value.clone();
cx.notify();
}).detach();
Parent { child, child_data: "".into() }
})
}
}

cx.notify():通知重新渲染

cx.notify() 是 GPUI 响应式系统的核心机制。当 Entity 的状态发生变化时,调用此方法通知框架重新渲染受影响的视图。它触发了以下链式反应:

  • 当前 Entity 的所有观察者收到通知
  • 观察者决定是否需要更新自身状态
  • 框架重新构建受影响的 Element 树
  • 仅发生变化的区域被重新绘制(增量渲染)

cx.listener():类型安全的回调

cx.listener() 创建带有 Entity 上下文的类型安全回调。与普通闭包不同,listener 自动携带 View 的引用,确保回调始终在正确的 Entity 上下文中执行。

使用 listener 创建带 Entity 上下文的回调

listener.rs

rust 复制代码
div()
.child("点击我")
.on_click(cx.listener(|button: &mut Button, event: &ClickEvent, cx| {
// button 是 Button Entity 的可变引用
// event 包含点击事件的详细信息
// cx 提供 ViewContext 的所有能力
button.click_count += 1;
cx.notify();
}))

所有权模型

GPUI 基于 Rust 的所有权系统设计了自己的状态管理模型,确保内存安全和线程安全。

GPUI 的所有权规则

  • 每个 Entity 持有 T 的唯一所有权
  • 通过 entity.read(cx) 获取不可变引用(多个可同时存在)
  • 通过 entity.update(cx, |data, cx| { ... }) 获取可变引用(独占)
  • 框架保证同一时刻只有一个可变引用或任意多个不可变引用

entity.clone() 获取句柄

entity.clone() 克隆的是 Entity 的句柄(类似 Rc),不会克隆实际数据。克隆的句柄指向同一份数据,可以通过同一个 Entity 进行读写。

clone.rs

rust 复制代码
// entity.clone() 克隆句柄,数据共享
let entity1 = cx.new(|cx| MyData { value: 42 });
let entity2 = entity1.clone(); // 句柄克隆,不克隆数据

entity2.update(cx, |data, cx| {
data.value = 100; // entity1 和 entity2 指向同一份数据
cx.notify();
});

assert_eq!(entity1.read(cx).value, 100); // 通过

弱引用:WeakEntity

WeakEntity 是 Entity 的弱引用版本,用于打破引用循环和防止内存泄漏。与 Entity 不同,WeakEntity 不保证目标 Entity 仍然存在。

weak_entity.rs

rust 复制代码
// 获取弱引用
let weak: WeakEntity<Child> = entity.downgrade();

// 使用弱引用前需要升级
if let Some(entity) = weak.upgrade(cx) {
entity.update(cx, |data, cx| {
data.do_something();
cx.notify();
});
}

// 检查 Entity 是否还有活跃引用
weak.is_valid(cx);

Entity vs Rc/RefCell 的优势

  • 自动通知 :Entity 通过 cx.notify() 自动触发 UI 更新,无需手动管理订阅
  • 生命周期集成:Entity 与 GPUI 窗口生命周期紧密绑定,窗口关闭时自动清理
  • 线程安全:Entity 通过 Context 访问,框架保证线程安全,无需显式加锁
  • 焦点感知 :支持 cx.observe() 响应式监听,实现声明式的状态管理
  • 借用检查:编译时即可捕获数据竞争和借用冲突,防止运行时 panic
  • 弱引用支持 :内建 WeakEntity 防止引用循环导致的内存泄漏

核心

第13章 事件发布订阅

来源示例:ownership_post.rs

EventEmitter trait:事件发射器

EventEmitter trait 是 GPUI 实现组件间解耦通信的核心机制。通过派生宏 #[derive(EventEmitter)] 可以轻松将枚举类型声明为一组事件。

为结构体派生 EventEmitter

events.rs

rust 复制代码
use gpui::*;

// 定义事件枚举并派生 EventEmitter
#[derive(EventEmitter)]
pub enum MyEvent {
Clicked,
Updated { old_value: i32, new_value: i32 },
Deleted(EntityId),
}

// 在 Entity 上实现 EventEmitter
pub struct MyComponent {
value: i32,
_subscriptions: Vec<Subscription>,
}

impl EventEmitter<MyEvent> for MyComponent {}

cx.emit():发射事件

cx.emit() 用于从 Entity 中发射事件。所有订阅了该 Entity 的组件都会收到事件通知。

在 Entity 更新方法中发射事件

emit_example.rs

rust 复制代码
impl MyComponent {
fn update_value(&mut self, new_value: i32, cx: &mut ViewContext<Self>) {
let old_value = self.value;
self.value = new_value;
cx.notify();

// 发射事件通知所有订阅者
cx.emit(MyEvent::Updated { old_value, new_value });
}

fn handle_click(&mut self, cx: &mut ViewContext<Self>) {
// 发射简单事件
cx.emit(MyEvent::Clicked);
}
}

cx.subscribe():订阅事件

cx.subscribe() 用于订阅另一个 Entity 的事件。返回的 Subscription 需要在结构体中持有以保持订阅有效。

在组件中订阅其他 Entity 的事件

subscribe_example.rs

rust 复制代码
pub struct Observer {
target: Entity<MyComponent>,
last_event: Option<MyEvent>,
_subscription: Subscription,
}

impl Observer {
fn new(target: Entity<MyComponent>, cx: &mut ViewContext<Self>) -> Self {
let subscription = cx.subscribe(&target, |observer, emitter, event, cx| {
match event {
MyEvent::Clicked => {
observer.last_event = Some(MyEvent::Clicked);
}
MyEvent::Updated { old_value, new_value } => {
observer.last_event = Some(MyEvent::Updated {
old_value: *old_value,
new_value: *new_value,
});
}
MyEvent::Deleted(id) => {
observer.last_event = Some(MyEvent::Deleted(*id));
}
}
cx.notify();
});

Observer {
target,
last_event: None,
_subscription: subscription,
}
}
}

ownership_post.rs 完整示例

该示例展示了 GPUI 中多级事件传播的经典模式,模拟了帖子(Post)和回复(Reply)之间的事件流:

Post 实体 → Reply 实体的事件流

Post 和 Reply 之间通过事件订阅形成通信链路。当 Post 发生变化时,Reply 自动收到通知并更新。

多级事件传播

ownership_post.rs

rust 复制代码
// Post 事件定义
#[derive(EventEmitter)]
pub enum PostEvent {
Updated,
Deleted,
}

// Reply 事件定义
#[derive(EventEmitter)]
pub enum ReplyEvent {
Created,
Edited,
}

// Post:发布事件并允许订阅
pub struct Post {
content: SharedString,
reply_count: usize,
}

impl EventEmitter<PostEvent> for Post {}

impl Post {
fn edit(&mut self, new_content: SharedString, cx: &mut ViewContext<Self>) {
self.content = new_content;
cx.notify();
cx.emit(PostEvent::Updated); // 通知所有订阅者
}
}

// Reply:订阅 Post 事件
pub struct Reply {
post: Entity<Post>,
text: SharedString,
_post_subscription: Subscription,
}

impl Reply {
fn new(post: Entity<Post>, cx: &mut ViewContext<Self>) -> Self {
let subscription = cx.subscribe(&post, |reply, post, event, cx| {
match event {
PostEvent::Updated => {
// Post 更新时,Reply 做出响应
cx.notify();
}
PostEvent::Deleted => {
// Post 删除时,清理 Reply
cx.notify();
}
}
});

Reply {
post,
text: "".into(),
_post_subscription: subscription,
}
}
}

事件生命周期

GPUI 中的事件遵循以下生命周期规则:

  1. 定义 :通过 #[derive(EventEmitter)] 定义事件枚举
  2. 发射 :通过 cx.emit() 发送事件
  3. 传播:事件同步分发给所有订阅者,在当前帧内完成
  4. 处理 :订阅者回调中处理事件,通常调用 cx.notify() 触发重新渲染
  5. 取消订阅:当 Subscription 被 Drop 时自动取消订阅

💡 提示: EventEmitter 与 UI 事件(on_click)的区别

  • EventEmitter:业务逻辑层面的事件,用于组件之间的解耦通信。由开发者定义事件类型和传播时机。
  • UI 事件(on_click 等):用户交互触发的事件,由 GPUI 框架捕获并分发。属于平台层事件,与具体业务逻辑无关。
  • 典型组合:在 on_click 回调中调用 cx.emit(),将 UI 事件转化为业务事件。

中级

第14章 全局状态

来源示例:text.rs(Global trait)

Global trait:跨组件共享状态

Global trait 是 GPUI 提供的跨组件状态共享机制。与 Entity 不同,Global 状态不绑定到特定组件,而是在整个应用上下文中以单例形式存在。适合管理字体配置、主题设置、用户偏好等全局数据。

实现 Global trait

global_example.rs

rust 复制代码
use gpui::*;

pub struct MySettings {
pub theme: String,
pub font_size: Pixels,
pub language: String,
}

// 实现 Global trait
impl Global for MySettings {}

// 或者在初始化时提供默认值
impl Global for MySettings {
fn default() -> Self {
MySettings {
theme: "dark".into(),
font_size: px(14.0),
language: "zh-CN".into(),
}
}
}

cx.set_global():设置全局状态

cx.set_global() 用于设置或替换全局状态的值。如果全局状态尚未初始化,此方法会创建它;如果已存在,则替换为新值。

set_global.rs

rust 复制代码
// 设置全局状态
cx.set_global(MySettings {
theme: "light".into(),
font_size: px(16.0),
language: "en".into(),
});

// 在应用启动时初始化
fn main() {
App::new().run(|cx: &mut AppContext| {
cx.set_global(MySettings {
theme: "dark".into(),
font_size: px(14.0),
language: "zh-CN".into(),
});
// ... 其余初始化代码
});
}

cx.global():读取全局状态

cx.global::() 返回全局状态的不可变引用。如果全局状态未初始化,此方法会 panic------因此需要确保在使用前已通过 set_global() 设置。

read_global.rs

rust 复制代码
// 读取全局状态
let settings = cx.global::<MySettings>();
let current_theme = &settings.theme;
let font_size = settings.font_size;

// 在渲染中使用全局状态
div()
.text_size(cx.global::<MySettings>().font_size)
.child("使用全局字体大小")

cx.update_global():更新全局状态

cx.update_global(|state, cx| { ... }) 提供可变访问全局状态的方式。更新后会自动通知所有观察者。

update_global.rs

rust 复制代码
// 更新全局状态
cx.update_global(|settings: &mut MySettings, cx| {
settings.theme = "high_contrast".into();
settings.font_size = px(18.0);
});

cx.observe_global():观察全局状态变化

cx.observe_global::() 用于监听特定全局状态的变化。当任何地方调用 update_global 修改该全局状态时,观察者的回调将被触发。

observe_global.rs

rust 复制代码
pub struct ThemeAwareComponent {
current_theme: String,
_global_subscription: Subscription,
}

impl ThemeAwareComponent {
fn new(cx: &mut ViewContext<Self>) -> Self {
let subscription = cx.observe_global::<MySettings>(|component, cx| {
// 全局设置变化时更新本地状态
let settings = cx.global::<MySettings>();
component.current_theme = settings.theme.clone();
cx.notify();
});

ThemeAwareComponent {
current_theme: cx.global::<MySettings>().theme.clone(),
_global_subscription: subscription,
}
}
}

text.rs 中的字体全局状态管理

GPUI 内置的 text.rs 使用了 Global 模式管理字体配置,这是全局状态的一个经典应用场景:

text_global.rs

rust 复制代码
// GPUI 内部字体全局状态
pub struct TextSystem {
pub fonts: HashMap<String, Font>,
pub default_font_size: Pixels,
pub line_height: f32,
}

impl Global for TextSystem {}

// 应用启动时初始化字体系统
fn init_text_system(cx: &mut AppContext) {
cx.set_global(TextSystem {
fonts: load_system_fonts(),
default_font_size: px(14.0),
line_height: 1.5,
});
}

// 任意组件中访问字体系统
fn render(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let text_system = cx.global::<TextSystem>();
div()
.text_size(text_system.default_font_size)
.child("全局字体设置生效")
}

全局状态 vs Entity 的选择

⚠️ 注意: 全局状态的滥用风险

  • 紧耦合:过度使用全局状态会导致组件之间隐式依赖,降低代码可维护性
  • 测试困难:全局状态使单元测试变得复杂,需要额外的设置和清理代码
  • 并发风险:虽然 GPUI 提供线程安全的访问,但全局可变状态仍然是不良设计模式的信号
  • 命名冲突:一个类型只能有一个 Global 实例,如果多个模块都想使用同类型会有冲突
  • 最佳实践:优先使用 Entity 管理组件状态,仅在确实需要跨组件共享且确认为单例数据时才使用 Global

高级

第15章:列表与虚拟化

当列表数据量很大时,一次性渲染所有行会导致严重的性能问题。GPUI 提供了 uniform_list 和 list 两种虚拟化列表组件,只渲染可见区域的行,实现十万级数据的流畅渲染。

💡 提示: 💡 核心优势

虚拟化列表通过只渲染可视区域内的元素,将渲染复杂度从 O(n) 降低到 O(k),其中 k 是可见行数(通常不超过 50)。这意味着即使有 10 万行数据,也能保持 60fps 的流畅滚动。

uniform_list - 等高行虚拟列表

uniform_list 适用于所有行高度相同的场景,如文件列表、消息列表等。它通过 UniformListState 管理滚动状态和可见区域计算。

UniformListState 管理

使用 UniformListState 来跟踪列表的滚动位置、可见区域和项目高度。每个项目的高度必须相同。

cx.processor() 回调生成列表项

通过 cx.processor() 回调来动态生成列表项,只有在可见区域内的项目才会被创建和渲染。

滚动优化原理

uniform_list 通过监听滚动事件,计算当前可见区域的范围,只渲染该范围内的项目。当滚动时,它会复用已有的 DOM 元素,只更新内容,避免频繁的创建和销毁。

uniform_list_example.rs

rust 复制代码
use gpui::*;

struct ListItem {
id: usize,
label: String,
}

struct UniformListApp {
items: Vec<ListItem>,
list_state: UniformListState,
}

impl UniformListApp {
fn new(cx: &mut Context&Self) -> Self {
let items = (0..10000).map(|i| ListItem {
id: i,
label: format("Item {}", i),
}).collect();

let list_state = UniformListState::new(
cx.entity(),
// 每项高度 40px
40.0,
);

Self { items, list_state }
}
}

impl Render for UniformListApp {
fn render(&mut self, _window: &mut Window, cx: &mut Context&Self) -> impl IntoElement {
let items_len = self.items.len();
let items = self.items.clone();

div()
.id("uniform-list-container")
.size_full()
.uniform_list(self.list_state.clone(), items_len, move |range, _cx| {
items[range]
.iter()
.map(|item| {
div()
.id(ElementId::Name(format("item-{}", item.id).into()))
.h(10)
.px(4)
.bg(rgb(0xfafafa))
.border_b(1)
.border_color(rgb(0xe5e7eb))
.flex()
.items_center()
.child(item.label.clone())
})
.collect::Vec&_>()
})
.flex_grow()
}
}

list - 变高行列表

当列表中各行高度不一致时(如聊天消息、评论列表),使用 list 组件。它通过 ListState 管理,支持动态高度计算。

ListState 管理

ListState 需要手动管理每个项目的高度,或者提供一个高度估算函数。它会在滚动时动态测量实际高度。

ListAlignment::Bottom - 底部对齐

设置 ListAlignment::Bottom 可以让列表从底部开始显示,非常适合聊天应用------新消息出现在底部,历史消息向上滚动。

list_example.rs

rust 复制代码
use gpui::*;
use gpui::ListAlignment;

struct ChatMessage {
id: usize,
author: String,
content: String,
height: f32,
}

struct ChatApp {
messages: Vec&ChatMessage>,
list_state: ListState,
}

impl ChatApp {
fn new(cx: &mut Context&Self) -> Self {
let messages = vec![
ChatMessage { id: 0, author: "Alice".to_string(), content: "Hello!".to_string(), height: 60.0 },
ChatMessage { id: 1, author: "Bob".to_string(), content: "Hi there! How are you doing today?".to_string(), height: 80.0 },
ChatMessage { id: 2, author: "Alice".to_string(), content: "I'm good, thanks!".to_string(), height: 60.0 },
];

let list_state = ListState::new(
// 底部对齐,适合聊天
ListAlignment::Bottom,
cx.entity(),
);

Self { messages, list_state }
}
}

impl Render for ChatApp {
fn render(&mut self, _window: &mut Window, cx: &mut Context&Self) -> impl IntoElement {
let messages = self.messages.clone();
let messages_len = self.messages.len();

div()
.id("chat-list")
.size_full()
.list(self.list_state.clone(), messages_len, move |range, cx| {
messages[range.start..range.end.min(messages_len)]
.iter()
.map(|msg| {
div()
.id(ElementId::Name(format("msg-{}", msg.id).into()))
.min_h(msg.height)
.p(3)
.mb(2)
.bg(rgb(0xf0f9ff))
.rounded_lg()
.child(
div()
.text_sm()
.text_gray_500()
.child(msg.author.clone())
)
.child(
div()
.mt(1)
.child(msg.content.clone())
)
})
.collect()
})
.flex_grow()
}
}

自定义滚动条

GPUI 的列表组件内置了滚动功能,但你也可以通过 canvas() + on_mouse_event 实现自定义的滚动条外观和交互行为。

custom_scrollbar.rs

rust 复制代码
// 自定义滚动条实现思路:
// 1. 使用 canvas() 绘制滚动条轨道和滑块
// 2. 监听 on_mouse_down / on_mouse_move / on_mouse_up 实现拖拽
// 3. 根据列表滚动比例计算滑块位置

fn render_scrollbar(scroll_ratio: f32, thumb_size: f32) -> impl IntoElement {
canvas(move |bounds, _, cx| {
let track_rect = bounds.rect();
let thumb_y = scroll_ratio * (bounds.size.height - thumb_size);

// 绘制轨道
cx.paint_quad(track_rect, thumb_y, bounds.size.width, bounds.size.height, rgb(0xf1f5f9), 0.0);

// 绘制滑块
cx.paint_quad(
track_rect,
thumb_y,
bounds.size.width,
thumb_size,
rgb(0x94a3b8),
bounds.size.width / 2.0,
);
})
.on_mouse_down(MouseButton::Left, |event, phase, cx| {
// 处理拖拽开始
cx.stop_propagation();
})
}

data_table - 大数据表格

data_table 是 uniform_list 的实际应用案例,用于展示大量结构化数据。它通过虚拟化技术,轻松处理十万级别的数据行。

Rc 共享数据避免克隆

对于大型数据集,使用 Rc(引用计数指针)来共享数据,避免昂贵的数据克隆操作。多个行可以安全地引用同一份数据。

RenderOnce 实现 TableRow

对于只需要渲染一次或者数据不可变的情况,实现 RenderOnce trait 可以获得更好的性能。它允许 GPUI 在某些情况下跳过不必要的重新渲染。

data_table.rs

rust 复制代码
use std::rc::Rc;
use gpui::*;

/// 表格数据结构(共享)
struct TableData {
rows: Vec&TableRowData>,
columns: Vec&String>,
}

struct TableRowData {
id: usize,
values: Vec&String>,
}

struct DataTableApp {
data: Rc&TableData>,
list_state: UniformListState,
}

impl DataTableApp {
fn new(cx: &mut Context&Self) -> Self {
// 生成 100000 行测试数据
let rows = (0..100000).map(|i| TableRowData {
id: i,
values: vec![
format("{}", i),
format("User {}", i),
format("user{}@example.com", i),
],
}).collect();

let data = Rc::new(TableData {
rows,
columns: vec!["ID".to_string(), "Name".to_string(), "Email".to_string()],
});

let list_state = UniformListState::new(cx.entity(), 40.0);

Self { data, list_state }
}
}

impl Render for DataTableApp {
fn render(&mut self, _window: &mut Window, cx: &mut Context&Self) -> impl IntoElement {
let data = self.data.clone();
let row_count = self.data.rows.len();

div()
.size_full()
.flex_col()
// 表头
.child(
div()
.flex()
.h(10)
.bg(rgb(0xf1f5f9))
.border_b(2)
.border_color(rgb(0xcbd5e1))
.children(
self.data.columns.iter().map(|col| {
div()
.flex()
.flex_1()
.px(3)
.py(2)
.font_weight(500)
.child(col.clone())
})
)
)
// 虚拟列表体
.child(
div()
.flex_grow()
.uniform_list(
self.list_state.clone(),
row_count,
move |range, _cx| {
data.rows[range]
.iter()
.map(|row| {
div()
.flex()
.h(10)
.border_b(1)
.border_color(rgb(0xe2e8f0))
.children(
row.values.iter().map(|val| {
div()
.flex()
.flex_1()
.px(3)
.py(2)
.child(val.clone())
})
)
})
.collect()
}
)
)
}
}

uniform_list vs list 对比

特性 uniform_list list
行高 固定等高 可变高度
性能 更高(无需测量) 稍低(需要测量)
适用场景 文件列表、数据表格 聊天消息、评论、动态内容
底部对齐 不支持 支持(ListAlignment::Bottom)
状态管理 UniformListState ListState

💡 提示: 💡 性能提示

10 万行数据也能流畅渲染!关键在于只渲染可见区域(通常 20-50 行)。配合 Rc 共享数据和 RenderOnce 优化,可以获得极致的渲染性能。在选择列表类型时,如果行高固定,优先使用 uniform_list,它的性能更好。


中级

第16章:浮层 Popover

Popover 是 GUI 应用中常见的浮层组件,用于显示下拉菜单、工具提示、选择器等临时内容。GPUI 通过 deferred() 和 anchored() 两个核心 API 实现高效的 Popover 系统。

deferred() - 延迟渲染容器

deferred() 创建一个延迟渲染的容器,其内容只有在需要时才会被创建和渲染,避免不必要的渲染开销。这对于 Popover 这类"平时不可见,需要时才显示"的组件非常关键。

核心原理:deferred 将一个元素的渲染推迟到特定条件满足时。在 Popover 场景中,只有当用户点击触发器(如按钮)后,浮层内容才会被渲染到 DOM 中。

popover_basic.rs

rust 复制代码
use gpui::*;

struct PopoverApp {
show_popover: bool,
}

impl Render for PopoverApp {
fn render(&mut self, _window: &mut Window, cx: &mut Context&Self) -> impl IntoElement {
let show_popover = self.show_popover;

div()
.size_full()
.flex_col()
.items_center()
.justify_center()
.gap(4)
// 触发器按钮
.child(
button("Open Popover")
.on_click(move |_, cx| {
cx.update_model(|model, _| {
model.show_popover = !model.show_popover;
});
})
)
// 延迟渲染的 Popover 内容
.child(
deferred(move |_cx| show_popover)
.child(
div()
.absolute()
.mt(2)
.p(4)
.bg(white())
.rounded_lg()
.shadow_lg()
.child("Popover Content")
)
)
}
}

anchored() - 锚定定位

anchored() 将浮层内容锚定到某个参考元素(如按钮)的特定位置。通过 Anchor 枚举可以控制浮层出现在参考元素的哪个方向。

Anchor 枚举

GPUI 提供了 8 种锚定位置:

  • TopLeft - 参考元素左上角
  • TopRight - 参考元素右上角
  • BottomLeft - 参考元素左下角(常用:下拉菜单)
  • BottomRight - 参考元素右下角
  • LeftTop / LeftBottom - 参考元素左侧
  • RightTop / RightBottom - 参考元素右侧

popover_anchored.rs

rust 复制代码
use gpui::*;
use gpui::Anchor;

struct AnchoredPopoverApp {
show_popover: bool,
}

impl Render for AnchoredPopoverApp {
fn render(&mut self, _window: &mut Window, cx: &mut Context&Self) -> impl IntoElement {
let show_popover = self.show_popover;

div()
.id("popover-anchor")
.size_full()
.flex()
.items_start()
.justify_center()
.pt(20)
// 触发器
.child(
button("Open Menu")
.id("trigger-btn")
.on_click(move |_, cx| { /* toggle */ })
)
// 锚定的 Popover
.child(
deferred(move |_| show_popover)
.child(
anchored()
.anchor(Anchor::BottomLeft)
.child(
div()
.w(48)
.p(2)
.bg(white())
.rounded_lg()
.shadow_xl()
.child(
div()
.py(2)
.px(3)
.cursor_pointer()
.hover(bg(rgb(0xf1f5f9)))
.child("菜单项 1")
)
.child(
div()
.py(2)
.px(3)
.cursor_pointer()
.hover(bg(rgb(0xf1f5f9)))
.child("菜单项 2")
)
)
)
)
}
}

on_mouse_down_out - 点击外部关闭

on_mouse_down_out 事件用于处理"点击浮层外部区域时关闭浮层"的交互模式。这是 Popover、下拉菜单、模态框等组件的必备功能。

实现原理:当鼠标在元素外部按下时触发回调,在回调中设置状态为关闭。

popover_dismiss.rs

rust 复制代码
impl Render for DismissablePopover {
fn render(&mut self, _window: &mut Window, cx: &mut Context&Self) -> impl IntoElement {
let show_popover = self.show_popover;

div()
.size_full()
.flex_col()
.items_center()
.justify_center()
.child(
button("Open Popover")
.on_click(move |_, cx| {
cx.update_model(|m, _| m.show_popover = true);
})
)
// 点击外部关闭的 Popover
.child(
deferred(move |_| show_popover)
.child(
div()
.absolute()
.p(4)
.bg(white())
.rounded_lg()
.shadow_xl()
// 关键:点击外部关闭
.on_mouse_down_out(move |_event, cx| {
cx.update_model(|m, _| m.show_popover = false);
})
.child("Click outside to dismiss")
.child(
button("Close")
.on_click(move |_, cx| {
cx.update_model(|m, _| m.show_popover = false);
})
)
)
)
}
}

多级嵌套 Popover

复杂的 UI 场景中,一个 Popover 内部可能触发另一个 Popover(如级联菜单)。GPUI 通过 priority 层级控制来管理多个浮层的堆叠顺序。

priority 层级控制

每个浮层都有一个优先级(priority),优先级高的浮层会显示在优先级低的浮层之上。使用 with_priority() 可以设置浮层的优先级。

popover_nested.rs

rust 复制代码
/// 多级嵌套 Popover 示例(级联菜单)
struct NestedPopoverApp {
show_primary: bool,
show_secondary: bool,
}

impl Render for NestedPopoverApp {
fn render(&mut self, _window: &mut Window, cx: &mut Context&Self) -> impl IntoElement {
let show_primary = self.show_primary;
let show_secondary = self.show_secondary;

div()
.size_full()
.flex()
.p(10)
// 一级菜单触发器
.child(
button("File")
.on_click(move |_, cx| {
cx.update_model(|m, _| m.show_primary = !m.show_primary);
})
)
// 一级菜单(优先级 1)
.child(
deferred(move |_| show_primary)
.child(
with_priority(1)
.child(
anchored()
.anchor(Anchor::BottomLeft)
.child(
div()
.w(40)
.bg(white())
.rounded_lg()
.shadow_xl()
.p(1)
.child(
// "Open..." 菜单项,悬停时显示二级菜单
div()
.flex()
.justify_between()
.p(2)
.cursor_pointer()
.hover(bg(rgb(0xf1f5f9)))
.on_mouse_enter(move |_, cx| {
cx.update_model(|m, _| m.show_secondary = true);
})
.child("Open...")
.child("▶")
)
.child("Save")
.on_mouse_down_out(move |_, cx| {
cx.update_model(|m, _| {
m.show_primary = false;
m.show_secondary = false;
});
})
)
)
)
)
// 二级菜单(优先级 2,显示在一级菜单之上)
.child(
deferred(move |_| show_secondary)
.child(
with_priority(2)
.child(
anchored()
.anchor(Anchor::RightTop)
.child(
div()
.w(40)
.bg(white())
.rounded_lg()
.shadow_xl()
.p(1)
.child("Recent Files")
.child("From Disk...")
)
)
)
)
}
}

Popover 使用场景

  • 📋 下拉菜单:点击按钮展开菜单选项,使用 BottomLeft 锚定,配合 on_mouse_down_out 关闭。

  • 💬 工具提示 Tooltip:鼠标悬停时显示提示信息,使用 on_mouse_enter / on_mouse_exit 控制显示。

  • 🔍 选择器 Picker:输入时显示候选项列表,结合 text_input 和 uniform_list 实现搜索选择。

  • 🗂️ 级联菜单:多级嵌套 Popover,通过 with_priority() 控制层级堆叠顺序。

| Popover | 轻量级、非模态、锚定到触发元素。适合:下拉菜单、选择器、工具提示。 |

Modal 重量级、模态(阻止其他交互)、屏幕居中。适合:确认对话框、表单填写、重要通知。

选择原则:如果操作是上下文相关的且不需要完全打断用户流程,使用 Popover;如果需要用户专注完成某项任务,使用 Modal。


高级

第17章:动画系统

GPUI 提供了完整的动画系统,支持缓动函数(Easing)、变换(Transformation)和资源动画。通过 with_animation API,可以为任何元素的属性添加流畅的动画效果。

Animation 结构体

Animation 是 GPUI 动画系统的核心结构体。通过 Animation::new() 创建动画实例,然后使用 with_animation() 将动画附加到元素上。

animation_basic.rs

rust 复制代码
use gpui::*;
use std::time::Duration;

struct AnimationApp {
active: bool,
}

impl Render for AnimationApp {
fn render(&mut self, _window: &mut Window, cx: &mut Context&Self) -> impl IntoElement {
let active = self.active;

// 定义动画:从 0.0 到 1.0,持续 300ms
let animation = Animation::new(cx.entity(), active)
.with_duration(Duration::from_millis(300))
.ease_out();  // 使用 ease_out 缓动

div()
.size_full()
.flex()
.items_center()
.justify_center()
.child(
div()
.w(32)
.h(32)
.bg(rgb(0x7c3aed))
.rounded_full()
// 绑定动画到 scale 变换
.with_animation(animation, |div, progress| {
let scale = 0.8 + progress * 0.4;  // 0.8 -> 1.2
div.scale(scale)
})
)
.child(
button("Toggle")
.on_click(move |_, cx| {
cx.update_model(|m, _| m.active = !m.active);
})
)
}
}

Easing 函数

Easing 函数控制动画的"节奏感"。GPUI 内置了多种缓动函数,选择合适的缓动可以让动画更自然。

bounce()

弹跳效果,像球落地

ease_in_out()

对称缓入缓出,最常用

ease_in()

缓入,突然停止

ease_out()

缓出,优雅启动

spring()

弹簧效果,有过冲

animation_easing.rs

rust 复制代码
/// 不同缓动函数的动画效果对比
fn demo_easing(cx: &mut Context&Self, easing_fn: impl Fn(f32) -> f32) {
let anim = Animation::new(cx.entity(), true)
.with_duration(Duration::from_millis(500))
.with_easing(easing_fn);  // 自定义缓动函数

// 内置缓动快捷方法:
// .bounce()  - 弹跳
// .ease_in_out() - 缓入缓出
// .ease_in()  - 缓入
// .ease_out() - 缓出
// .spring()  - 弹簧
}

变换 Transformation

GPUI 支持三种基本变换操作,可以组合使用实现复杂的动画效果。

  • 🔄 rotate():旋转变换。参数为角度(弧度或度)。常用于加载动画、图标切换。

  • 📏 scale():缩放变换。参数 >1 放大,<1 缩小。常用于按压缩放、注意力引导。

  • ↔️ translate():平移变换。(x, y) 偏移量。常用于滑入滑出、拖拽动画。

animation.rs - SVG 旋转动画

rust 复制代码
use gpui::*;
use std::time::Duration;

struct SpinnerApp {
spinning: bool,
}

impl Render for SpinnerApp {
fn render(&mut self, _window: &mut Window, cx: &mut Context&Self) -> impl IntoElement {
let spinning = self.spinning;

// 持续旋转动画
let spin_anim = Animation::new(cx.entity(), spinning)
.with_duration(Duration::from_secs(1))
.repeat(AnimationRepeat::Forever)
.linear();

div()
.flex()
.items_center()
.justify_center()
.size_full()
.child(
svg()
.size(8)
.text_color(rgb(0x7c3aed))
.with_animation(spin_anim, |svg, progress| {
let angle = progress * 360.0;  // 0° -> 360°
svg.rotate(angle)
})
.path("icons/spinner.svg")
)
}
}

// 组合变换:同时缩放 + 旋转
fn combo_transform(element: impl IntoElement, progress: f32) -> impl IntoElement {
element
.scale(0.5 + progress)
.rotate(progress * 180.0)
.translate(progress * 20.0, progress * 10.0)
}

AssetSource - 加载动画资源

对于复杂的动画(如 Lottie 动画、SVG 序列帧),可以实现 AssetSource trait 来加载动画资源文件。

animation_asset.rs

rust 复制代码
use gpui::*;
use std::path::PathBuf;

/// 实现 AssetSource trait 来加载本地动画资源
struct LocalAssetSource {
base_path: PathBuf,
}

impl AssetSource for LocalAssetSource {
fn load_asset(&mut self, path: &str, cx: &mut Context&Self) -> Task&Result&Vec&u8>>> {
let full_path = self.base_path.join(path);
let path_clone = full_path.clone();

cx.background_executor().spawn(async move {
match std::fs::read(path_clone) {
Ok(bytes) => Ok(bytes),
Err(e) => Err(anyhow::anyhow("Failed to load asset: {}", e)),
}
})
}
}

/// 从文件加载 SVG 并应用动画
fn load_animated_svgpath: &str, cx: &mut Context&Self) -> impl IntoElement {
let asset_source = LocalAssetSource {
base_path: PathBuf::from("assets"),
};

// 加载 SVG 文件并绑定旋转动画
svg()
.size(6)
.with_asset_source(asset_source)
.path(path)
.with_animation(
Animation::new(cx.entity(), true)
.with_duration(Duration::from_secs(2))
.repeat(AnimationRepeat::Forever)
.linear(),
|svg, progress| {
svg.rotate(progress * 360.0)
}
)
}

手动动画:window.request_animation_frame()

对于需要逐帧控制的复杂动画(如游戏、粒子效果),可以使用 window.request_animation_frame() 手动驱动动画循环。

manual_animation.rs

rust 复制代码
use gpui::*;
use std::time::{Duration, Instant};

struct ManualAnimApp {
start_time: Option&Instant>,
elapsed: Duration,
}

impl ManualAnimApp {
fn start_animation(&mut self, window: &mut Window, cx: &mut Context&Self) {
self.start_time = Some(Instant::now());

// 请求下一帧
window.request_animation_frame(cx.entity(), {
move |app, window, cx| {
let elapsed = app.start_time.unwrap().elapsed();
app.elapsed = elapsed;
cx.notify();  // 触发重新渲染

// 持续请求下一帧(动画循环)
if elapsed < Duration::from_secs(3) {
window.request_animation_frame(cx.entity(), /* ... */);
}
}
});
}
}

impl Render for ManualAnimApp {
fn render(&mut self, window: &mut Window, cx: &mut Context&Self) -> impl IntoElement {
let progress = (self.elapsed.as_millis() as f32) / 3000.0;

div()
.size_full()
.flex()
.items_center()
.justify_center()
.child(
div()
.w(8)
.h(8)
.bg(rgb(0x3b82f6))
.rounded_full()
.translate(progress * 200.0, 0.0)
)
.child(
button("Start")
.on_click(move |_, cx| { /* start_animation */ })
)
}
}

💡 提示: 💡 动画性能优化建议

  • 优先使用 with_animation:内置动画系统会自动处理帧率和重绘,比手动 request_animation_frame 更高效。
  • 避免频繁触发 notify():每次 notify() 都会触发重新渲染,确保在动画回调中只更新必要的状态。
  • 使用 transform 而非修改 width/height:CSS transform 由 GPU 加速,修改宽高会触发 layout 重排。
  • 合理设置动画时长:界面动画建议 150-300ms,加载动画建议 800-1200ms 循环。
  • 减少动画元素数量:同时运行的动画元素越多,帧率下降越明显。考虑使用虚拟化或限制可见动画数量。

高级

第18章:底层绘图

当内置的布局系统无法满足需求时,GPUI 提供了 canvas() API 进行底层绘图。通过 PathBuilder 构建路径,使用 paint_path / paint_quad 进行填充和描边,可以实现任意形状的绘制。

⚠️ 注意: ⚠️ 注意事项

canvas() 的绘制内容绑定于元素尺寸。如果父元素尺寸发生变化,canvas 内容不会自动重绘,需要手动调用 cx.notify() 触发重新渲染。对于需要响应式绘制的场景,请监听尺寸变化事件。

canvas() 函数

canvas() 在 div 内部创建一个绘图区域。通过 paint 回调获取 PaintCtx,它提供了所有绘图 API。

canvas_basic.rs

rust 复制代码
use gpui::*;

/// 创建一个基础的画布元素
fn basic_canvas() -> impl IntoElement {
div()
.id("my-canvas")
.w(64)   // 画布宽度
.h(64)   // 画布高度
.bg(rgb(0xf8fafc))
.border(1)
.border_color(rgb(0xcbd5e1))
.rounded_lg()
.canvas(move |bounds, _hitbox, cx| {
// bounds: 画布的边界矩形 (px, py, width, height)
let center_x = bounds.origin.x + bounds.size.width / 2.0;
let center_y = bounds.origin.y + bounds.size.height / 2.0;
let radius = bounds.size.width.min(bounds.size.height) / 4.0;

// 使用 PathBuilder 构建一个圆形路径
let mut path = PathBuilder::new();
path.move_to(Point::new(center_x + radius, center_y));
path.arc_to(
Point::new(center_x, center_y),
radius,
0.0,
std::f32::consts::PI * 2.0,
);
path.close();

// 填充圆形
cx.paint_path(path.build(), Fill::new(rgb(0x3b82f6)));
})
}

/// 在 render 方法中使用
/*
impl Render for MyApp {
fn render(&mut self, _window: &mut Window, _cx: &mut Context&Self) -> impl IntoElement {
div()
.size_full()
.flex()
.items_center()
.justify_center()
.child(basic_canvas())
}
}
*/

PathBuilder - 路径构建

PathBuilder 提供了类似 SVG Path 的 API 来构建矢量路径。构建完成后通过 paint_path 或 paint_quad 进行渲染。

基本操作

  • move_to(point) - 移动画笔到指定点(不绘制)
  • line_to(point) - 从当前点画直线到目标点
  • close() - 闭合路径(连接到 move_to 的起点)

填充与描边模式

  • Fill::new(color) - 填充模式,填充封闭路径内部
  • Stroke::new(color, width) - 描边模式,绘制路径轮廓

path_basic.rs

rust 复制代码
/// 绘制基本形状:三角形(填充)+ 菱形(描边)
fn draw_shapes(cx: &mut PaintCtx, bounds: &Bounds&Px>) {
let ox = bounds.origin.x;
let oy = bounds.origin.y;
let w = bounds.size.width;
let h = bounds.size.height;

// 1. 填充三角形(左半区)
{
let mut path = PathBuilder::new();
path.move_to(Point::new(ox + w * 0.25, oy + h * 0.2));
path.line_to(Point::new(ox + w * 0.1,  oy + h * 0.8));
path.line_to(Point::new(ox + w * 0.4,  oy + h * 0.8));
path.close();
cx.paint_path(path.build(), Fill::new(rgb(0xef4444)));
}

// 2. 描边菱形(右半区)
{
let mut path = PathBuilder::new();
let cx2 = ox + w * 0.7;
let cy = oy + h * 0.5;
let sz = w * 0.15;
path.move_to(Point::new(cx2, cy - sz));  // 上
path.line_to(Point::new(cx2 + sz, cy));       // 右
path.line_to(Point::new(cx2, cy + sz));       // 下
path.line_to(Point::new(cx2 - sz, cy));       // 左
path.close();
cx.paint_path(
path.build(),
Stroke::new(rgb(0x22c55e), 3.0),
);
}
}

arc_to / curve_to - 曲线路径

除了直线,PathBuilder 还支持圆弧和贝塞尔曲线,可以绘制圆、椭圆、平滑曲线等复杂形状。

arc_to - 圆弧

arc_to(center, radius, start_angle, sweep_angle) 从当前点画一条圆弧到指定位置。常用于绘制扇形、圆角矩形等。

curve_to - 贝塞尔曲线

curve_to(control1, control2, end_point) 绘制三次贝塞尔曲线。通过两个控制点来定义曲线的形状。

painting.rs - 圆弧绘制

rust 复制代码
/// 从 painting.rs 提取的圆弧绘制示例
/// 绘制一个带缺口的圆环(类似仪表盘)
fn draw_arc_ring(cx: &mut PaintCtx, bounds: &Bounds&Px>) {
use std::f32::consts::PI;

let center = Point::new(
bounds.origin.x + bounds.size.width / 2.0,
bounds.origin.y + bounds.size.height / 2.0,
);
let outer_r = bounds.size.width.min(bounds.size.height) / 2.0 * 0.9;
let inner_r = outer_r * 0.7;

// 起始角度和扫过角度(弧度)
let start_angle = PI * 0.75;   // 135°
let sweep_angle = PI * 1.5;    // 270°

// 外圆弧
let mut outer_path = PathBuilder::new();
outer_path.move_to(Point::new(
center.x + outer_r * cos(start_angle),
center.y + outer_r * sin(start_angle),
));
outer_path.arc_to(center, outer_r, start_angle, sweep_angle);

// 内圆弧(反向)
let mut inner_path = PathBuilder::new();
inner_path.move_to(Point::new(
center.x + inner_r * cos(start_angle + sweep_angle),
center.y + inner_r * sin(start_angle + sweep_angle),
));
inner_path.arc_to(center, inner_r, start_angle + sweep_angle, -sweep_angle);

// 合并为闭合路径(圆环)
let mut ring_path = PathBuilder::new();
ring_path.move_to(outer_path.current_point());
// ... 实际代码中需要更精细的路径构建
// 这里简化为直接填充外圆再挖空内圆

cx.paint_path(outer_path.build(), Fill::new(rgb(0x7c3aed)));
}

paint_path / paint_quad

构建好路径后,需要通过渲染命令将其绘制到画布上。

API 用途 示例
paint_path(path, fill) 填充任意路径 圆形、多边形、自定义形状
paint_path(path, stroke) 描边任意路径 曲线、虚线边框
paint_quad(rect, fill, radius) 快速绘制圆角矩形 按钮、卡片、背景块

paint_quad.rs

rust 复制代码
/// 使用 paint_quad 高效绘制圆角矩形
/// paint_quad 比 paint_path + PathBuilder 更快,适合绘制大量矩形
fn draw_quads(cx: &mut PaintCtx, bounds: &Bounds&Px>) {
let b = bounds;

// 1. 绘制背景卡片(圆角矩形,填充)
cx.paint_quad(
Rect::new(
Point::new(b.origin.x + 10.0, b.origin.y + 10.0),
Size::new(b.size.width - 20.0, b.size.height - 20.0),
),
Fill::new(rgb(0xffffff)),
8.0,  // 圆角半径
);

// 2. 绘制进度条(矩形,填充)
let progress = 0.65;  // 65% 进度
cx.paint_quad(
Rect::new(
Point::new(b.origin.x + 20.0, b.origin.y + b.size.height - 30.0),
Size::new((b.size.width - 40.0) * progress, 8.0),
),
Fill::new(rgb(0x7c3aed)),
4.0,
);

// 3. 绘制边框(矩形,描边)
cx.paint_quad(
Rect::new(
Point::new(b.origin.x + 10.0, b.origin.y + 10.0),
Size::new(b.size.width - 20.0, b.size.height - 20.0),
),
Stroke::new(rgb(0xcbd5e1), 1.0),
8.0,
);
}

鼠标绘图交互

painting.rs 展示了一个完整的画板应用:用户按住鼠标拖动来绘图,按住 Shift 键可以绘制直线。这是 canvas() + 鼠标事件联合使用的典型案例。

核心思路

  1. 在 on_mouse_down 中记录画笔起点
  2. 在 on_mouse_move 中持续更新当前点,触发重绘
  3. 在 on_mouse_up 中完成当前笔画
  4. 按住 Shift 时,将当前点吸附到起点/终点的直线方向上

painting.rs - 鼠标绘图核心逻辑

rust 复制代码
use gpui::*;

struct PaintPoint {
x: f32,
y: f32,
}

struct PaintStroke {
points: Vec&PaintPoint>,
color: u32,
width: f32,
}

struct DrawingApp {
strokes: Vec&PaintStroke>,
current_stroke: Option&PaintStroke>,
shift_pressed: bool,
}

impl DrawingApp {
/// 处理鼠标按下:开始新笔画
fn on_mouse_down(&mut self, event: &MouseDownEvent, cx: &mut Context&Self) {
let pos = event.position;
self.current_stroke = Some(PaintStroke {
points: vec![PaintPoint { x: pos.x, y: pos.y }],
color: 0x000000,
width: 2.0,
});
cx.notify();
}

/// 处理鼠标移动:追加点到当前笔画
fn on_mouse_move(&mut self, event: &MouseMoveEvent, cx: &mut Context&Self) {
if let Some(ref mut stroke) = self.current_stroke {
let mut pos = event.position;

// Shift 吸附:如果按住 Shift,将点吸附到水平/垂直/45°方向
if self.shift_pressed && stroke.points.len() >= 1 {
let first = stroke.points.first().unwrap();
let dx = (pos.x - first.x).abs();
let dy = (pos.y - first.y).abs();
if dx > dy {
// 水平方向
pos.y = first.y;
} else {
// 垂直方向
pos.x = first.x;
}
}

stroke.points.push(PaintPoint { x: pos.x, y: pos.y });
cx.notify();  // 触发重绘
}
}

/// 处理鼠标释放:完成笔画
fn on_mouse_up(&mut self, _event: &MouseUpEvent, cx: &mut Context&Self) {
if let Some(stroke) = self.current_stroke.take() {
self.strokes.push(stroke);
cx.notify();
}
}
}

复杂图形:Rust logo、饼图、波浪线

通过组合基本路径操作,可以绘制非常复杂的图形。以下是几个经典案例的概要。

  • 🦀 Rust Logo:通过多个圆弧和直线路径组合,绘制 Gear 形状的 Rust logo。关键:精确计算每条弧的起始/结束角度。

  • 🥧 饼图 Pie Chart:从同一圆心出发,每段数据用一个 arc_to 弧 + 两条半径线构成扇形,填充不同颜色。

  • 〰️ 波浪线 Wave:使用 curve_to 贝塞尔曲线,控制点的 y 坐标按正弦规律变化,形成平滑波浪效果。

⚠️ 注意: ⚠️ canvas 绑定于元素尺寸

canvas() 的 paint 回调接收到的 bounds 是元素在布局中的实际尺寸。如果父元素尺寸发生变化(如窗口缩放),需要手动监听 on_resize 事件并调用 cx.notify() 触发重绘,否则画布内容可能停留在旧尺寸上。


中级

第19章:渐变与图案

GPUI 支持丰富的渐变和图案填充功能。通过 linear_gradient 创建线性渐变,使用 ColorSpace 控制色彩空间,还可以用 pattern_slash 创建图案填充。

linear_gradient - 线性渐变

线性渐变沿一条直线方向平滑过渡多种颜色。使用 linear_gradient() 创建渐变对象,通过 linear_color_stop() 定义每个色标的位置和颜色。

基本用法

  1. 创建渐变:linear_gradient(start_point, end_point, color_stops, color_space)
  2. 定义色标:linear_color_stop(offset, color),offset 范围 0.0 ~ 1.0
  3. 将渐变作为 Fill 传递给 paint_path 或 paint_quad

gradient_basic.rs

rust 复制代码
use gpui::*;

/// 创建一个基础线性渐变(蓝紫渐变)
fn basic_linear_gradient(cx: &mut PaintCtx, bounds: &Bounds&Px>) {
let rect = Rect::new(bounds.origin, bounds.size);

// 从左到右的蓝紫渐变
let gradient = linear_gradient(
Point::new(0.0, 0.0),          // 起点(左)
Point::new(1.0, 0.0),          // 终点(右)
vec![
linear_color_stop(0.0, rgb(0x3b82f6)),  // 蓝色(起点)
linear_color_stop(1.0, rgb(0x7c3aed)),  // 紫色(终点)
],
ColorSpace::Oklab,                  // 使用感知均匀的色彩空间
);

// 用渐变填充矩形
cx.paint_quad(rect, Fill::new(gradient), 12.0);
}

/// 多色标渐变(彩虹渐变)
fn rainbow_gradient(cx: &mut PaintCtx, bounds: &Bounds&Px>) {
let gradient = linear_gradient(
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
vec![
linear_color_stop(0.0,  rgb(0xef4444)),  // 红
linear_color_stop(0.25, rgb(0xf97316)),  // 橙
linear_color_stop(0.5,  rgb(0xeab308)),  // 黄
linear_color_stop(0.75, rgb(0x22c55e)),  // 绿
linear_color_stop(1.0,  rgb(0x3b82f6)),  // 蓝
],
ColorSpace::Oklab,
);

let rect = Rect::new(
Point::new(bounds.origin.x + 10.0, bounds.origin.y + 10.0),
Size::new(bounds.size.width - 20.0, 60.0),
);
cx.paint_quad(rect, Fill::new(gradient), 8.0);
}

ColorSpace - 色彩空间

色彩空间决定了颜色之间的过渡方式。GPUI 支持两种色彩空间,选择不同的空间会得到截然不同的渐变效果。

色彩空间 特点 适用场景
ColorSpace::Oklab 感知均匀,渐变过渡自然,符合人眼感知 UI 渐变、品牌色过渡(推荐)
ColorSpace::Srgb 屏幕 RGB 空间,过渡可能偏灰/偏暗 需要与 CSS 渐变完全一致时

gradient_colorspace.rs

rust 复制代码
/// 对比 Oklab vs Srgb 两种色彩空间的渐变效果
/// 在 canvas 中并排绘制两个渐变条
fn compare_color_spaces(cx: &mut PaintCtx, bounds: &Bounds&Px>) {
let b = bounds;
let bar_h = 40.0;
let bar_w = (b.size.width - 40.0) / 2.0;

// 共用的色标:从红到蓝
let stops = vec![
linear_color_stop(0.0, rgb(0xef4444)),
linear_color_stop(1.0, rgb(0x3b82f6)),
];

// 左半区:Oklab(感知均匀,过渡自然)
{
let gradient_oklab = linear_gradient(
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
stops.clone(),
ColorSpace::Oklab,
);
let rect = Rect::new(
Point::new(b.origin.x + 10.0, b.origin.y + 20.0),
Size::new(bar_w, bar_h),
);
cx.paint_quad(rect, Fill::new(gradient_oklab), 6.0);
}

// 右半区:Srgb(可能偏灰)
{
let gradient_srgb = linear_gradient(
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
stops,
ColorSpace::Srgb,
);
let rect = Rect::new(
Point::new(b.origin.x + 20.0 + bar_w, b.origin.y + 20.0),
Size::new(bar_w, bar_h),
);
cx.paint_quad(rect, Fill::new(gradient_srgb), 6.0);
}
}

在 canvas() 中绘制渐变路径

渐变不仅可以填充矩形,还可以填充任意路径。结合 PathBuilder 构建的路径,可以创建渐变描边、渐变形状等高级效果。

gradient.rs - 渐变路径绘制

rust 复制代码
/// 在 canvas 中绘制渐变填充的圆形
/// 来源:gradient.rs 示例
fn draw_gradient_circle(cx: &mut PaintCtx, bounds: &Bounds&Px>) {
let ox = bounds.origin.x;
let oy = bounds.origin.y;
let w = bounds.size.width;
let h = bounds.size.height;
let cx2 = ox + w / 2.0;
let cy = oy + h / 2.0;
let r = w.min(h) / 3.0;

// 创建从中心到边缘的径向渐变(如果支持)
// 此处演示:用线性渐变填充圆形路径
let gradient = linear_gradient(
Point::new(0.0, 0.0),   // 左上
Point::new(1.0, 1.0),   // 右下(对角线渐变)
vec![
linear_color_stop(0.0, rgb(0xf472b6)),  // 粉色
linear_color_stop(0.5, rgb(0xc084fc)),  // 紫色
linear_color_stop(1.0, rgb(0x818cf8)),  // 靛蓝
],
ColorSpace::Oklab,
);

// 构建圆形路径
let mut path = PathBuilder::new();
path.move_to(Point::new(cx2 + r, cy));
path.arc_to(
Point::new(cx2, cy),
r,
0.0,
std::f32::consts::PI * 2.0,
);
path.close();

// 用渐变填充圆形
// 注意:需要将渐变坐标映射到路径边界框
cx.paint_path(
path.build(),
Fill::new(gradient),
);
}

/// 渐变描边(使用 Stroke 模式)
fn draw_gradient_stroke(cx: &mut PaintCtx, bounds: &Bounds&Px>) {
let ox = bounds.origin.x;
let oy = bounds.origin.y;
let w = bounds.size.width;

let gradient = linear_gradient(
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
vec![
linear_color_stop(0.0, rgb(0x06b6d4)),
linear_color_stop(1.0, rgb(0x8b5cf6)),
],
ColorSpace::Oklab,
);

// 绘制一条直线路径并描边
let mut path = PathBuilder::new();
path.move_to(Point::new(ox + 40.0, oy + 100.0));
path.line_to(Point::new(ox + w - 40.0, oy + 100.0));

// 用渐变描边(注意:GPUI 的 Stroke 目前可能不支持渐变,
// 此处为概念演示,实际请参考最新 API 文档)
cx.paint_path(
path.build(),
Stroke::new(/* gradient */ rgb(0x000000), 4.0),
);
}

pattern_slash - 图案填充

pattern_slash 创建斜线图案填充,常用于表示"禁用"、"加载中"或装饰性背景。来自 pattern.rs 示例。

pattern.rs - 斜线图案

rust 复制代码
use gpui::*;

/// 使用 pattern_slash 创建斜线图案填充
/// 来源:pattern.rs 示例
fn draw_pattern_fill(cx: &mut PaintCtx, bounds: &Bounds&Px>) {
let rect = Rect::new(bounds.origin, bounds.size);

// 创建斜线图案
// pattern_slash(color, line_width, gap) -> Pattern
let pattern = pattern_slash(
rgb(0x94a3b8),   // 斜线颜色(灰色)
2.0,                 // 线宽
8.0,                 // 间距
);

// 用图案填充矩形
cx.paint_quad(rect, Fill::new(pattern), 0.0);
}

/// 在元素中使用图案背景
/// 通过 canvas 将图案应用到任意元素背景
fn pattern_background() -> impl IntoElement {
div()
.id("pattern-demo")
.w(64)
.h(64)
.rounded_xl()
.border(2)
.border_color(rgb(0xcbd5e1))
.canvas(move |bounds, _, cx| {
let pattern = pattern_slash(rgb(0x7c3aed), 1.5, 6.0);
let rect = Rect::new(bounds.origin, bounds.size);
cx.paint_quad(rect, Fill::new(pattern), 12.0);
})
.flex()
.items_center()
.justify_center()
.child("斜线背景")
}

/// pattern.rs 中的其他图案示例(概念性)
/*
// 网格图案
let grid_pattern = pattern_grid(color, cell_size, line_width);

// 点状图案
let dot_pattern = pattern_dots(color, dot_radius, spacing);

// 棋盘图案
let checker_pattern = pattern_checker(color1, color2, tile_size);
*/

渐变与图案:功能对比

  • 🌈 linear_gradient:线性渐变,支持多色标、两种色彩空间。适合按钮、背景、卡片装饰。

  • 🎨 ColorSpace::Oklab:感知均匀色彩空间,渐变更自然。GPUI 推荐默认使用此空间。

  • 🎨 ColorSpace::Srgb:传统屏幕色彩空间,与 CSS 渐变一致。过渡可能不够自然。

  • 〰️ pattern_slash:斜线图案填充,可自定义颜色、线宽、间距。适合禁用状态背景。

💡 提示: 💡 Oklab vs Srgb 的选择

优先使用 Oklab:Oklab 是为人眼感知设计的色彩空间,两个颜色之间的渐变过渡在视觉上更均匀。例如,从红色到蓝色的 Oklab 渐变会呈现出丰富的紫色中间调,而 Srgb 渐变可能会在中间出现暗灰色带。


系统

第20章:窗口管理

GPUI 提供了完整的窗口管理 API,支持创建、配置、调整和监听窗口变化。通过 WindowOptions 可以精细控制窗口的外观和行为,使用 WindowKind 枚举定义窗口类型。

WindowOptions 完整字段

WindowOptions 是窗口创建的核心配置结构体,控制窗口的显示、位置、大小和外观。以下列出所有主要字段:

字段 类型 说明
display_id Option 指定窗口显示的设备 ID,用于多显示器场景
window_bounds WindowBounds 窗口位置和尺寸配置,可选 Fixed/Maximized/Fullscreen
titlebar TitlebarOptions 标题栏配置,包括标题文字、是否显示、自定义样式
kind WindowKind 窗口类型:Normal、PopUp、Floating、Dialog
focus bool 窗口创建后是否自动获取焦点
show bool 窗口创建后是否立即显示
center bool 是否在屏幕上居中显示窗口
is_movable bool 窗口是否可被用户拖动(仅部分平台)

完整 WindowOptions 配置

window_options.rs

rust 复制代码
use gpui::*;

/// 创建具有完整 WindowOptions 配置的窗口
fn create_window(cx: &mut App) {
let options = WindowOptions {
window_bounds: WindowBounds::Fixed(
Bounds<DevicePixels> {
origin: Point::new(DevicePixels(100), DevicePixels(100)),
size: Size {
width: DevicePixels(800),
height: DevicePixels(600),
},
}
),

// 标题栏配置
titlebar: TitlebarOptions {
title: Some(SharedString::from("My GPUI App")),
appears_transparent: false,
traffic_light_position: Some(Point::new(Pixels(12.0), Pixels(12.0))),
},

// 窗口类型
kind: WindowKind::Normal,

// 行为选项
focus: true,      // 创建后获取焦点
show: true,       // 创建后立即显示
center: true,     // 居中显示
is_movable: true, // 可拖动

// 显示设备(多显示器)
display_id: None,  // None 表示使用主显示器

..WindowOptions::default()
};

// 使用配置打开窗口
cx.open_window(options, |cx| {
cx.new(|_| MyApp)
});
}

WindowKind 枚举

WindowKind 定义了窗口的类型,不同类型在不同的操作系统中有不同的行为和外观表现。

枚举值 说明 典型用途
WindowKind::Normal 普通窗口,带标题栏和标准按钮(最小化/最大化/关闭) 主应用窗口
WindowKind::PopUp 弹出窗口,通常无标题栏,点击外部自动关闭 下拉菜单、右键菜单
WindowKind::Floating 浮动窗口,始终保持在父窗口上方 工具栏、调色板
WindowKind::Dialog 对话框,模态或非模态,系统原生外观 文件选择器、确认对话框

💡 提示: 平台差异

macOS 平台:部分 WindowKind 变体(如 PopUp)在 macOS 上有特殊的原生行为,包括聚焦策略和动画效果。开发时应多平台测试。

窗口操作

通过 WindowHandle 可以执行各种窗口级操作,包括系统对话框、尺寸调整、显示/隐藏等。

window.prompt() - 系统对话框

通过窗口句柄触发系统级对话框。提示文本将显示在对话框标题栏中。

window_operations.rs

rust 复制代码
/// 系统提示对话框
fn show_prompt(window: &mut Window, cx: &mut App) {
window.prompt(
PromptLevel::Info,     // 提示级别
"文件已保存",              // 提示文字
&["确定", "取消"],         // 按钮选项
cx,
|result, cx| {
match result {
0 => println!("用户点击了确定"),
1 => println!("用户点击了取消"),
_ => {}
}
},
);
}

window.resize() - 调整窗口大小

window_resize.rs

rust 复制代码
/// 调整窗口大小到指定尺寸(逻辑像素)
fn resize_window(window: &mut Window, cx: &mut App) {
let new_size = Size {
width:  Pixels(1024.0),
height: Pixels(768.0),
};
window.resize(new_size);
}

窗口显示控制

window_visibility.rs

rust 复制代码
/// 窗口显示/隐藏和最大/最小化操作
fn control_window(cx: &mut WindowContext) {
// 隐藏当前窗口
cx.hide();

// 最小化窗口
cx.minimize_window();

// 最大化窗口
cx.maximize_window();

// 切换全屏
// cx.fullscreen() / cx.exit_fullscreen()
}

监听窗口变化

使用 cx.observe_window_bounds() 可以监听窗口的位置和尺寸变化,这在需要响应式布局或窗口状态跟踪时非常有用。

observe_bounds.rs

rust 复制代码
use gpui::*;

struct WindowTracker {
current_bounds: Bounds<Pixels>,
}

impl Render for WindowTracker {
fn render(&mut self, _window: &mut Window, cx: &mut ViewContext<Self>) -> impl IntoElement {
// 监听窗口边界变化
cx.observe_window_bounds(|cx| {
let bounds = cx.window_bounds();
println!("窗口位置: ({}, {}), 尺寸: {}x{}",
bounds.origin.x,
bounds.origin.y,
bounds.size.width,
bounds.size.height,
);
});

format!("窗口: {}x{}",
self.current_bounds.size.width,
self.current_bounds.size.height,
);

div()
.p_4()
.child("窗口跟踪器")
}
}

window.rs 完整示例

以下是 window.rs 中的窗口管理完整示例代码。

window.rs - 完整示例

rust 复制代码
use gpui::*;
use std::sync::Arc;

struct WindowExample {
window_count: usize,
bounds_info: String,
}

impl WindowExample {
fn open_new_window(&mut self, cx: &mut WindowContext) {
let options = WindowOptions {
window_bounds: WindowBounds::Fixed(
Bounds {
origin: Point::new(DevicePixels(200), DevicePixels(200)),
size: Size {
width:  DevicePixels(600),
height: DevicePixels(400),
},
}
),
titlebar: TitlebarOptions {
title: Some(SharedString::from("新窗口")),
..TitlebarOptions::default()
},
kind: WindowKind::Normal,
center: false,
..WindowOptions::default()
};

cx.open_window(options, |cx| {
cx.new(|_| WindowExample {
window_count: 0,
bounds_info: String::new(),
})
});
}
}

impl Render for WindowExample {
fn render(&mut self, _window: &mut Window, cx: &mut ViewContext<Self>) -> impl IntoElement {
// 监听窗口边界变化
cx.observe_window_bounds(|cx| {
let b = cx.window_bounds();
// 更新边界信息
});

div()
.flex()
.flex_col()
.gap_4()
.p_4()
.child(
div()
.child("窗口管理示例")
)
.child(
div()
.on_click(|event, cx| {
// 调整窗口大小
cx.window().resize(Size {
width:  Pixels(900.0),
height: Pixels(600.0),
});
})
.child("调整大小")
)
}
}

💡 提示: 多窗口应用的架构建议

对于需要多个独立窗口的应用(如 Zed 编辑器),建议:

  1. 使用 WindowOptions 精确控制每个窗口的初始位置和大小
  2. 通过 cx.observe_window_bounds() 保存窗口状态,实现"记住上次位置"
  3. 利用 display_id 在正确的屏幕上打开副窗口
  4. 考虑使用全局状态(Global)在不同窗口之间共享数据
  5. 使用 cx.hide() 而非 cx.remove_window() 来"临时关闭"窗口,以便快速恢复

系统

第21章:多屏定位

在现代桌面应用中,多显示器支持已成为标配需求。GPUI 提供了完整的多屏 API,从枚举所有显示器到精确定位窗口,让你可以优雅地处理多屏场景。

多显示器支持

cx.displays() - 枚举所有显示器

cx.displays() 返回系统中所有可用的显示器列表,每个显示器包含边界信息、UUID 和 DPI 缩放因子。通过 DisplayId 可以唯一标识每个显示设备。

list_displays.rs

rust 复制代码
use gpui::*;

/// 枚举并打印系统中所有显示器信息
fn list_all_displays(cx: &AppContext) {
let displays = cx.displays();

for (i, display) in displays.iter().enumerate() {
let bounds = display.bounds();
let uuid = display.uuid();
let dpi = display.scale_factor();

println!(
"显示器 {}: UUID={:?}, 位置=({},{}), 尺寸={}x{}, DPI={}",
i, uuid,
bounds.origin.x, bounds.origin.y,
bounds.size.width, bounds.size.height,
dpi,
);
}
}

DisplayId - 显示器标识符

DisplayId 是一个不透明标识符,用于在 WindowOptions 中指定窗口显示的目标设备。可以通过 display.id() 获取。

display_id_usage.rs

rust 复制代码
/// 在第二个显示器上打开窗口
fn open_on_second_display(cx: &mut App) {
let displays = cx.displays();
if displays.len() >= 2 {
let second_display = &displays[1];

let options = WindowOptions {
display_id: Some(second_display.id()),
center: true,
kind: WindowKind::Normal,
..WindowOptions::default()
};

cx.open_window(options, |cx| {
cx.new(|_| MyApp)
});
}
}

Bounds 结构

Bounds 是 GPUI 中描述矩形区域的核心结构,用于窗口、屏幕和元素的定位和尺寸描述。

bounds_structure.rs

rust 复制代码
/// Bounds 结构定义(简化版)
struct Bounds<T> {
origin: Point<T>,   // 矩形左上角坐标
size:   Size<T>,    // 矩形宽度和高度
}

/// Point 和 Size 结构
struct Point<T> {
x: T,  // X 坐标
y: T,  // Y 坐标
}

struct Size<T> {
width:  T,  // 宽度
height: T,  // 高度
}

/// 常用类型别名
type ScreenBounds = Bounds<DevicePixels>;
type WindowBounds = Bounds<DevicePixels>;

屏幕方位方法

Bounds 提供了 8 个便捷方法用于获取矩形区域的关键坐标点。这在窗口定位、Popup 对齐等场景中非常实用。

方法 返回值 说明
screen.bounds().top_left() Point 左上角坐标
screen.bounds().top_right() Point 右上角坐标
screen.bounds().top_center() Point 上边中点坐标
screen.bounds().bottom_left() Point 左下角坐标
screen.bounds().bottom_right() Point 右下角坐标
screen.bounds().bottom_center() Point 下边中点坐标
screen.bounds().left_center() Point 左边中点坐标
screen.bounds().right_center() Point 右边中点坐标

这些方法可以组合使用,轻松计算窗口在各屏幕之间的位置:

bounds_positions.rs

rust 复制代码
/// 使用方位方法计算窗口位置
fn position_window_on_right_display(cx: &AppContext) -> Point<DevicePixels> {
let displays = cx.displays();
if displays.len() < 2 {
return (displays[0].bounds().origin);
}

let right_display = &displays[1];
let bounds = right_display.bounds();

// 获取右下角的坐标
let bottom_right = bounds.bottom_right();

// 获取左上角的坐标
let top_left = bounds.top_left();

println!("右下角: ({}, {})", bottom_right.x, bottom_right.y);
println!("左上角: ({}, {})", top_left.x, top_left.y);

top_left
}

window_positioning.rs 多屏定位示例

以下是完整的 window_positioning.rs 示例,展示了如何在不同显示器之间精确定位窗口。

window_positioning.rs - 完整示例

rust 复制代码
use gpui::*;

struct PositioningDemo {
display_info: Vec<DisplayInfo>,
}

struct DisplayInfo {
index: usize,
bounds: Bounds<DevicePixels>,
is_primary: bool,
}

impl PositioningDemo {
/// 获取所有显示器信息
fn scan_displays(cx: &AppContext) -> Vec<DisplayInfo> {
cx.displays().iter().enumerate().map(|(i, d)| DisplayInfo {
index: i,
bounds: d.bounds(),
is_primary: d.is_primary(),
}).collect()
}

/// 将窗口移动到指定显示器并居中
fn move_to_display(&mut self, display_idx: usize, cx: &mut WindowContext) {
if let Some(display) = self.display_info.get(display_idx) {
let screen_bounds = display.bounds;
let win_size = cx.window_bounds().size;

// 计算居中位置
let new_x = screen_bounds.origin.x + (screen_bounds.size.width - win_size.width) / 2.0;
let new_y = screen_bounds.origin.y + (screen_bounds.size.height - win_size.height) / 2.0;

cx.activate_window();
// 通过 resize 和 move 实现定位
}
}
}

窗口居中算法

窗口居中是 GUI 开发中的常见需求。以下是在指定区域(屏幕或父窗口)中居中窗口的通用算法。

center_window.rs

rust 复制代码
/// 通用窗口居中算法
fn center_bounds(
content_size: Size<DevicePixels>,
container_bounds: Bounds<DevicePixels>,
) -> Point<DevicePixels> {
let x = container_bounds.origin.x
+ (container_bounds.size.width - content_size.width) / 2.0;

let y = container_bounds.origin.y
+ (container_bounds.size.height - content_size.height) / 2.0;

Point::new(x, y)
}

/// 将窗口居中在当前屏幕上
fn center_on_current_screen(cx: &mut WindowContext) {
let window_bounds = cx.window_bounds();
let displays = cx.displays();

// 查找包含窗口当前中心的屏幕
let window_center = window_bounds.center();

for display in &displays {
let db = display.bounds();
if db.contains(&window_center) {
let new_origin = center_bounds(window_bounds.size, db);
// 设置新位置
break;
}
}
}

跨屏移动窗口

在多屏环境中,有时需要将窗口从一个屏幕移动到另一个。以下是跨屏移动窗口的实现方案。

cross_screen_move.rs

rust 复制代码
/// 将窗口移动到下一个屏幕
fn move_to_next_display(cx: &mut WindowContext) {
let displays = cx.displays();
if displays.len() <= 1 { return; }

let window_bounds = cx.window_bounds();
let window_center = window_bounds.center();

// 找到当前屏幕的索引
let mut current_idx = 0usize;
for (i, d) in displays.iter().enumerate() {
if d.bounds().contains(&window_center) {
current_idx = i;
break;
}
}

// 计算下一个屏幕(循环)
let next_idx = (current_idx + 1) % displays.len();

// 移动到下一个屏幕的对应相对位置
let current_d = displays[current_idx].bounds();
let next_d = displays[next_idx].bounds();

let rel_x = window_bounds.origin.x - current_d.origin.x;
let rel_y = window_bounds.origin.y - current_d.origin.y;

let new_origin = Point::new(
next_d.origin.x + rel_x,
next_d.origin.y + rel_y,
);

// 确保新位置在目标屏幕内
// ... 边界检查代码 ...
}

💡 提示: 多显示器 DPI 缩放处理

在多显示器环境中,不同屏幕可能有不同的 DPI 缩放因子(如 1x 外接显示器 + 2x Retina 内置屏)。关键注意事项:

  1. DevicePixels vs Pixels:display.bounds() 使用 DevicePixels(物理像素),而布局尺寸使用 Pixels(逻辑像素)。通过 display.scale_factor() 进行转换。
  2. 窗口跨屏 DPI 变化:当窗口从低 DPI 屏幕拖到高 DPI 屏幕时,GPUI 会自动处理缩放,但可能导致窗口外观短暂闪烁。
  3. 精确位置控制:如果需要精确像素对齐(如贴边吸附),应该使用 DevicePixels 而非 Pixels。
  4. 推荐做法:在 WindowOptions 中使用 DevicePixels 设定初始位置,让 GPUI 处理 DPI 转换。

高级

第22章:自定义窗口装饰

GPUI 允许完全自定义窗口的标题栏和边框,甚至可以完全替换系统原生装饰。通过 WindowDecorations 枚举和相关的拖拽/缩放 API,可以实现高度定制化的窗口外观。

WindowDecorations 枚举

WindowDecorations 控制窗口的标题栏和边框由谁来绘制。

枚举值 行为 适用场景
WindowDecorations::Server 由系统(窗口管理器)绘制原生标题栏和边框 标准应用窗口
WindowDecorations::Client 由 GPUI 自己渲染标题栏和边框区域 自定义 UI 风格(如 VS Code、Spotify)

window_decorations.rs

rust 复制代码
use gpui::*;

/// 使用客户端装饰创建窗口
fn create_client_decorated_window(cx: &mut App) {
let options = WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some(SharedString::from("自定义窗口")),
appears_transparent: true,  // 标题栏透明(仅 macOS)
}),
decorations: WindowDecorations::Client,
..WindowOptions::default()
};

cx.open_window(options, |cx| {
cx.new(|_| CustomWindow)
});
}

自定义标题栏实现

使用 div() 构建标题栏 UI,通过 start_window_move() 实现拖拽移动区域,通过 start_window_resize() 实现缩放边缘。

custom_titlebar.rs

rust 复制代码
use gpui::*;

struct CustomWindow {
title: SharedString,
is_maximized: bool,
}

impl Render for CustomWindow {
fn render(&mut self, window: &mut Window, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.size_full()
// 自定义标题栏
.child(
div()
.flex()
.flex_row()
.items_center()
.h(Pixels(32.0))
.bg(rgb(0x1e2030))
.px_3()
.// 点击此区域可以拖拽窗口
.on_mouse_down(MouseButton::Left, |event, cx| {
cx.start_window_move();
})
// 标题文字
.child(self.title.clone())
// 右侧按钮区(占满剩余空间)
.child(
div()
.flex()
.flex_row()
.gap_2()
.ml_auto()
// 最小化按钮
.child(div().child("─").on_click(|_, cx| { cx.minimize_window(); }))
// 最大化按钮
.child(div().child("□").on_click(|_, cx| { cx.maximize_window(); }))
// 关闭按钮
.child(div().child("✕").on_click(|_, cx| { cx.hide(); }))
)
)
)
// 主内容区域
.child(
div()
.flex()
.flex_1()
.bg(rgb(0x0f0f1a))
.items_center()
.justify_center()
.child("主内容区域")
)
}
}

ResizeEdge 枚举

在客户端装饰模式下,需要手动实现窗口缩放逻辑。ResizeEdge 枚举定义了 8 个缩放方向的边缘。

枚举值 位置 光标样式
ResizeEdge::TopLeft 左上角 ↖ 双向箭头
ResizeEdge::Top 上边缘 ↑ 垂直箭头
ResizeEdge::TopRight 右上角 ↗ 双向箭头
ResizeEdge::Right 右边缘 → 水平箭头
ResizeEdge::BottomRight 右下角 ↘ 双向箭头
ResizeEdge::Bottom 下边缘 ↓ 垂直箭头
ResizeEdge::BottomLeft 左下角 ↙ 双向箭头
ResizeEdge::Left 左边缘 ← 水平箭头

resize_handles.rs

rust 复制代码
/// 在窗口四周添加缩放手柄
fn create_resize_handles() -> impl IntoElement {
div()
.absolute()
.size_full()
.// 上边缘
.child(div()
.absolute()
.top(0).left(8).right(8).h(4)
.cursor(CursorStyle::ResizeUpDown)
.on_mouse_down(MouseButton::Left, |_, cx| {
cx.start_window_resize(ResizeEdge::Top);
})
)
// 下边缘
.child(div()
.absolute()
.bottom(0).left(8).right(8).h(4)
.cursor(CursorStyle::ResizeUpDown)
.on_mouse_down(MouseButton::Left, |_, cx| {
cx.start_window_resize(ResizeEdge::Bottom);
})
)
// 左边缘
.child(div()
.absolute()
.left(0).top(8).bottom(8).w(4)
.cursor(CursorStyle::ResizeLeftRight)
.on_mouse_down(MouseButton::Left, |_, cx| {
cx.start_window_resize(ResizeEdge::Left);
})
)
// 右边缘
.child(div()
.absolute()
.right(0).top(8).bottom(8).w(4)
.cursor(CursorStyle::ResizeLeftRight)
.on_mouse_down(MouseButton::Left, |_, cx| {
cx.start_window_resize(ResizeEdge::Right);
})
)
// 四个角的缩放手柄...
// 类似模式:TopLeft, TopRight, BottomLeft, BottomRight
// 使用对应的 CursorStyle
}

set_client_inset() - 设置客户区内边距

set_client_inset() 定义了窗口的"客户区"(内容渲染区域)与窗口边缘之间的间距。在客户端装饰模式下,这用于在视觉上创建标题栏和边框的效果。

client_inset.rs

rust 复制代码
/// 设置客户区域内边距
fn configure_client_inset(window: &mut Window) {
window.set_client_inset(Inset {
top:    Pixels(32.0),   // 标题栏高度
left:   Pixels(4.0),    // 左边框
right:  Pixels(4.0),    // 右边框
bottom: Pixels(4.0),    // 下边框
});
}

struct Inset<T> {
top:    T,
left:   T,
right:  T,
bottom: T,
}

window_shadow.rs 完整示例

以下是 window_shadow.rs 中的自定义窗口装饰完整示例代码。

window_shadow.rs - 完整示例

rust 复制代码
use gpui::*;

struct CustomDecoratedWindow {
title: SharedString,
}

impl CustomDecoratedWindow {
fn build_titlebar(&self) -> impl IntoElement {
div()
.flex()
.flex_row()
.items_center()
.h(Pixels(36.0))
.bg(rgb(0x1a1b2e))
.px_3()
.// 整个标题栏可拖拽
.on_mouse_down(MouseButton::Left, |_, cx| {
cx.start_window_move();
})
.child(self.title.clone())
.child(
div().flex().gap_1().ml_auto()
.child(div().w(12).h(12).rounded_full().bg(rgb(0xfbbf24))
.on_click(|_, cx| { cx.minimize_window(); }))
.child(div().w(12).h(12).rounded_full().bg(rgb(0x34d399))
.on_click(|_, cx| { cx.maximize_window(); }))
.child(div().w(12).h(12).rounded_full().bg(rgb(0xef4444))
.on_click(|_, cx| { cx.hide(); }))
)
}

fn build_resize_border() -> impl IntoElement {
div()
.absolute()
.inset_0()
.border(1)
.border_color(rgba(0x7c3aed, 0.3))
// 四个边缘各有 resize 手柄...
}
}

impl Render for CustomDecoratedWindow {
fn render(&mut self, _window: &mut Window, cx: &mut ViewContext<Self>) -> impl IntoElement {
let titlebar = self.build_titlebar();
// let resize_border = Self::build_resize_border();

div()
.flex()
.flex_col()
.h_full()
.child(titlebar)
.child(
div()
.flex()
.flex_1()
.bg(rgb(0x0f111a))
.items_center()
.justify_center()
.child("Custom Decorated Window")
)
}
}

窗口阴影

在自定义装饰模式下,窗口阴影需要手动实现。可以通过 GPUI 的阴影系统添加自定义窗口阴影效果。

custom_shadow.rs

rust 复制代码
/// 为自定义窗口添加阴影
fn window_with_shadow() -> impl IntoElement {
div()
.flex()
.flex_col()
.bg(rgb(0x1a1b2e))
.rounded_xl()
// 阴影效果(需要通过 canvas 或 border 模拟)
.border(1)
.border_color(rgba(0x7c3aed, 0.15))
// 或者使用 BoxShadow
// .shadow(BoxShadow::new()
//     .offset(Point::new(0, 8))
//     .blur(32)
//     .color(rgba(0x000000, 0.3)))
.child("带阴影的窗口内容")
}

⚠️ 注意: 自定义装饰的实现注意事项

必须手动实现拖拽和缩放逻辑:使用客户端装饰时,窗口的移动和缩放不会由系统自动处理。你需要:

  1. 使用 cx.start_window_move() 实现标题栏拖拽
  2. 使用 cx.start_window_resize(ResizeEdge) 实现 8 个方向的缩放
  3. 正确设置光标样式(CursorStyle)以匹配缩放方向
  4. 使用 set_client_inset() 定义内容区的有效边界

API 速查手册

本手册以表格形式整理了 GPUI 框架中最常用的 API,方便快速查阅。涵盖布局、样式、事件、焦点、状态管理、动画和列表等方面。

布局 API 速查表

GPUI 的布局系统基于 Flexbox 和 CSS Grid,提供声明式的链式调用 API。

API 说明 示例
flex() 启用 Flexbox 布局 div().flex()
flex_row() 水平排列子元素(主轴为行) div().flex().flex_row()
flex_col() 垂直排列子元素(主轴为列) div().flex().flex_col()
items_center() 交叉轴居中对齐 div().items_center()
items_start() 交叉轴起始对齐 div().items_start()
items_end() 交叉轴末尾对齐 div().items_end()
justify_center() 主轴居中对齐 div().justify_center()
justify_between() 主轴两端对齐(均匀分布) div().justify_between()
justify_around() 主轴环绕分布 div().justify_around()
gap(gap) 设置子元素间距 div().gap(px(8.))
gap_x(gap) 设置水平间距 div().gap_x(px(8.))
gap_y(gap) 设置垂直间距 div().gap_y(px(8.))
flex_1() 弹性填充剩余空间(flex: 1) div().flex_1()
flex_none() 不伸缩、不收缩 div().flex_none()
flex_shrink_0() 收缩比例为 0(不收缩) div().flex_shrink_0()
grid() 启用 CSS Grid 布局 div().grid()
grid_rows(rows) 设置 Grid 行数 div().grid().grid_rows(3)
grid_cols(cols) 设置 Grid 列数 div().grid().grid_cols(3)
absolute() 绝对定位(脱离文档流) div().absolute()
relative() 相对定位 div().relative()
top(n) 设置上边距/位置 div().top(px(10.))
left(n) 设置左边距/位置 div().left(px(10.))
right(n) 设置右边距/位置 div().right(px(10.))
bottom(n) 设置下边距/位置 div().bottom(px(10.))
size(size) 同时设置宽度和高度 div().size(px(100.))
size_full() 占满父容器 100% 宽高 div().size_full()
w(n) 设置宽度 div().w(px(200.))
h(n) 设置高度 div().h(px(40.))
min_w(n) / max_w(n) 最小/最大宽度 div().min_w(px(100.))
min_h(n) / max_h(n) 最小/最大高度 div().max_h(px(500.))
p(n) 四边内边距(padding) div().p(px(16.))
px(n) / py(n) 水平/垂直内边距 div().px(px(16.)).py(px(8.))
pt(n) / pr(n) / pb(n) / pl(n) 上/右/下/左内边距 div().pt(px(8.)).pl(px(12.))
m(n) 四边外边距(margin) div().m(px(8.))
mx(n) / my(n) 水平/垂直外边距 div().mx(px(8.)).my(px(4.))
ml_auto() 左侧自动外边距(推至右侧) div().ml_auto()
overflow_hidden() 溢出隐藏 div().overflow_hidden()
overflow_x_scroll() 水平溢出滚动 div().overflow_x_scroll()
overflow_y_scroll() 垂直溢出滚动 div().overflow_y_scroll()

样式 API 速查表

API 说明 示例
bg(color) 设置背景颜色 div().bg(rgb(0x7c3aed))
text_color(color) 设置文字颜色 div().text_color(rgb(0xffffff))
text_size(size) 设置文字大小 div().text_size(px(14.))
font_weight(weight) 设置字重 div().font_weight(600)
rgb(r, g, b) 创建 RGB 颜色 rgb(0x7c3aed)
rgba(r, g, b, a) 创建带透明度的颜色 rgba(0x000000, 0.5)
hsla(h, s, l, a) 创建 HSL 颜色 hsla(240., 0.8, 0.6, 1.0)
border(n) 设置边框宽度(像素) div().border(2)
border_color(color) 设置边框颜色 div().border_color(rgb(0xd4d4d8))
rounded(n) 设置圆角半径 div().rounded(px(8.))
rounded_xl() 大圆角(12px) div().rounded_xl()
rounded_full() 完全圆角(圆形/胶囊形) div().rounded_full()
rounded_t_full() 顶部完全圆角 div().rounded_t_full()
rounded_b_xl() 底部大圆角 div().rounded_b_xl()
shadow(BoxShadow) 设置盒阴影 div().shadow(BoxShadow::new().blur(8))
shadow_xs() 极小阴影(预设) div().shadow_xs()
shadow_sm() 小阴影(预设) div().shadow_sm()
shadow_md() 中等阴影(预设) div().shadow_md()
shadow_lg() 大阴影(预设) div().shadow_lg()
opacity(n) 设置不透明度(0.0 ~ 1.0) div().opacity(0.5)
visible_on_hover() 悬停时可见 div().visible_on_hover()
cursor(style) 设置光标样式 div().cursor(CursorStyle::PointingHand)
line_height(n) 设置行高 div().line_height(px(24.))

事件 API 速查表

API 说明 示例
on_click(handler) 鼠标点击事件 div().on_click(
on_hover(handler) 鼠标悬停/离开 div().on_hover(
on_mouse_down(button, handler) 鼠标按下事件 div().on_mouse_down(MouseButton::Left,
on_mouse_up(button, handler) 鼠标释放事件 div().on_mouse_up(MouseButton::Left,
on_drag(handler) 拖拽事件 div().on_drag(MouseButton::Left,
on_drop(handler) 放置事件 div().on_drop(
on_scroll(handler) 滚动事件 div().on_scroll(
on_key_down(handler) 键盘按下事件 div().on_key_down(
on_key_up(handler) 键盘释放事件 div().on_key_up(
cursor(style) 设置鼠标光标样式 div().cursor(CursorStyle::PointingHand)
cx.stop_propagation() 阻止事件冒泡
cx.prevent_default() 阻止默认行为

焦点 API 速查表

API 说明 示例
FocusHandle 焦点句柄类型 focus_handle: FocusHandle
cx.focus_handle() 创建焦点句柄 let handle = cx.focus_handle();
focus(handle) 绑定焦点句柄到元素 div().focus(handle)
window.focus(handle) 手动设置焦点 window.focus(&self.handle, cx)
focus_visible() 焦点可见性样式 div().focus_visible(
tab_index(n) 设置 Tab 导航顺序 div().tab_index(0)

状态 API 速查表

API 说明 示例
Entity 实体类型(有状态 UI 组件的核心) struct MyState { ... }
cx.new(f) 创建新 Entity cx.new(
entity.read(cx) 只读访问 Entity entity.read(cx).value
entity.update(cx, f) 可变访问 Entity 并触发重绘 entity.update(cx,
cx.notify() 通知框架需要重绘 cx.notify()
cx.subscribe(entity, handler) 订阅另一个 Entity 的变化 cx.subscribe(&other,
cx.observe(entity, handler) 观察 Entity 属性变化 cx.observe(&entity,
Model 共享状态模型 let model = cx.new_model(

动画 API 速查表

API 说明 示例
Animation 动画类型 let anim = Animation::new(300);
with_animation(id, key, value) 元素动画属性绑定 div().with_animation("fade", "opacity", 1.0)
easing(easing_fn) 设置缓动函数 Animation::new(300).easing(ease_in_out)
Transformation 变换(平移/旋转/缩放) div().with_animation("move", "translation", Point::new(100., 0.))
cx.on_next_frame(f) 下一帧回调 cx.on_next_frame(

列表 API 速查表

API 说明 示例
uniform_list(builder, count) 等高虚拟列表(高性能) uniform_list(cx, "items", 1000,
ListState 列表状态(选中项、滚动位置) list_state: ListState
list(builder) 普通列表(非等高) list(cx, "items",
UniformListScrollHandle 均匀列表滚动控制句柄 scroll_handle: UniformListScrollHandle
scroll_handle.scroll_to(idx) 滚动到指定索引 handle.scroll_to(50);

示例代码索引

GPUI 框架附带了 30+ 个示例文件,覆盖了从基础到高级的所有核心功能。以下是所有示例的完整索引。

所有示例文件一览

  • hello_world.rs (入门 ⭐) --- 入门 Hello World,创建第一个 GPUI 窗口

  • animation.rs (中级 ⭐⭐) --- 动画系统,演示过渡动画和缓动函数

  • anchor.rs (中级 ⭐⭐) --- 锚点定位,演示绝对定位锚点系统

  • data_table.rs (高级 ⭐⭐⭐) --- 大数据表格,展示虚拟化表格处理大量数据

  • drag_drop.rs (中级 ⭐⭐) --- 拖拽功能,演示元素的拖拽和放置

  • focus_visible.rs (中级 ⭐⭐) --- 焦点可见性,展示键盘焦点环和焦点样式

  • gif_viewer.rs (高级 ⭐⭐⭐) --- GIF 动图查看器,加载和播放 GIF 动画

  • gradient.rs (中级 ⭐⭐) --- 渐变色,线性渐变和色彩空间对比

  • grid_layout.rs (中级 ⭐⭐) --- CSS Grid 布局,演示 Grid 布局系统

  • image_gallery.rs (中级 ⭐⭐) --- 图像画廊,展示图片的网格浏览体验

  • image_loading.rs (中级 ⭐⭐) --- 图像加载,异步加载和显示外部图片

  • input.rs (中级 ⭐⭐) --- 文本输入,演示文本输入框的交互

  • list_example.rs (中级 ⭐⭐) --- 列表示例,展示普通列表的创建和使用

  • mouse_pressure.rs (高级 ⭐⭐⭐) --- 鼠标压力,响应触控笔的压力感应

  • opacity.rs (中级 ⭐⭐) --- 透明度,演示元素透明度和淡化效果

  • ownership_post.rs (高级 ⭐⭐⭐) --- Entity 所有权,理解 GPUI 中的所有权模型

  • painting.rs (高级 ⭐⭐⭐) --- 底层绘图,自定义 Canvas 绘制路径和图形

  • pattern.rs (中级 ⭐⭐) --- 图案填充,斜线、网格、点状图案

  • popover.rs (中级 ⭐⭐) --- 浮层,Popover 弹出层的创建和定位

  • scrollable.rs (中级 ⭐⭐) --- 滚动视图,演示可滚动区域的使用

  • set_menus.rs (中级 ⭐⭐) --- 菜单栏,创建和管理应用菜单

  • shadow.rs (中级 ⭐⭐) --- 阴影效果,BoxShadow 的各种用法

  • tab_stop.rs (中级 ⭐⭐) --- Tab 停止,键盘 Tab 导航顺序控制

  • text.rs (中级 ⭐⭐) --- 文字排版,富文本渲染和文字样式

  • text_layout.rs (中级 ⭐⭐) --- 文本布局,多段落文字布局和排版

  • text_wrapper.rs (中级 ⭐⭐) --- 文本换行,自动换行和文字超出处理

  • tree.rs (高级 ⭐⭐⭐) --- 树形控件,可折叠的树形结构 UI

  • uniform_list.rs (高级 ⭐⭐⭐) --- 等高虚拟列表,高性能大数据列表渲染

  • window.rs (系统 ⭐⭐⭐) --- 窗口管理,多窗口创建和配置

  • window_positioning.rs (系统 ⭐⭐⭐) --- 多屏定位,多显示器中的窗口定位

  • window_shadow.rs (高级 ⭐⭐⭐) --- 窗口阴影,自定义窗口装饰和阴影

难度分布统计

难度 数量 示例文件
入门 ⭐ 1 hello_world
中级 ⭐⭐ 18 animation, anchor, drag_drop, focus_visible, gradient, grid_layout, image_gallery, image_loading, input, list_example, opacity, pattern, popover, scrollable, set_menus, shadow, tab_stop, text, text_layout, text_wrapper
高级 ⭐⭐⭐ 10 data_table, gif_viewer, mouse_pressure, ownership_post, painting, tree, uniform_list, window, window_positioning, window_shadow

💡 提示: 学习建议

建议按以下顺序浏览示例:

  1. 先运行 hello_world.rs 确认环境正常
  2. 然后尝试 text.rsinput.rs 了解基本 UI
  3. 接着学习 grid_layout.rs、anchor.rs 掌握布局
  4. 再深入 animation.rs、drag_drop.rs 学习交互
  5. 最后挑战 tree.rs、uniform_list.rs 等高级示例
相关推荐
千纸鹤の脉搏1 小时前
多线程的初步使用
java·开发语言·学习·多线程
一楼的猫1 小时前
茄子写作助手是什么——网文作者长篇小说AI创作工具完整说明
人工智能·学习·机器学习·chatgpt·ai写作
AI_零食1 小时前
HarmonyOS-鸿蒙原生 ArkTS 布局系统:width(‘100%‘) 的本质与 padding 陷阱
前端·学习·华为·harmonyos·鸿蒙
Kobebryant-Manba2 小时前
学习自定义层&读写文件&使用gpu
学习
好家伙VCC2 小时前
Rust+Bioinfo:80ms极速SNP注释引擎
java·开发语言·算法·rust
啦哈拉哈2 小时前
【Python】知识点零碎学习7
python·学习·算法
Fuly10242 小时前
LangGraph学习-(1)跑通一个最小状态图
数据库·学习
段一凡-华北理工大学2 小时前
工业领域的Hadoop架构学习~系列文章19:能源行业Hadoop应用实践
大数据·人工智能·hadoop·分布式·学习·架构·高炉炼铁
syagain_zsx2 小时前
Linux进程控制学习总结(1/2)
linux·运维·学习