引言
Freya 0.4 和 0.3 有着本质区别
Freya 0.4 是一次彻底重写。最大的变化是:
对比项:
v0.3(稳定版)
v0.4(开发版)
底层依赖:
依赖 Dioxus(VDOM、RSX 宏、Hooks)
完全独立,自带响应式核心
UI 写法:
rsx! { rect { … } } 宏语法
rect().width(…).child(…) Builder 链式调用
状态管理:
use_signal
use_state(自带 read() / write())
依赖数量:
需要同时引入 freya + dioxus
只需 freya 一个依赖
宏:
大量使用 rsx!、#[component] 等
完全不用宏,纯 Rust 代码
简单说:0.4 的写法就是纯 Rust 方法链,没有任何宏魔法,IDE 补全更友好,编译报错也更清晰。
⚠️ Freya 0.4 截至 2026 年 3 月仍处于 alpha 阶段(0.4.0-alpha.3),API 可能继续变化。本教程基于该版本的公开示例和文档编写。
环境准备
前提条件: 已安装 Rust 工具链(rustup)
平台额外依赖:
平台 需要安装
Windows Visual Studio Build Tools(推荐 MSVC 工具链)
macOS Xcode 命令行工具:xcode-select --install
Linux fontconfig、libxcb、libxkbcommon、mesa 开发库
创建项目
cargonew freya-hellocdfreya-hello编辑 Cargo.toml,添加依赖。由于 0.4 尚未发布到 crates.io 稳定版,需从 Git 仓库引入:
[dependencies] freya = { git = "https://github.com/marc2332/freya", branch = "main" }如果网络访问 GitHub 较慢,可配置 Git 代理或使用镜像。
Hello World —— 第一个窗口
打开 src/main.rs,写入以下代码:
usefreya::prelude::*;fnmain(){launch(LaunchConfig::new().with_window(WindowConfig::new(app).with_size(400.0,300.0)));}fnapp()->implIntoElement{rect().width(Size::fill()).height(Size::fill()).background((30,50,90)).color((255,255,255)).center().font_size(32.0).child("Hello Freya 0.4!")}运行:
cargorun编译完成后会弹出一个 400×300 的窗口,深蓝色背景上居中显示白色文字 “Hello Freya 0.4!”。
核心语法逐一拆解
(1)启动流程:launch + LaunchConfig + WindowConfig
launch(LaunchConfig::new()// 创建启动配置.with_window(WindowConfig::new(app)// 指定根组件函数.with_size(400.0,300.0)// 窗口宽高));三层结构很清晰:
- LaunchConfig:全局启动配置(后续可加主题、日志等)
- WindowConfig::new(app):绑定根组件函数 app
- .with_size(w, h):设置窗口尺寸
(2)根组件函数签名
fnapp()->implIntoElement{...}- 返回类型是 impl IntoElement,不是 0.3 的 Element
- 不需要任何 #[component] 宏标注
- 就是一个普通的 Rust 函数
(3)Builder 模式构建 UI
0.4 的核心变化——所有 UI 元素都通过函数调用 + 方法链构建:
rect()// 创建一个矩形容器.width(Size::fill())// 宽度撑满父容器.height(Size::fill())// 高度撑满父容器.background((30,50,90))// 背景色 RGB.color((255,255,255))// 文字颜色.center()// 内容水平+垂直居中.font_size(32.0)// 字体大小.child("Hello Freya 0.4!")// 添加子元素(文本)对比 0.3 的 RSX 宏写法:
// 0.3 宏写法(已淘汰)
rsx!{rect{width:"100%",height:"100%",background:"rgb(30, 50, 90)",color:"white",main_align:"center",cross_align:"center",font_size:"32","Hello Freya 0.4!"}}Builder 模式的好处:**
- 纯 Rust 语法,不需要学习特殊的 DSL
- IDE 自动补全所有可用方法
- 编译错误信息更精准,直接指向具体方法调用
##(4)常用尺寸类型 Size**
写法 含义
Size::fill() 撑满父容器(等同于 0.3 的 “100%”)
Size::percent(50.0) 占父容器的 50%
Size::flex(1.0) 弹性分配剩余空间
Size::Pixels(100.0) 固定 100 像素
(5)颜色表示
0.4 直接用元组表示颜色,不需要字符串:
.background((30,50,90))// RGB.background((30,50,90,200))// RGBA(第4个是透明度).color((255,255,255))// 白色文字(6)布局方向与对齐
rect().horizontal()// 子元素水平排列(默认是垂直).spacing(12.0)// 子元素间距 12px.center()// 内容居中.child(...).child(...)状态管理:use_state
0.4 用 use_state 替代了 0.3 的 use_signal。核心区别是读写分离:
letmutcount=use_state(||0);// 创建状态,初始值为 0count.read()// 读取当前值(返回引用)count.write()// 获取可写引用,修改后自动触发 UI 刷新完整计数器示例:
usefreya::prelude::*;fnmain(){launch(LaunchConfig::new().with_window(WindowConfig::new(app).with_size(400.0,350.0)));}fnapp()->implIntoElement{letmutcount=use_state(||0);// 上半部分:显示数字letdisplay=rect().width(Size::fill()).height(Size::percent(50.0)).center().background((15,163,242)).color((255,255,255)).font_size(75.0).font_weight(FontWeight::BOLD).shadow((0.0,4.0,20.0,4.0,(0,0,0,80))).child(count.read().to_string());// 下半部分:按钮区域letactions=rect().horizontal().width(Size::fill()).height(Size::percent(50.0)).center().spacing(8.0).child(Button::new().on_press(move|_|{*count.write()+=1;}).child("增加")).child(Button::new().on_press(move|_|{*count.write()-=1;}).child("减少"));// 根容器:上下堆叠rect().child(display).child(actions)}关键语法点解读:
- count.read().to_string():读取状态值并转为字符串,作为文本子元素显示
- *count.write() += 1:解引用可写引用,直接修改值。修改后框架自动检测到变化,重新调用 app() 刷新 UI
- Button::new().on_press(…):内置按钮组件,.on_press() 绑定点击回调
- .child(“增加”):给按钮添加文本内容
自定义组件
在 0.4 中,任何实现了 Component trait 的类型都可以作为组件:
#[derive(PartialEq)]structGreetingCard;implComponentforGreetingCard{fnrender(&self)->implIntoElement{rect().width(Size::fill()).height(Size::fill()).background((40,40,60)).color((255,255,255)).center().font_size(28.0).child("这是一个自定义组件")}}然后在其他地方使用:
fnapp()->implIntoElement{rect().width(Size::fill()).height(Size::fill()).child(GreetingCard)}注意:
- 组件结构体需要 #[derive(PartialEq)]
- 根组件 app 可以只是一个普通函数,不需要实现 trait
- 子组件通过 .child(组件名) 嵌入
常用样式方法速查
方法 作用 示例
.width(Size) 宽度 .width(Size::fill())
.height(Size) 高度 .height(Size::percent(50.0))
.background(color) 背景色 .background((30, 50, 90))
.color(color) 文字颜色 .color((255, 255, 255))
.font_size(f32) 字号 .font_size(24.0)
.font_weight(FontWeight) 字重 .font_weight(FontWeight::BOLD)
.center() 内容居中 .center()
.horizontal() 水平布局 .horizontal()
.spacing(f32) 子元素间距 .spacing(12.0)
.shadow(…) 阴影 .shadow((0., 4., 20., 4., (0,0,0,80)))
.child(…) 添加子元素 .child(“文本”) 或 .child(rect())
课后练习
练习 1:基础修改
把 Hello World 的背景色改成深红色 (180, 40, 40),文字改成 “你好,Freya 0.4!”,字号改为 28。
练习 2:简单交互
创建一个窗口,中间显示文字 “未点击”。点击文字所在区域后变成 “已点击”,再次点击变回 “未点击”。
提示:给 rect 添加 .onclick(move |_| { … }) 事件,用 use_state 管理一个布尔值。
练习 3:三色灯
实现一个窗口,垂直排列三个按钮,分别标记 “红”、“绿”、“蓝”。点击任意按钮后,整个窗口背景色变为对应颜色。
**提示:**用 use_state 存储一个颜色元组,三个按钮的 on_press 分别写入不同的颜色值。
常见问题
- 编译报 unresolved import freya::prelude:确认 Cargo.toml 中依赖指向的是 main 分支,且 cargo update 拉取了最新代码
- 窗口弹不出来:确认 main 函数中调用了 launch(…),且传入了 WindowConfig::new(app)
- 状态修改后 UI 不刷新:确认是通过 *count.write() = 新值 修改的,而不是只调用了 count.read()
- Button 找不到:Button 是 freya::prelude:😗 中导出的内置组件,确保 use freya::prelude:😗; 在文件顶部
第 1 课的内容就到这里。完成练习后可以继续学习第 2 课的布局进阶(Flex 布局、嵌套容器、滚动视图等),有任何问题随时问我。
贴代码
Freya UI 0.4 第1课 —— 课后练习答案与知识点总结
练习 1:基础修改
题目要求: 背景色改成深红色 (180, 40, 40),文字改成 “你好,Freya 0.4!”,字号改为 28。
usefreya::prelude::*;fnmain(){launch(LaunchConfig::new().with_window(WindowConfig::new(app).with_size(400.0,300.0)));}fnapp()->implIntoElement{rect().width(Size::fill()).height(Size::fill()).background((180,40,40))// ← 深红色背景.color((255,255,255)).center().font_size(28.0)// ← 字号 28.child("你好,Freya 0.4!")// ← 中文文字}考查点:
- background() 接收 RGB 元组,三个值分别对应红、绿、蓝通道(0~255)
- font_size() 接收 f32 类型,不是字符串
- .child(“文本”) 直接传入字符串即可创建文本节点
练习 2:简单交互
题目要求: 中间显示 “未点击”,点击后变成 “已点击”,再次点击变回 “未点击”。
usefreya::prelude::*;fnmain(){launch(LaunchConfig::new().with_window(WindowConfig::new(app).with_size(400.0,300.0)));}fnapp()->implIntoElement{// 创建一个布尔状态,初始值为 false(未点击)letmutclicked=use_state(||false);// 根据状态决定显示的文字lettext=if*clicked.read(){"已点击"}else{"未点击"};rect().width(Size::fill()).height(Size::fill()).background((50,50,80)).color((255,255,255)).center().font_size(32.0)// 绑定点击事件:每次点击取反布尔值.onclick(move|_|{letmutval=clicked.write();*val=!*val;}).child(text)}考查点:
- use_state(|| false) 创建布尔状态,闭包返回初始值
- clicked.read() 返回 &bool,用 * 解引用后做 if 判断
- clicked.write() 返回 &mut bool,用 *val = !*val 取反
- .onclick(move |_| { … }) 绑定点击回调,move 关键字把 clicked 的所有权移入闭包
- 状态变化后框架自动重新调用 app() 函数,UI 随之刷新
易错点:
- 忘记 move 关键字 → 编译器报生命周期错误
- 用 clicked.read() 去修改值 → 编译报错,读引用不能写
- 直接写 clicked = !clicked → clicked 不是 bool,是状态句柄
练习 3:三色灯
题目要求: 三个按钮分别标记 “红”、“绿”、“蓝”,点击后整个窗口背景变为对应颜色。
usefreya::prelude::*;fnmain(){launch(LaunchConfig::new().with_window(WindowConfig::new(app).with_size(400.0,400.0)));}fnapp()->implIntoElement{// 状态存储当前背景色,默认为深灰色letmutbg_color=use_state(||(50u8,50u8,50u8));// 三个按钮letbtn_red=Button::new().on_press({letbg=bg_color.clone();move|_|{*bg.write()=(200,50,50);}}).child("红");letbtn_green=Button::new().on_press({letbg=bg_color.clone();move|_|{*bg.write()=(50,180,50);}}).child("绿");letbtn_blue=Button::new().on_press({letbg=bg_color.clone();move|_|{*bg.write()=(50,80,200);}}).child("蓝");// 根容器:背景色跟随状态rect().width(Size::fill()).height(Size::fill()).background(*bg_color.read()).center().spacing(16.0).child(btn_red).child(btn_green).child(btn_blue)}考查点:
- use_state(|| (50u8, 50u8, 50u8)) 用元组存储 RGB 颜色值
- bg_color.clone() 克隆状态句柄,让多个闭包各自持有独立引用
- 每个闭包内部通过 *bg.write() = 新颜色 修改状态
- .background(*bg_color.read()) 解引用读取当前颜色值,应用到根容器背景
- 状态变化 → app() 重新执行 → .background() 拿到新颜色 → 画面刷新
易错点:
- 不 clone 直接在多个闭包中使用 bg_color → 所有权被第一个闭包夺走,后续编译报错
- background() 传入的是值不是引用,需要 *bg_color.read() 解引用
知识点总结
核心概念一览
**知识点 :**说明
Builder 模式 :所有 UI 元素通过 rect() / label() 等构造函数创建,再用 .方法() 链式设置属性
启动流程:launch() → LaunchConfig → WindowConfig::new(根组件) → .with_size(w, h)
根组件签名:fn app() -> impl IntoElement,纯 Rust 函数,无需宏标注
状态管理:use_state( 初始值) 创建,.read() 读,.write() 写,修改后自动刷新 UI
事件绑定:.onclick(closure) / .on_press(closure),闭包需要 move 关键字
子元素添加:.child(元素) 添加单个子元素,可链式调用多次添加多个
颜色表示:(r, g, b) 或 (r, g, b, a) 元组,值域 0~255
尺寸类型:Size::fill() 撑满、Size::percent(n) 百分比、Size::Pixels(n) 固定像素
和 0.3 版本的关键对照
场景:
0.3(宏写法)
0.4(Builder 写法)
创建容器:
rsx! { rect { … } }
rect().child(…)
设置属性:
width: “100%”
.width(Size::fill())
创建状态:
use_signal( 0)
use_state( 0)
读取状态:
*count()
*count.read()
修改状态:
*count.write() += 1
*count.write() += 1
事件绑定:
onclick: move _ …
.onclick(move _ …)
组件标注:
#[component]
不需要
状态修改的完整流程
用户操作(点击按钮)
↓
闭包执行 *state.write() = 新值
↓
框架检测到状态变化
↓
自动重新调用 app() 函数
↓
Builder 链重新执行,生成新的 UI 描述
↓
框架对比差异,只更新变化的部分
↓
屏幕刷新
这个流程就是 Freya 0.4 的响应式核心:你只管改状态,UI 自动跟着变。
如果三道练习都跑通了,说明第 1 课的基础已经打牢,可以继续第 2 课的布局进阶内容了。