Freya 🦀
Freya 是一个用于 Rust 🦀 的跨平台、原生、声明式 GUI 库。
用法 📜
最新稳定版本:
freya = "0.4"
下次发布:
freya = "0.5.0-rc.1"
main 分支:
freya = { git = "https://github.com/marc2332/freya", branch = "main" }
试一试 🚀
请确保 开发环境 已准备就绪。
⚠️ 此仓库使用 git submodules,如果不使用
--recurse-submodules进行克隆,示例将无法编译。
git clone --recurse-submodules https://github.com/marc2332/freya.git
cd freya
已经克隆但未包含子模块? 运行
git submodule update --init --recursive以获取它们。
然后运行一个示例:
cargo run --example counter
⚠️ 如果你恰好在 Windows 上使用
windows-gnu并遇到编译错误,也许可以去看看这个 issue。
组件与状态
Freya 的组件模型允许你创建可复用的 UI 元素,当它们所依赖的状态发生变化时会自动重新渲染。组件可以持有自己的内部状态,也可以订阅共享状态,并以 UI 作为其输出。任何实现了 Component trait 的类型都可以作为组件,而根(app)组件可以简单地是一个函数。内置示例包括 Button 和 Switch 等组件。
|
|
开箱即用组件
Freya 提供了一套开箱即用的组件,从简单的 Button、Switch、Slider 到更复杂的 VirtualScrollView、Calendar、ColorPicker 等。
你可以在 examples 文件夹 中查看所有以 component_ 开头的示例。
component_input.rs] 的示例:
流畅动画
为颜色、大小、位置和其他视觉属性创建过渡效果。动画 API 让你完全控制时间、缓动函数和动画序列。
代码
use freya::prelude::*;
fn app() -> impl IntoElement {
let mut animation = use_animation(|_| AnimColor::new((246, 240, 240), (205, 86, 86)).time(400));
rect()
.background(&*animation.read())
.expanded()
.center()
.spacing(8.0)
.child(
Button::new()
.on_press(move |_| {
animation.start();
})
.child("Start"),
)
.child(
Button::new()
.on_press(move |_| {
animation.reverse();
})
.child("Reverse"),
)
}
富文本编辑
Freya 提供了超越简单输入框的文本编辑功能。您可以创建支持光标管理、文本选择、键盘快捷键、自定义格式、虚拟化等功能的富文本编辑器。
代码
use freya::prelude::*;
fn app() -> impl IntoElement {
let holder = use_state(ParagraphHolder::default);
let mut editable = use_editable(|| "Hello, World!".to_string(), EditableConfig::new);
let focus = use_focus();
paragraph()
.a11y_id(focus.a11y_id())
.cursor_index(editable.editor().read().cursor_pos())
.highlights(
editable
.editor()
.read()
.get_selection()
.map(|selection| vec![selection])
.unwrap_or_default(),
)
.on_mouse_down(move |e: Event<MouseEventData>| {
focus.request_focus();
editable.process_event(EditableEvent::Down {
location: e.element_location,
editor_line: EditorLine::SingleParagraph,
holder: &holder.read(),
});
})
.on_mouse_move(move |e: Event<MouseEventData>| {
editable.process_event(EditableEvent::Move {
location: e.element_location,
editor_line: EditorLine::SingleParagraph,
holder: &holder.read(),
});
})
.on_global_pointer_up(move |_| editable.process_event(EditableEvent::Release))
.on_key_down(move |e: Event<KeyboardEventData>| {
editable.process_event(EditableEvent::KeyDown {
key: &e.key,
modifiers: e.modifiers,
});
})
.on_key_up(move |e: Event<KeyboardEventData>| {
editable.process_event(EditableEvent::KeyUp { key: &e.key });
})
.span(editable.editor().read().to_string())
.holder(holder.read().clone())
}
代码编辑器
创建并控制文本代码编辑器。它是状态无关的,因此只要能够转换为 Writable 即可正常工作。使用 Rope 进行文本编辑,使用 tree-sitter 进行语法高亮。您需要自行提供 tree-sitter 语法及其高亮查询,因此可以支持任何语言。
通过 code-editor 功能启用。
Code
fn app() -> impl IntoElement {
use_init_theme(dark_theme);
let focus = use_focus();
let editor = use_state(|| {
let path = PathBuf::from("./crates/freya-code-editor/src/editor_ui.rs");
let rope = Rope::from_str(&std::fs::read_to_string(&path).unwrap());
let language = EditorLanguage::new(
tree_sitter_rust::LANGUAGE,
tree_sitter_rust::HIGHLIGHTS_QUERY,
);
let mut editor = CodeEditorData::new(rope, language);
editor.parse();
editor.measure(14., "Jetbrains Mono");
editor
});
CodeEditor::new(editor, focus.a11y_id())
}
使用 MarkdownViewer 组件渲染 Markdown 文档。
通过 markdown 功能启用。
代码
fn app() -> impl IntoElement {
MarkdownViewer::new("# Hello World\n\nThis is **bold** and *italic* text.")
}
路由与导航
定义路由,管理导航状态,并在不同视图之间进行切换。
通过 router 功能启用。
代码
use freya::prelude::*;
use freya::router::prelude::*;
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
#[layout(AppBottomBar)]
#[route("/")]
Home,
#[route("/settings")]
Settings,
}
fn app() -> impl IntoElement {
router::<Route>(|| RouterConfig::default().with_initial_path(Route::Settings))
}
#[derive(PartialEq)]
struct AppBottomBar;
impl Component for AppBottomBar {
fn render(&self) -> impl IntoElement {
rect()
.native_router()
.content(Content::flex())
.child(
rect()
.width(Size::fill())
.height(Size::flex(1.))
.center()
.child(outlet::<Route>()),
)
.child(
rect()
.horizontal()
.width(Size::fill())
.main_align(Alignment::center())
.padding(8.)
.spacing(8.)
.child(
Link::new(Route::Home)
.child(FloatingTab::new().child("Home"))
.activable_route(Route::Home)
.exact(true),
)
.child(
Link::new(Route::Settings)
.child(FloatingTab::new().child("Settings"))
.activable_route(Route::Settings)
.exact(true),
),
)
}
}
#[derive(PartialEq)]
struct Home {}
impl Component for Home {
fn render(&self) -> impl IntoElement {
Button::new()
.on_press(|_| {
RouterContext::get().replace(Route::Settings);
})
.child("Go Settings")
}
}
#[derive(PartialEq)]
struct Settings {}
impl Component for Settings {
fn render(&self) -> impl IntoElement {
Button::new()
.on_press(|_| {
let _ = RouterContext::get().replace(Route::Home);
})
.child("Go Home")
}
}
全局状态管理
Freya 的 freya-radio 状态管理系统通过 channels 系统提供高效的全局状态管理。组件订阅特定的“channels”,仅当数据通过其 channel 被修改并通知时才会收到更新。
使用 radio 功能启用。
Code
use freya::prelude::*;
use freya::radio::*;
#[derive(Default)]
struct Data {
pub lists: Vec<Vec<String>>,
}
#[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
pub enum DataChannel {
ListCreation,
SpecificListItemUpdate(usize),
}
impl RadioChannel<Data> for DataChannel {}
fn app() -> impl IntoElement {
use_init_radio_station::<Data, DataChannel>(Data::default);
let mut radio = use_radio::<Data, DataChannel>(DataChannel::ListCreation);
let on_press = move |_| {
radio.write().lists.push(Vec::default());
};
rect()
.horizontal()
.child(Button::new().on_press(on_press).child("Add new list"))
.children(
radio
.read()
.lists
.iter()
.enumerate()
.map(|(list_n, _)| ListComp(list_n).into()),
)
}
#[derive(PartialEq)]
struct ListComp(usize);
impl Component for ListComp {
fn render(&self) -> impl IntoElement {
let list_n = self.0;
let mut radio = use_radio::<Data, DataChannel>(DataChannel::SpecificListItemUpdate(list_n));
println!("Running DataChannel::SpecificListItemUpdate({list_n})");
rect()
.child(
Button::new()
.on_press(move |_| radio.write().lists[list_n].push("Hello, World".to_string()))
.child("New Item"),
)
.children(
radio.read().lists[list_n]
.iter()
.enumerate()
.map(move |(i, item)| label().key(i).text(item.clone()).into()),
)
}
}
图标库
轻松将图标集成到您的应用中,目前仅支持 Lucide。
代码
use freya::prelude::*;
use freya::icons;
fn app() -> impl IntoElement {
SvgViewer::new(icons::lucide::antenna())
.color((120, 50, 255))
.expanded()
}
无头测试
使用 freya-testing 你可以在无窗口(无头)环境中测试你的 Freya 组件。不过,你可以在任意时刻将应用渲染到文件。freya-testing 实际上被 Freya 本身用于测试所有开箱即用的组件和其他 API。
代码
use freya::prelude::*;
use freya_testing::prelude::*;
fn app() -> impl IntoElement {
let mut state = use_consume::<State<i32>>();
rect()
.expanded()
.center()
.background((240, 240, 240))
.on_mouse_up(move |_| *state.write() += 1)
.child(format!("Clicked: {}", state.read()))
}
fn main() {
// Create headless testing runner
let (mut test, state) = TestingRunner::new(
app,
(300., 300.).into(),
|runner| runner.provide_root_context(|| State::create(0)),
1.,
);
test.sync_and_update();
assert_eq!(*state.peek(), 0);
// Simulate user interactions
test.click_cursor((15., 15.));
assert_eq!(*state.peek(), 1);
// Render the current ui state to a file
test.render_to_file("./demo-1.png");
}
高级绘图与图表
使用 Plotters 库,您可以直接在应用程序中创建图表、图形和数据可视化。
通过 ploters 功能启用。
代码
use freya::prelude::*;
use freya::plot::*;
use freya::plot::plotters::*;
fn on_render(ctx: &mut RenderContext, (cursor_x, cursor_y): (f64, f64)) {
let backend = PlotSkiaBackend::new(
ctx.canvas,
ctx.font_collection,
ctx.layout_node.area.size.to_i32().to_tuple(),
).into_drawing_area();
backend.fill(&WHITE).unwrap();
let pitch = std::f64::consts::PI * (0.5 - cursor_y / ctx.layout_node.area.height() as f64);
let yaw = std::f64::consts::PI * 2.0 * (cursor_x / ctx.layout_node.area.width() as f64 - 0.5);
let scale = 0.4 + 0.6 * (1.0 - cursor_y / ctx.layout_node.area.height() as f64);
let x_axis = (-3.0..3.0).step(0.1);
let z_axis = (-3.0..3.0).step(0.1);
let mut chart = ChartBuilder::on(&backend)
.caption("3D Surface Plot", ("sans", 20))
.build_cartesian_3d(x_axis.clone(), -3.0..3.0, z_axis.clone())
.unwrap();
chart.with_projection(|mut pb| {
pb.pitch = pitch;
pb.yaw = yaw;
pb.scale = scale;
pb.into_matrix()
});
chart
.draw_series(
SurfaceSeries::xoz(
(-30..30).map(|f| f as f64 / 10.0),
(-30..30).map(|f| f as f64 / 10.0),
|x, z| (x * x + z * z).cos(),
)
.style(BLUE.mix(0.2).filled()),
)
.unwrap()
.label("Interactive Surface")
.legend(|(x, y)| Rectangle::new([(x + 5, y - 5), (x + 15, y + 5)], BLUE.mix(0.5).filled()));
}
fn app() -> impl IntoElement {
let mut cursor_position = use_state(CursorPoint::default);
let on_global_pointer_move = move |e: Event<PointerEventData>| {
// Dont move when the cursor goes outside the window
if e.global_location().to_tuple() != (-1., -1.) {
cursor_position.set(e.global_location());
let platform = Platform::get();
platform.send(UserEvent::RequestRedraw);
}
};
canvas(RenderCallback::new(move |context| {
on_render(context, cursor_position().to_tuple());
}))
.expanded()
.on_global_pointer_move(on_global_pointer_move)
}
国际化 (i18n)
Freya 支持国际化,内置了对 Fluent 本地化系统的支持。轻松管理翻译、复数形式以及特定区域的格式化。
通过 i18n 功能启用。
代码
use freya::prelude::*;
use freya::i18n::*;
fn app() -> impl IntoElement {
let mut i18n = use_init_i18n(|| {
I18nConfig::new(langid!("en-US"))
.with_locale((langid!("en-US"), include_str!("./i18n/en-US.ftl")))
.with_locale((langid!("es-ES"), PathBuf::from("./examples/i18n/es-ES.ftl")))
});
let change_to_english = move |_| i18n.set_language(langid!("en-US"));
let change_to_spanish = move |_| i18n.set_language(langid!("es-ES"));
rect()
.expanded()
.center()
.child(
rect()
.horizontal()
.child(Button::new().on_press(change_to_english).child("English"))
.child(Button::new().on_press(change_to_spanish).child("Spanish")),
)
.child(t!("hello_world"))
.child(t!("hello", name: "Freya!"))
}
Material Design 组件
Freya 提供受 Material Design 启发的样式修饰符。
通过 material-design 功能启用。
代码
use freya::prelude::*;
use freya::material_design::*;
fn app() -> impl IntoElement {
rect().center().expanded().child(
Button::new()
.on_press(|_| println!("Material button pressed"))
.ripple() // Adds Material Design ripple effect
.child("Material Button"),
)
}
WebView 集成
借助 Freya 的 WebView 支持,将 Web 内容集成到您的原生应用中。嵌入 Web 应用程序,或简单地与您的原生 UI 组件并排显示基于 Web 的内容。
通过 webview 功能启用。
代码
use freya::prelude::*;
use freya::webview::*;
fn app() -> impl IntoElement {
// Multi-tab webview implementation
let mut tabs = use_state(|| vec![Tab {
id: WebViewId::new(),
title: "Tab 1".to_string(),
url: "https://duckduckgo.com".to_string(),
}]);
let mut active_tab = use_state(|| tabs.read()[0].id);
rect()
.expanded()
.height(Size::fill())
.background((35, 35, 35))
.child(
rect()
.width(Size::fill())
.height(Size::px(45.))
.padding(4.)
.background((50, 50, 50))
.horizontal()
.cross_align(Alignment::Center)
.spacing(4.)
.children(tabs.read().iter().map(|tab| {
// Tab implementation...
}))
)
.child(WebView::new("https://duckduckgo.com").expanded())
}
终端仿真
Freya 包含终端仿真功能,并支持完整的 PTY(伪终端)。创建集成终端应用程序、SSH 客户端或开发工具。
使用 terminal 功能启用。
代码
use freya::prelude::*;
use freya::terminal::*;
fn app() -> impl IntoElement {
let mut handle = use_state(|| {
let mut cmd = CommandBuilder::new("bash");
cmd.env("TERM", "xterm-256color");
cmd.env("COLORTERM", "truecolor");
TerminalHandle::new(TerminalId::new(), cmd, None).ok()
});
rect()
.expanded()
.center()
.background((30, 30, 30))
.color((245, 245, 245))
.padding(6.)
.child(if let Some(handle) = handle.read().clone() {
Terminal::new(handle.clone())
.a11y_id(focus.a11y_id())
.on_key_down(move |e: Event<KeyboardEventData>| {
let _ = handle.write_key(&e.key, e.modifiers);
})
} else {
"Terminal exited".into_element()
})
}
开发者工具
实时检查组件树。
在 freya 中启用 devtools 功能,然后运行 devtools 应用。
贡献 🧙♂️
如果您有兴趣参与贡献,请确保先阅读 Contributing 指南!
联系
您可以通过 marc@mespin.me 联系我,咨询问题、合作或任何您想到的事情。
支持 🤗
如果您有兴趣支持本项目的开发,请随意向我的 Github Sponsor 页面进行捐赠。
感谢我的赞助者对本项目的支持! 😄




