ITADN
marc2332/freya
README.md
以下内容由 AI 翻译,如有问题请点此提交 issue 反馈

Freya 🦀

Freya logo

Discord Server Github Sponsors codecov

网站 | 文档 | Discord | 联系

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)组件可以简单地是一个函数。内置示例包括 ButtonSwitch 等组件。

fn app() -> impl IntoElement {
    let mut count = use_state(|| 4);

    let counter = rect()
        .width(Size::fill())
        .height(Size::percent(50.))
        .center()
        .color((255, 255, 255))
        .background((15, 163, 242))
        .font_weight(FontWeight::BOLD)
        .font_size(75.)
        .shadow((0., 4., 20., 4., (0, 0, 0, 80)))
        .child(count.read().to_string());

    let actions = rect()
        .horizontal()
        .width(Size::fill())
        .height(Size::percent(50.))
        .center()
        .spacing(8.0)
        .child(
            Button::new()
                .on_press(move |_| {
                    *count.write() += 1;
                })
                .child("Increase"),
        )
        .child(
            Button::new()
                .on_press(move |_| {
                    *count.write() -= 1;
                })
                .child("Decrease"),
        );

    rect().child(counter).child(actions)
}

开箱即用组件

Freya 提供了一套开箱即用的组件,从简单的 ButtonSwitchSlider 到更复杂的 VirtualScrollViewCalendarColorPicker 等。

你可以在 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"),
        )
}

Portal 示例

Component Portal

富文本编辑

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 页面进行捐赠。

感谢我的赞助者对本项目的支持! 😄

User avatar: User avatar: 高庆丰User avatar: Huddy BuddyUser avatar: Gabriel JõeUser avatar: Mark

特别感谢 💪

  • Jonathan KelleyEvan 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

许可证

MIT 许可证