本篇以仓库根目录 README.md 为骨架,结合 druid 源码、druid-shell 与 druid-derive 实现细节,系统讲解 Druid 的核心架构(druid-shell / piet / widgets / layout / Data / Lens)、快速上手方式、平台支持与构建配置,以及其项目现状与继承者 Xilem。读完你将掌握如何在 Rust 项目中接入 Druid、理解其>use druid::widget::{Button, Flex, Label}; use druid::{AppLauncher, LocalizedString, PlatformError, Widget, WidgetExt, WindowDesc}; fn main() -> Result<(), PlatformError> { let main_window = WindowDesc::new(ui_builder()); let data = 0_u32; AppLauncher::with_window(main_window) .log_to_console() .launch(data) } fn ui_builder() -> impl Widget<u32> { // The label text will be computed dynamically based on the current locale and count let text = LocalizedString::new("hello-counter").with_arg("count", |data: &u32, _env| (*data).into()); let label = Label::new(text).padding(5.0).center(); let button = Button::new("increment") .on_click(|_ctx, data, _env| *data += 1) .padding(5.0); Flex::column().with_child(label).with_child(button) }
这个例子直观展示了>druid = "0.8.3"
在 druid/src/lib.rs 的文档中,还给出了带 feature 的写法:
[dependencies.druid] version = "0.8.3" features = ["im", "svg", "image"]平台说明
Linux:Druid 依赖 gtk+3,需要先安装 GTK 开发库。基于 Ubuntu 的发行版执行:
sudo apt-get install libgtk-3-devOpenBSD:同样需要 gtk+3,通过包管理器安装:
pkg_add gtk+3此外还有一个X11 后端可供尝试,但目前缺失不少功能,可通过 feature 启用:
cargo run --features=x11注意 druid/Cargo.toml 中默认 feature 为
gtk,x11与wayland都属于非默认的可选后端。Feature 矩阵在源码中有明确声明:wayland被标注为"not ready for the prime time. Many things don't work yet."(非常实验性)。可选的 feature 体系
从 druid/src/lib.rs 的 "Optional Features" 文档与 druid/Cargo.toml 的
[features]段可以整理出完整的 feature 矩阵:工具类 feature:
im:高效不可变数据结构(基于imcrate),以druid::im模块 re-export;svg:基于usvg/resvg/tiny-skia的可缩放矢量图支持(图标等);image:基于imagecrate 的位图支持;x11:实验性 X11 后端(替代 GTK);wayland:实验性 Wayland 后端;serde:部分内部类型(主要是 kurbo 基础几何类型)的 Serde 支持;raw-win-handle:为WindowHandle实现HasRawWindowHandle;chrono:时间类型(chrono::Duration、NaiveDate等)的Data实现。图片格式 feature:
png、jpeg、gif、bmp、ico、tiff、webp、pnm、dds、tga、hdr,以及一键开启全部格式的image-all。源码注释特别说明:不支持 AVIF,因为 Druid 仅使用图片的decode(解码)能力。Windows 应用注意:默认情况下 Windows 会为应用同时打开控制台窗口;若不需要,在 crate 顶部添加
#![windows_subsystem = "windows"]。设计目标(Goals)与非目标(Non-Goals)
Goals
Druid 的目标是让开发者在所有常见平台上轻松编写并部署体验流畅顺滑的高质量桌面应用,为此追求:
- 在所有受支持平台上易于构建与打包;
- 提供抽象层以规避平台特有的怪异差异(quirks);
- 尊重平台约定与用户预期;
- 以很小的成本可靠处理显示分辨率与缩放;
- 支持简单而强大的国际化(i18n);
- 提供稳健的无障碍(accessibility)支持;
- 产出小体积、快速度、低内存占用的二进制;
- 拥有小依赖树、高质量代码库与良好组织;
- 聚焦于强大的桌面级应用;
- 提供灵活的布局与常用控件集合;
- 便于按需创建自定义组件与应用逻辑。
Non-Goals
为了达成上述目标,Druid 不可能支持所有场景,README 明确列出了非目标及社区替代方案:
- 使用平台原生控件或模仿它们 → 替代:[Relm]、[Slint]
- 轻易嵌入自定义渲染管线 → 替代:[Conrod]
- 遵循特定架构风格(如 Elm)→ 替代:[Iced]、[Relm]
- 面向 Web 时渲染成 HTML → 替代:[Iced]、[Moxie]
Druid 只是当时众多 [Rust-native GUI 实验] 之一。
核心概念与源码级解析
druid-shell:平台抽象层
Druid 通过druid-shell获得平台抽象的应用外壳。它的职责是:启动原生平台 runloop、监听事件、将其转换为平台无关的表示、并调用用户提供的处理器。
在 druid-shell 中可以看到完整的后端矩阵:
backend/gtk、backend/mac、backend/x11、backend/wayland、backend/web、backend/windows,以及共享的键盘/定时器抽象。每个后端都提供application、window、clipboard、menu、dialog、screen等模块。虽然 druid-shell 是为 Druid 工具包开发的,但它被设计得足够通用,可供其他 Rust GUI 实验项目复用——例如 druid-shell/examples/shello.rs 就是一个不依赖 druid 的独立示例。
piet:2D 图形与文本布局
Druid 依赖Piet 库进行绘制与文本布局。Piet 是 2D 图形抽象,拥有多个后端:
piet-direct2d、piet-coregraphics、piet-cairo、piet-web、piet-svg。Druid 的平台映射为:
- macOS →
piet-coregraphics- Linux / OpenBSD / FreeBSD →
piet-cairo- Windows →
piet-direct2d- Web →
piet-webREADME 给出了一个直接使用 piet/kurbo API 的绘制示例(通常在自定义控件的
paint中调用,ctx.size()返回当前绘制区域的布局尺寸):use druid::kurbo::{BezPath, Point, Rect}; use druid::piet::Color; // Create an arbitrary bezier path // (ctx.size() returns the size of the layout rect we're painting in) let mut path = BezPath::new(); path.move_to(Point::ORIGIN); path.quad_to( (80.0, 90.0), (ctx.size().width, ctx.size().height), ); // Create a color let stroke_color = Color::rgb8(0x00, 0x80, 0x00); // Stroke the path with thickness 1.0 ctx.stroke(path, &stroke_color, 1.0); // Rectangles: the path for practical people let rect = Rect::from_origin_size((10., 10.), (100., 100.)); // Note the Color:rgba8 which includes an alpha channel (7F in this case) let fill_color = Color::rgba8(0x00, 0x00, 0x00, 0x7F); ctx.fill(rect, &fill_color);在 druid/src/lib.rs 中,
kurbo与piet通过pub use druid_shell::{kurbo, piet};直接 re-export,并额外导出Affine、Insets、Point、Rect、Size、Vec2、Color、ImageBuf、LinearGradient、RadialGradient、RenderContext、UnitPoint等常用绘图类型,因此用户通常不需要直接依赖 piet/kurbo。widgets:控件与 Widget trait
Druid 中的控件(文本框、按钮、布局组件等)都是实现了
Widgettrait的对象。该 trait 以关联数据类型T参数化:所有 trait 方法都能访问这份数据,其中event拿到的是可变引用,事件可以直接更新数据。每当应用数据变化时,框架会以
update方法遍历控件层级。所有 trait 方法都配有对应的上下文(EventCtx、LifeCycleCtx、UpdateCtx、LayoutCtx、PaintCtx),控件通过调用上下文的方法来请求行为;同时所有方法都提供环境Env,其中包含当前主题参数(颜色、尺寸等)。README 展示了
Widgettrait 的完整签名(以Button<T>为例):impl<T: Data> Widget<T> for Button<T> { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { ... } fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { ... } fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { ... } fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { ... } fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { ... } }从 druid/src/widget/widget.rs 的源码文档可以进一步看到各方法的职责细节:
event:处理Event枚举中的各类事件,可以请求EventCtx行为、修改数据或提交Command;lifecycle:处理与控件图结构或控件自身状态相关的生命周期通知,不应在此时修改应用状态(需要修改时可提交Command延后执行);update:数据或环境变化时调用,可通过UpdateCtx::request_paint/request_layout精确安排重绘;旧数据值会被传入以便计算细粒度增量;可通过env_changed/env_key_changed判断Env变化;layout:叶子控件确定满足约束的尺寸并返回;容器控件递归调用子控件的WidgetPod::layout并设置原点,布局策略深受 Flutter 启发;paint:通过PaintCtx(deref 到RenderContext)绘制外观;容器可在递归子控件前绘制背景、之后绘制覆盖层(如滚动条),并可施加蒙版与变换(对滚动尤其有用)。容器控件通常不直接调用子控件的 trait 方法,而是持有包裹在
WidgetPod中的子控件并调用WidgetPod上的对应方法——WidgetPod负责决定是否递归等逻辑。源码中Widget还隐含了WidgetId(基于NonZeroU64的唯一标识,自动为参与布局的每个WidgetPod分配,可用于通过submit_command+Target在控件间通信)。Druid 提供了大量基础工具与布局控件,见 druid/src/widget 目录(
Flex、Label、Button、TextBox、Scroll、Split、Tabs、List、SizedBox、Padding、Align等),也很容易实现自定义控件,或将控件组合成新控件:fn build_widget() -> impl Widget<u32> { let mut col = Flex::column(); for i in 0..30 { let button = Button::new(format!("Button {}", i).padding(5.0); col.add_child(button); } Scroll::new(col) }(注:README 原文中该示例的
Button::new(...)少了一个右括号,实际编译时应写为Button::new(format!("Button {}", i)).padding(5.0)。)layout:Flutter 风格的盒约束布局
Druid 的布局协议强烈借鉴了 Flutter 的 box layout model。控件会收到一个
BoxConstraints,提供布局的最小与最大尺寸;控件还负责为子控件计算合适的约束(如果适用)。从 druid/src/box_constraints.rs 源码可以看到:
BoxConstraints内部就是min: Size与max: Size两个字段;- 约束总是朝远离零的方向取整到整数,以实现像素级精确布局(
Size::expand);- 提供
UNBOUNDED(任意非负尺寸都可满足)、new、tight(只能满足唯一尺寸)、loosen(最小尺寸归零)、constrain(把尺寸 clamp 进约束区间)等构造与操作方法;Widget::layout方法签名中的bc: &BoxConstraints即为该约束对象。容器的典型流程是:递归调用子控件的
layout(可自由选择顺序,例如先算非 flex 控件的尺寸,再决定 flex 控件可用的剩余空间),计算布局后对每个子控件调用set_origin,最后返回容器自身的尺寸。为了效率,容器应对每个子控件只调用一次 layout。data:Data trait 与值类型
Druid 用
Datatrait表示值类型(value types)。这些类型应当比较便宜、克隆便宜。核心方法只有一个same:fn same(&self, other: &Self) -> bool;从 druid/src/data.rs 的 trait 文档可以提炼关键语义:
same是设计为总是快速的操作;若返回true则两个值必须相等,但两个相等的值不一定same(例如分别分配的两份拷贝通常不视为 same);- 这里 "equal" 的含义与
PartialEq略有不同,例如两个 NaN 浮点数在位表示相同时应视为相等——因此f32/f64的same用to_bits()比较而非==;- 标准库中大量基础类型(整数、
bool、char、String、时间类型、Arc/Rc(指针相等)、Option、Result、元组、数组、Range族)以及 kurbo 几何类型、piet 颜色/字体类型都已实现Data;std集合类型没有Data实现,因为比较它们可能很昂贵。有两个简单替代方案:把集合包进Arc,或开启imfeature——它会为imcrate 的不可变集合(Vector、HashMap、OrdMap等)添加Data实现。im的实现对Vector做了优化:小到可以内联(inline)时逐元素比较,否则用指针相等ptr_eq(见 druid/src/data.rs 第 562-573 行)。一般情况下可以直接用
derive生成Data实现:#[derive(Clone, Data)] struct AppState { which: bool, value: f64, }derive 宏还支持字段属性(详见 druid-derive 的 derive 文档与 tests/data.rs 测试):
#[data(ignore)]:跳过该字段的same比较(如缓存、时间戳等不属于数据模型的部分);#[data(same_fn = "path")]:为某字段指定自定义比较函数,签名需为fn(&T, &T) -> bool;- 实际使用中还有
#[data(eq)]等便捷属性(文档示例中用#[data(eq)]让无Data实现的PathBuf直接按相等性比较);- C 风格枚举(纯单元变体)生成的实现会检查相等性,因此此类类型还必须实现
PartialEq。
Data是 Druid 数据流的核心:正因为有了same,框架才能判断数据是否真的变化,从而决定是否触发update遍历(Widgettrait 的T: Data约束即源于此)。lens:聚焦大数据结构的一部分
Lens数据类型用于访问更大的数据结构中的一部分。与Data一样,它也可以通过 derive 生成:派生出的 lens 以与字段同名的关联常量形式访问。#[derive(Clone, Data, Lens)] struct AppState { which: bool, value: f64, }要使用 lens,用
LensWrap包裹控件即可(注意AppState::value由 CamelCase 类型名自动转换而来):LensWrap::new(WidgetThatExpectsf64::new(), AppState::value);对于结构体、元组和可索引容器,还可以随时用
lens!宏按需构造 lens:LensWrap::new(WidgetThatExpectsf64::new(), lens!(AppState, value));这在处理定义在另一个 crate 里的类型时特别有用(无法给别人的类型加 derive)。
从 druid/src/lens/lens.rs 源码看,
Lens<T, U>trait 只有两个方法,都以闭包而非返回值的方式工作:
with(&self, data: &T, f: FnOnce(&U) -> V) -> V:对字段做只读访问。之所以设计成闭包形式而非直接返回引用,是为了允许 lens 按需临时合成数据;with_mut(&self, data: &mut T, f: FnOnce(&mut U) -> V) -> V:对字段做可变访问。设计成闭包是因为要配合值类型/不可变数据结构使用——例如不可变 list 的 lens 可以先克隆 list、让闭包修改克隆、闭包返回后再更新原引用。
LensExt提供了组合与变换能力(见 druid/src/lens/mod.rs 导出的Field、Identity、Index、Map、Then、Deref、InArc、Constant、Unit等):
get/put:拷贝或写回目标值;then:将Lens<A, B>与Lens<B, C>组合成Lens<A, C>(如lens!(Foo, x).then(lens!((u32, bool), 1)));map:用一对正反函数把一个不存在于原始结构中的计算值适配成 lens(例如把 0-2 范围的值映射给范围 0-1 的Slider使用);deref:调用类型的Deref实现;- 还有
index等针对可索引容器的 lens。
WidgetExt::lens扩展方法(如TextBox::new().lens(MyState::search_term))内部即用LensWrap包装,让一个Widget<String>变成Widget<MyState>,典型场景是把TextBox嵌入数据模型不是String的更大结构中。应用启动流程:AppLauncher 源码解读
README 示例中的
AppLauncher::with_window(...).log_to_console().launch(data)背后,是 druid/src/app.rs 中的完整启动链路:
AppLauncher::with_window(window: WindowDesc<T>):用给定窗口描述创建启动器(内部维护窗口列表、可选的env_setup、l10n 资源、AppDelegate与扩展事件宿主);WindowDesc::title(impl Into<LabelText<T>>):设置窗口标题,接受String、LocalizedString或计算字符串的闭包,会随应用状态变化保持更新;不调用时默认标题为LocalizedString::new("app-name");WindowConfig::window_size(size: impl Into<Size>):设置初始窗口尺寸(单位为显示点 display points,平台可能因 DPI 略有调整;Windows 上另有兼容处理);log_to_console():等价于start_console_logging(true);launch(data: T):真正创建Application、构建Env(含 i18n 资源)、装配控件树并启动平台 runloop;若窗口无法实例化则返回PlatformError(通常是致命错误)。这也解释了为什么
main()的返回类型是Result<(), PlatformError>。总结:何时值得学习 Druid
Druid 的源码(约 40 余个示例、
druid-shell的六套后端、druid-derive的 derive 宏与 UI 测试、以及 druid/tests 中的布局/失效测试)是一份质量相当高的 Rust GUI 教学材料。它的>赞
- 跨平台
- 桌面应用
- UI组件
【免费下载链接】druid
A>项目地址:https://gitcode.com/gh_mirrors/drui/druid