特别感谢 💪
- Jonathan Kelley 和 Evan Almloff 感谢他们创建了 Dioxus 以及提供的帮助,尤其是在我还在创建 Freya 的时候。
- Armin 感谢他创建了 rust-skia 以及提供的帮助,并出于好意为 Freya 使用的功能组合托管了 skia 的预构建二进制文件。
- geom3trik 感谢他帮助我弄清楚如何添加增量渲染。
- Tropical 感谢他对改进可访问性和渲染的贡献。
- Aiving 感谢他对 rust-skia 做出的大量贡献以改善 SVG 支持,并帮助优化了 Freya 中的图像渲染。
- RobertasJ 感谢他为
calc()函数添加了嵌套括号,并推动了动画 API 的改进。 - 以及其余的贡献者和任何给我提供过任何形式反馈的人!
- SparkyTD 感谢他贡献了 Android 支持。
与 Dioxus 的历史
Freya 0.1、0.2 和 0.3 基于 Dioxus 的核心 crate。从 0.4 开始,Freya 不再使用 Dioxus,而是使用其自己的响应式核心,部分受 Dioxus 启发,但存在许多差异。
Claude Code Skill
一个包含 Freya 最佳实践和模式的 Claude Code skill 现已可用。使用以下命令安装:
/plugin marketplace add marc2332/freya
/plugin install freya@freya-marketplace
对于其他 AI 编码代理,你可以直接使用技能文档作为上下文:plugins/freya/skills/freya/SKILL.md