원클릭으로
freya
// Freya Rust GUI framework best practices, patterns, and conventions. Use when writing Freya components, hooks, elements, or working on a Freya project.
// Freya Rust GUI framework best practices, patterns, and conventions. Use when writing Freya components, hooks, elements, or working on a Freya project.
| name | freya |
| description | Freya Rust GUI framework best practices, patterns, and conventions. Use when writing Freya components, hooks, elements, or working on a Freya project. |
| user-invocable | true |
Freya is a cross-platform, native, declarative GUI library for Rust.
General rules:
Start by asking the user what they would like to do:
#[derive(PartialEq)]
struct Counter {
initial: i32,
}
impl Component for Counter {
fn render(&self) -> impl IntoElement {
let mut count = use_state(|| self.initial);
label()
.on_mouse_up(move |_| *count.write() += 1)
.text(format!("Count: {}", count.read()))
}
}
#[derive(PartialEq)] is required - Freya uses it to skip re-rendering unchanged subtrees.KeyExt and ChildrenExt when the component can be keyed or accept children.render needs to own self)Component::render takes &self, so moving fields into closures forces let foo = self.foo.clone(); boilerplate. ComponentOwned::render takes self by value (the framework clones it for you), letting you move self directly. Requires #[derive(Clone)]. Reach for it only when you'd otherwise clone self (or several of its fields) inside render.
#[derive(PartialEq, Clone)]
struct Item { state: State<Vec<i32>>, i: usize }
impl ComponentOwned for Item {
fn render(mut self) -> impl IntoElement {
Button::new()
.on_press(move |_| { self.state.write().remove(self.i); })
.child("Remove")
}
}
The app root is a plain function. Hooks like use_init_theme, use_init_radio_station, use_provide_context belong here.
fn app() -> impl IntoElement {
rect().child("Hello, World!")
}
To pass data from main into the root, use the App trait:
struct MyApp { number: u8 }
impl App for MyApp {
fn render(&self) -> impl IntoElement {
label().text(self.number.to_string())
}
}
fn colored_label(color: Color, text: &str) -> impl IntoElement {
label().color(color).text(text.to_string())
}
Reusable UI that uses hooks or props MUST be a Component (struct + impl Component). Plain functions are only for the app root and stateless helpers. Functions with hooks won't benefit from diffing/memoization and can't be keyed or accept reactive props cleanly.
Built-in element constructors:
rect() - layout primitive (direction, alignment, sizing, background, borders, corners, shadows, padding, scroll).label() - single-line text.paragraph() - multi-line / rich text via .text_span(...) children; also the target for use_editable.image(holder) - raster image; holder from static_bytes(...), dynamic_bytes(...), or asset loaders.svg(bytes) - vector image.&str / String implement Into<Label>, so prefer rect().child("Hi") over rect().child(label().text("Hi")).
Elements use a fluent builder API. Never store an element in a variable to modify it later - chain all methods directly or use .maybe / .map.
// Good
rect()
.background((255, 0, 0))
.width(Size::fill())
.height(Size::px(100.))
.center() // centers children both axes
.expanded() // fills available space in parent's main axis
.horizontal() // sets layout direction to horizontal
.maybe(is_active, |el| el.child("Active"))
.map(some_value, |el, v| el.child(v.to_string()))
// Bad - storing to modify later
let mut element = rect();
Common layout shorthands: .center() centers children on both axes; .expanded() makes the element fill all remaining space along the parent's main axis (equivalent to flex: 1 in CSS); .horizontal() and .vertical() set the layout direction (prefer them over .direction(Direction::Horizontal/Vertical)).
rect()
.maybe(show_badge, |el| el.child("New")) // bool condition
.map(large_size, |el, size| el.height(size)) // Option<T>, passes value
.maybe_child(optional_element) // Option<impl IntoElement>
.maybe(bool, |el| el) - applies the callback when the condition is true.map(Option<T>, |el, val| el) - applies the callback when the Option is Some, passing the inner value.maybe_child(Option<impl IntoElement>) - appends a child only when SomeName the element argument el in .maybe / .map callbacks (not r, rect, e, etc.).
Prefer one outer .maybe / .map over several consecutive .maybe_child calls gated on the same condition - it keeps the gating in one place and avoids re-evaluating the same predicate.
// Good, single .maybe wraps all conditional children
rect()
.maybe(show, |el| {
el.child(Title::new("Hi"))
.child(Content::new().child("Hello"))
.child(Footer::new())
})
// Bad, same predicate repeated per child
rect()
.maybe_child(show.then(|| Title::new("Hi")))
.maybe_child(show.then(|| Content::new().child("Hello")))
.maybe_child(show.then(|| Footer::new()))
Attach handlers via builder methods on any element. Handlers receive Event<T>; use move closures to capture state.
rect()
.on_press(move |_| { /* left click, tap, or Enter/Space when focused */ })
.on_key_down(move |e: Event<KeyboardEventData>| { /* only while focused */ })
.on_wheel(move |e| { /* scroll delta */ })
.on_pointer_enter(move |_| { /* hover begin (mouse or touch) */ })
Catalog (all prefixed on_):
press (left/tap/Enter/Space), all_press (any mouse button), secondary_down (right-click).mouse_up, mouse_down, mouse_move.pointer_press, pointer_down, pointer_move, pointer_enter, pointer_leave, pointer_over, pointer_out.key_down, key_up.wheel.touch_start, touch_end, touch_move, touch_cancel.file_drop.sized (measured size changed).global_pointer_press, global_pointer_down, global_pointer_move, global_key_down, global_key_up, global_file_hover, global_file_hover_cancelled.capture_global_pointer_press, capture_global_pointer_move.Prefer on_press over raw mouse/pointer events for interactive elements: it covers click, tap, and keyboard activation, so accessibility comes free. Use on_mouse_* / on_pointer_* only when you need pointer-specific behavior (drag handles, canvas tools).
Event<T> has .stop_propagation() to cancel bubbling and .map(...) / .try_map(...) to transform inner data.
Use EventHandler<T> for callback props; closures convert via .into().
#[derive(PartialEq)]
struct Confirm { on_accept: EventHandler<()> }
impl Component for Confirm {
fn render(&self) -> impl IntoElement {
let on_accept = self.on_accept;
Button::new().on_press(move |_| on_accept.call(())).child("OK")
}
}
Confirm { on_accept: (move |()| println!("yes")).into() }
EventHandler<T> is Copy; capture directly in move closures.
Focusable elements need a stable AccessibilityId from use_a11y() (one per focusable node), attached via .a11y_id(...) and .a11y_focusable(true). Track focus state with use_focus(id).
#[derive(PartialEq)]
struct FocusableBox;
impl Component for FocusableBox {
fn render(&self) -> impl IntoElement {
let a11y_id = use_a11y();
let focus = use_focus(a11y_id);
rect()
.a11y_id(a11y_id)
.a11y_focusable(true)
.a11y_role(AccessibilityRole::Button)
.on_press(move |_| println!("activated"))
.maybe(focus() == Focus::Keyboard, |el| {
el.border(Border::new().fill(Color::BLUE).width(2.))
})
.child("Click or Tab to me")
}
}
Focus variants:
Focus::Not - not focused.Focus::Pointer - focused by mouse/touch (no focus ring needed).Focus::Keyboard - focused via Tab (render a focus ring).focus.is_focused() matches Pointer or Keyboard.on_press already fires on Enter/Space when focused, so keyboard activation is free. For raw key handling, KeyboardEventExt::is_press_event(&event) detects the OS activation gesture (Enter/Space; Ctrl+Alt+Space on macOS with VoiceOver).
Other a11y builders: .a11y_role(...), .a11y_alt("description"), .a11y_auto_focus(true), .a11y_member_of(other_id), .a11y_builder(|node| { /* raw accesskit::Node */ }).
Read the focused id globally via Platform::get().focused_accessibility_id.
Hooks are prefixed with use_ (e.g. use_state, use_animation). Follow these rules:
render - never inside conditionals, loops, or closures.render and capture the values in move closures instead.render method or a function component.spawn callbacks are async and cannot call hooks; capture state before spawning. Some hooks have non-hook counterparts that are safe to call in async contexts (e.g. use_consume → consume_context()).Capture hook values in move closures for event handlers:
let mut state = use_state(|| false);
let on_click = move |_| state.set(true); // capture, not call inside handler
rect().on_mouse_up(on_click)
let mut count = use_state(|| 0);
*count.write() += 1; // write
let n = *count.read(); // read
count.set(5); // convenience setter
count.set_if_modified(5); // only writes (and notifies) if the new value differs
use_state returns a Copy type (State<T>). No .clone() needed when passing it around.
Avoid drop() on guards from .read()/.write(); guards release on scope exit. Prefer a smaller scope ({ let v = state.read(); ... }) or copying the value out (let n = *count.read();). Only use explicit drop(guard) when you must release a borrow before re-borrowing in the same scope.
Prefer set_if_modified over set when the new value may equal the current (syncing external/derived values, handlers that may fire unchanged). It skips the write and avoids waking subscribers. Requires T: PartialEq. set_if_modified_and_then(value, || { ... }) runs a callback only on actual change.
Pass local state to child components:
#[derive(PartialEq)]
struct Child(State<i32>);
Use Freya Radio for large or deeply nested app state where you need surgical, fine-grained updates - only the components subscribed to a specific channel re-render when that channel changes. This makes it well-suited for complex UIs (e.g. a tab system where each tab has independent state, or a big data model where different parts of the UI subscribe to different slices).
Define your state and a channel enum that maps to the parts of the state that can change independently:
#[derive(Default, Clone)]
struct AppState {
count: i32,
name: String,
}
#[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
enum AppChannel {
Count,
Name,
}
impl RadioChannel<AppState> for AppChannel {}
Initialize once in the root component, then subscribe from any descendant:
// Root
use_init_radio_station::<AppState, AppChannel>(AppState::default);
// Any component - only re-renders when AppChannel::Count changes
let mut radio = use_radio(AppChannel::Count);
radio.read().count;
radio.write().count += 1;
For channels where a write to one should also notify subscribers of another, override derive_channel:
impl RadioChannel<AppState> for AppChannel {
fn derive_channel(self, _state: &AppState) -> Vec<Self> {
match self {
// Writing to Count also notifies Name subscribers
AppChannel::Count => vec![self, AppChannel::Name],
AppChannel::Name => vec![self],
}
}
}
For complex state transitions, implement the reducer pattern with DataReducer:
impl DataReducer for AppState {
type Channel = AppChannel;
type Action = AppAction;
fn reduce(&mut self, action: AppAction) -> ChannelSelection<AppChannel> {
match action {
AppAction::Increment => { self.count += 1; }
AppAction::SetName(n) => { self.name = n; }
}
ChannelSelection::Current
}
}
// Then in a component:
radio.apply(AppAction::Increment);
Readable<T> and Writable<T> are type-erased wrappers over any reactive value of type T. Use them as component props so the component works regardless of where the state lives: local State<T>, a Memo<T>, a radio slice, or a plain owned value. Both are PartialEq (always equal) and Clone, usable directly as component fields.
#[derive(PartialEq)]
struct NameInput { name: Writable<String> }
impl Component for NameInput {
fn render(&self) -> impl IntoElement {
Input::new(self.name.clone()) // Input is two-way bound to the Writable
}
}
// Caller side: any source converts via `into_writable()` / `into_readable()`
NameInput { name: local_name.into_writable() } // from State<String>
NameInput { name: name_slice.into_writable() } // from a RadioSliceMut
Conversions:
State<T> → Writable<T> / Readable<T> via IntoWritable / IntoReadable.Memo<T> → Readable<T> via IntoReadable.RadioSlice → Readable<T>; RadioSliceMut → Readable<T> or Writable<T>.Writable<T> → Readable<T> via From (downgrade write access).T → Readable<T> via From (non-reactive; tests, defaults).API surface:
Readable<T>: read() (subscribes), peek() (no subscription).Writable<T>: read(), peek(), write(), plus WritableUtils helpers (set, set_if_modified, with_mut, ...). Subscriptions and notifications route to the original source.Prefer Readable<T> for read-only consumers; use Writable<T> only when the component must mutate.
Use context to make a value available to any descendant component without threading it through every prop. Prefer this over static variables, thread_local!, or global singletons - context is scoped to the component tree and plays well with Freya's reactivity.
// Provider: stores the value and makes it available to all descendants
fn app() -> impl IntoElement {
use_provide_context(|| AppConfig { theme: Theme::Dark });
rect().child(DeepChild {})
}
// Consumer: retrieve by type, walks up the tree until found
#[derive(PartialEq)]
struct DeepChild;
impl Component for DeepChild {
fn render(&self) -> impl IntoElement {
let config = use_consume::<AppConfig>();
format!("Theme: {:?}", config.theme)
}
}
Use use_try_consume::<T>() when the context may not be present. If context is not found, use_consume panics.
Context values are identified by type, so each distinct type gets its own slot. Providing the same type again in a deeper component shadows the ancestor's value for that subtree.
Context is the right tool for dependency injection (e.g. passing a DB client, config, or theme down the tree). For reactive shared state use Freya Radio; for passing state between a parent and immediate children, plain props or State<T> are simpler.
use_state - component-local stateReadable/Writable - reusable components that don't care about backing storageFor simple derived values, compute them directly in render - no hook needed:
let doubled = *count.read() * 2;
For expensive computations that should only re-run when their dependencies change, use use_memo. It subscribes to any State read inside the callback and caches the result:
let expensive = use_memo(move || {
let n = *count.read(); // subscribed - reruns when count changes
compute_something(n)
});
let value = expensive.read();
For side effects that should re-run when state changes (e.g. logging, triggering external systems), use use_side_effect. Do not use it to sync one state into another - derive values directly or use use_memo instead:
use_side_effect(move || {
let value = *count.read(); // subscribed
println!("count changed: {value}");
});
Use Freya's spawn() (not tokio::spawn) for async work that updates the UI. Tasks spawned with spawn() are tied to Freya's reactivity system and can safely write to component state:
let mut data = use_state(|| None);
use_hook(move || {
spawn(async move {
let result = fetch_something().await;
data.set(Some(result));
});
});
use_hook runs once on mount, making it the right place for one-shot side effects. spawn returns a TaskHandle you can cancel if needed.
Components and hooks are synchronous - you cannot await inside render. Prefer use_future for typical async work (see below). Only reach for use_hook + spawn when you need fine-grained control over the task lifecycle:
// Only when you need manual control
use_hook(move || {
spawn(async move {
let s = some_async_fn().await;
result.set(s);
});
});
use_future wraps this pattern: it starts an async task on mount and exposes its state as FutureState<D> (Pending, Loading, Fulfilled(D)):
let task = use_future(|| async {
fetch_user(42).await
});
match &*task.state() {
FutureState::Pending | FutureState::Loading => "Loading...",
FutureState::Fulfilled(user) => user.name.as_str(),
}
Call task.start() to restart and task.cancel() to stop it.
For data that should be cached, deduplicated, and automatically refetched, use freya-query (features = ["query"]):
// Define the query
#[derive(Clone, PartialEq, Hash, Eq)]
struct FetchUser;
impl QueryCapability for FetchUser {
type Ok = String;
type Err = String;
type Keys = u32;
async fn run(&self, user_id: &u32) -> Result<String, String> {
Ok(format!("User {user_id}"))
}
}
// Use it in a component
impl Component for UserProfile {
fn render(&self) -> impl IntoElement {
let query = use_query(Query::new(self.0, FetchUser));
match &*query.read().state() {
QueryStateData::Pending => "Loading...",
QueryStateData::Settled { res, .. } => res.as_deref().unwrap_or("Error"),
QueryStateData::Loading { .. } => "Refreshing...",
}
}
}
Multiple components using the same (capability, keys) pair share one cache entry. Invalidate with query.invalidate() or QueriesStorage::<FetchUser>::invalidate_all().await.
For write operations, use use_mutation + MutationCapability. The on_settled callback is the right place to invalidate related queries after a mutation.
Prefer freya-query over manual use_future + state when you need caching, background refetch, or deduplication.
Freya has its own async runtime. To use Tokio-ecosystem crates (reqwest, sqlx, etc.), enter a Tokio runtime context in main before launching:
fn main() {
let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
let _guard = rt.enter(); // keep alive for the whole program
launch(LaunchConfig::new().with_window(WindowConfig::new(app)))
}
Use Freya's spawn() for UI updates. tokio::spawn runs on the Tokio runtime and cannot update component state.
Use the theming system to centralize component styling. Prefer it over hand-rolled structs of Color/size constants or scattered hardcoded values.
A Theme bundles a ColorsSheet (the app palette) and per-component *ThemePreference entries indexed by string key. Components read their theme via get_theme! and resolve color references against the active ColorsSheet.
Initialize a theme in the root component. The returned State<Theme> is reactive - writing to it switches the theme app-wide:
fn app() -> impl IntoElement {
let mut theme = use_init_theme(light_theme); // or dark_theme, or your own
rect()
.theme_background()
.theme_color()
.expanded()
.center()
.child(Button::new().on_press(move |_| theme.set(dark_theme())).child("Dark"))
}
Use use_init_root_theme to register at the root scope. To follow the OS preference, convert Platform::get().preferred_theme via the FromPreference::to_theme extension.
Built-in elements expose helpers that read the active theme - prefer these over hardcoded colors:
rect().theme_background(), rect().theme_color()label().theme_color(), paragraph().theme_color()svg(...).theme_color() / .theme_accent_color() / .theme_fill() / .theme_stroke() / .theme_accent_fill() / .theme_accent_stroke()Start from light_theme() / dark_theme() and override what you need. Use LIGHT_COLORS / DARK_COLORS as base palettes with struct update syntax:
fn brand_theme() -> Theme {
let mut theme = dark_theme();
theme.name = "brand";
theme.colors = ColorsSheet {
primary: Color::from_rgb(37, 52, 63),
secondary: Color::from_rgb(255, 155, 81),
tertiary: Color::from_rgb(81, 155, 255),
..DARK_COLORS
};
theme
}
ColorsSheet covers brand (primary/secondary/tertiary), status (success/warning/error/info), surfaces (background, surface_primary/secondary/tertiary, surface_inverse[_secondary|_tertiary]), borders (border, border_focus, border_disabled), text (text_primary/secondary/placeholder/inverse/highlight), interaction states (hover, focus, active, disabled), and utility (overlay, shadow).
Use define_theme! to generate the theme types for a component. This replaces hand-rolled "props struct of colors and sizes" patterns:
define_theme! {
%[component]
pub StatusBadge {
%[fields]
background: Color,
color: Color,
corner_radius: CornerRadius,
padding: Gaps,
}
}
#[derive(PartialEq)]
struct StatusBadge {
theme: Option<StatusBadgeThemePartial>,
}
impl Component for StatusBadge {
fn render(&self) -> impl IntoElement {
let StatusBadgeTheme { background, color, corner_radius, padding } =
get_theme!(&self.theme, StatusBadgeThemePreference, "status_badge");
rect()
.background(background)
.corner_radius(corner_radius)
.padding(padding)
.child(label().text("Active").color(color).font_size(12.))
}
}
The macro generates three structs:
StatusBadgeThemePartial - Option<Preference<T>> per field, used for per-instance overrides.StatusBadgeThemePreference - Preference<T> per field, registered in the Theme as the default.StatusBadgeTheme - resolved concrete values, returned by get_theme!.get_theme!(&self.theme, StatusBadgeThemePreference, "status_badge") looks up the registered preference, applies any ThemePartial override on the instance, and resolves it against the active ColorsSheet.
Register the default preference on your custom theme:
theme.set(
"status_badge",
StatusBadgeThemePreference {
background: Preference::Reference("secondary"), // resolves from ColorsSheet
color: Preference::Reference("text_inverse"),
corner_radius: CornerRadius::new_all(99.).into(), // Specific via Into
padding: Gaps::new(4., 10., 4., 10.).into(),
},
);
Preference::Reference("...") looks up the named color in the active ColorsSheet so the component automatically follows theme switches. Only Color fields support references - Size, Gaps, CornerRadius, f32, and Duration must use Preference::Specific(v) (or v.into()).
The macro also generates a StatusBadgeThemePartialExt trait implemented on the component, giving callers per-field builder methods:
StatusBadge::new()
.background(Color::from_rgb(123, 123, 123)) // from the generated Ext trait
.corner_radius(CornerRadius::new_all(4.))
For components that store the partial under a non-default field name (e.g. theme_colors alongside theme_layout), pass for = ...; theme_field = ...; to point the generated builder at the right field. Examples: Button, Card, Switch, Input in crates/freya-components/.
define_theme!.Color/size constants that callers tweak: replace it with a theme.Use .key(id) on elements in dynamic lists to ensure correct reconciliation on reorders:
VirtualScrollView::new(|i, _| {
rect()
.key(i)
.child(format!("Item {i}"))
.into()
})
.length(items.len())
Missing .key() in dynamic lists causes element misidentification during reorders.
Enable with features = ["i18n"]. Uses Fluent (.ftl files) for translations.
1. Define .ftl files:
# en-US.ftl
hello_world = Hello, World!
hello = Hello, { $name }!
2. Initialize once in the root component:
use freya::i18n::*;
let mut i18n = use_init_i18n(|| {
I18nConfig::new(langid!("en-US"))
.with_locale(Locale::new_static(langid!("en-US"), include_str!("../i18n/en-US.ftl")))
.with_locale(Locale::new_static(langid!("es-ES"), include_str!("../i18n/es-ES.ftl")))
.with_fallback(langid!("en-US"))
});
3. Translate in any descendant component:
// t! panics if key missing, te! returns Result, tid! falls back to the key string
t!("hello_world") // "Hello, World!"
t!("hello", name: {"Alice"}) // "Hello, Alice!"
te!("hello_world") // Ok("Hello, World!")
tid!("missing-key") // "message-id: missing-key should be translated"
4. Switch language at runtime:
let mut i18n = I18n::get(); // retrieve from any descendant
i18n.set_language(langid!("es-ES"));
For multi-window apps, create with I18n::create_global in main and share with use_share_i18n.
Use use_animation for manual control and use_animation_transition to animate between two values reactively:
// Manual: call .start() / .reverse() yourself
let mut anim = use_animation(|_| AnimColor::new((240, 240, 240), (200, 80, 80)).time(400));
rect().background(&*anim.read()).on_press(move |_| anim.start())
// Transition: re-runs automatically when the tracked value changes
let color = use_animation_transition(is_active, |from, to| AnimColor::new(from, to).time(300));
rect().background(&*color.read())
Animate colors (AnimColor), sizes, positions, and other numeric properties. Easing functions and sequencing are supported.
Enable with features = ["router"]. Define routes with #[derive(Routable)], render them with router::<Route>(), place the current page with outlet::<Route>(), and navigate with Link or RouterContext::get().replace(...):
#[derive(Routable, Clone, PartialEq)]
enum Route {
#[route("/")]
Home,
#[route("/settings")]
Settings,
}
fn app() -> impl IntoElement {
router::<Route>(|| RouterConfig::default())
}
freya-testing lets you test components without a window. Use TestingRunner to mount a component, simulate interactions, and assert on state:
use freya_testing::prelude::*;
let (mut runner, state) = TestingRunner::new(app, (300., 300.).into(), |r| {
r.provide_root_context(|| State::create(0))
}, 1.);
runner.sync_and_update();
runner.click_cursor((15., 15.));
assert_eq!(*state.peek(), 1);
Call runner.render_to_file("out.png") to snapshot the current UI.
Enable with features = ["icons"]. Uses Lucide icons rendered as SVGs:
use freya::icons;
svg(icons::lucide::antenna()).color((120, 50, 255)).expanded()
Use use_editable to manage a text editor with cursor, selection, keyboard shortcuts, and virtualization. Wire it to a paragraph() element's event handlers and feed EditableEvents from mouse/keyboard events. See examples/ for full wiring.
Enable with features = ["code-editor"]. CodeEditorData holds a Rope-backed buffer with tree-sitter syntax highlighting. Pass it to the CodeEditor component:
let editor = use_state(|| {
let mut e = CodeEditorData::new(Rope::from_str(src), LanguageId::Rust);
e.parse();
e.measure(14., "Jetbrains Mono");
e
});
CodeEditor::new(editor, focus.a11y_id())
Enable with features = ["plot"]. Use the plot() element with a RenderCallback and draw into it using the Plotters API via PlotSkiaBackend:
plot(RenderCallback::new(|ctx| {
let backend = PlotSkiaBackend::new(ctx.canvas, ctx.font_collection, size).into_drawing_area();
// ... Plotters drawing code
})).expanded()
Enable with features = ["material-design"]. Adds style modifiers like .ripple() to built-in components:
use freya::material_design::*;
Button::new().ripple().child("Click me")
Enable with features = ["webview"]. Embeds a browser view into your UI:
use freya::webview::*;
WebView::new("https://example.com").expanded()
Enable with features = ["terminal"]. Spawns a PTY process and renders it as a terminal:
use freya::terminal::*;
let mut cmd = CommandBuilder::new("bash");
cmd.env("TERM", "xterm-256color");
let handle = TerminalHandle::new(TerminalId::new(), cmd, None).ok();
// Render with Terminal::new(handle) and forward keyboard events via handle.write_key()
Enable with features = ["camera"]. Streams frames from a webcam into reactive state:
use freya::camera::*;
let camera = use_camera(CameraConfig::default);
CameraViewer::new(camera)
On macOS, call freya::camera::init() from main to request authorization before launching.
Enable with features = ["devtools"]. Adds a real-time component tree inspector. Run the devtools app alongside your app to examine layout, props, and state.
Add to your Cargo.toml as needed:
freya = { version = "...", features = ["router", "radio"] }
| Feature | What it enables |
|---|---|
router | Page routing (freya-router) |
i18n | Internationalization via Fluent (freya-i18n) |
remote-asset | Load images/assets from remote URLs |
radio | Global state management (freya-radio) |
query | Async data fetching with caching (freya-query) |
sdk | Generic utility APIs (freya-sdk) |
plot | Chart/plotting via Plotters (freya-plotters-backend) |
gif | Animated GIF support in GifViewer |
calendar | Calendar date-picker component |
markdown | Markdown renderer component |
icons | SVG icon library via Lucide (freya-icons) |
material-design | Material Design theme (freya-material-design) |
webview | Embed a WebView (freya-webview) |
terminal | Terminal emulator (freya-terminal) |
code-editor | Code editing APIs (freya-code-editor) |
camera | Webcam capture (freya-camera) |
tray | System tray support |
titlebar | Custom window titlebar component |
devtools | Developer tools overlay |
performance | Performance monitoring plugin |
hotpath | Hot-path optimization |
all | All of the above (except devtools/performance/hotpath) |
AGENTS.md (also symlinked as CLAUDE.md) in the repo root - authoritative dev workflow and Rust conventions for working on Freya itself.crates/freya/src/_docs/ - in-source documentation for hooks, state management, components, routing, animations, and more.examples/ - 150+ working examples covering every feature.