Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Kael

GPU-accelerated UI framework for native desktop apps in Rust.

Kael replaces Electron with a single Rust crate that gives you everything you need to build production desktop applications — IDEs, video editors, dashboards, design tools — with native GPU performance on macOS, Windows, and Linux.

What you get

LayerWhat Kael provides
WidgetsButton, TextInput, Checkbox, Toggle, RadioGroup, Slider, Select, DatePicker, Modal, Popover, Tabs, Disclosure, Progress, Toast, Splitter, and more
LayoutGPU-accelerated flexbox via Taffy, responsive sizing, scroll containers
RenderingMetal (macOS), DirectX 11 (Windows), Vulkan (Linux) — 120fps, sRGB-correct, pixel-perfect
StateReactive Entity<T> system with automatic re-rendering on change
PlatformFile dialogs, system tray, native menus, global hotkeys, notifications, clipboard, printing, auto-updates, session persistence
AdvancedPlugin system (WASM sandboxed), multi-process IPC, accessibility, theming, gestures

Quick start

Add to your Cargo.toml:

[dependencies]
kael = "0.1"

Write your first app:

use kael::*;
use kael::prelude::*;

struct Hello {
    name: SharedString,
}

impl Render for Hello {
    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        div()
            .size_full()
            .flex()
            .items_center()
            .justify_center()
            .bg(rgb(0x1E1E1E))
            .text_xl()
            .text_color(rgb(0xFFFFFF))
            .child(format!("Hello, {}!", self.name))
    }
}

fn main() {
    Application::new().run(|cx: &mut App| {
        let bounds = Bounds::centered(None, size(px(400.0), px(300.0)), cx);
        cx.open_window(
            WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(bounds)),
                ..Default::default()
            },
            |_, cx| cx.new(|_| Hello { name: "World".into() }),
        ).unwrap();
        cx.activate(true);
    });
}

Native rendering quality

Kael renders with the same sharpness as first-party platform apps:

  • macOS: SF Pro Text/Display optical sizing (automatic swap at 20pt), sRGB Metal pipeline, continuous (squircle) corners matching SwiftUI, AppKit-matched font smoothing
  • All platforms: PixelSnapPolicy for hairline strokes, device-pixel-aligned text baselines, and crisp fills at any DPI

Platform support

PlatformRendererStatus
macOSMetalStable
WindowsDirectX 11Stable
Linux (X11)Vulkan/BladeStable
Linux (Wayland)Vulkan/BladeStable

Acknowledgements

Kael is built on top of GPUI, the GPU-accelerated UI framework originally created by Zed Industries for the Zed code editor. We are grateful for their foundational work which made Kael possible.

Getting Started

Prerequisites

macOS: Xcode command line tools

xcode-select --install

Linux (Ubuntu/Debian):

sudo apt-get install -y \
  libxkbcommon-dev libwayland-dev libxcb1-dev \
  libvulkan-dev libfontconfig1-dev

Windows: Visual Studio Build Tools with C++ workload

Create a new project

cargo new my_app
cd my_app

Add Kael to Cargo.toml:

[dependencies]
kael = "0.1"

Your first window

Replace src/main.rs with:

use kael::*;
use kael::prelude::*;

struct Counter {
    count: i32,
}

impl Render for Counter {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let entity = cx.entity();

        div()
            .size_full()
            .flex()
            .flex_col()
            .items_center()
            .justify_center()
            .gap_4()
            .bg(rgb(0x1E1E1E))
            .text_color(rgb(0xFFFFFF))
            .child(
                div().text_3xl().child(format!("Count: {}", self.count))
            )
            .child(
                div()
                    .flex()
                    .gap_2()
                    .child(
                        button("decrement")
                            .label("-1")
                            .on_click({
                                let entity = entity.clone();
                                move |_, _, cx| {
                                    entity.update(cx, |this, cx| {
                                        this.count -= 1;
                                        cx.notify();
                                    });
                                }
                            })
                    )
                    .child(
                        button("increment")
                            .label("+1")
                            .on_click({
                                let entity = entity.clone();
                                move |_, _, cx| {
                                    entity.update(cx, |this, cx| {
                                        this.count += 1;
                                        cx.notify();
                                    });
                                }
                            })
                    )
            )
    }
}

fn main() {
    Application::new().run(|cx: &mut App| {
        let bounds = Bounds::centered(None, size(px(400.0), px(300.0)), cx);
        cx.open_window(
            WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(bounds)),
                ..Default::default()
            },
            |_, cx| cx.new(|_| Counter { count: 0 }),
        ).unwrap();
        cx.activate(true);
    });
}

Run it:

cargo run

What just happened

  1. Application::new().run() — boots the platform event loop
  2. cx.open_window() — creates a native window with GPU rendering
  3. cx.new(|_| Counter { count: 0 }) — creates a reactive Entity<Counter>
  4. impl Render for Counter — defines what the entity draws each frame
  5. cx.notify() — tells the framework to re-render after state changes

Key patterns

Builder pattern for UI

Every element uses method chaining. No JSX, no templates — just Rust:

#![allow(unused)]
fn main() {
div()
    .flex()
    .flex_col()
    .gap_4()
    .p_4()
    .bg(rgb(0x1E1E1E))
    .text_color(rgb(0xFFFFFF))
    .child("Hello")
}

Reactive state

State lives in your struct. Mutate it, call cx.notify(), and the framework re-renders:

#![allow(unused)]
fn main() {
entity.update(cx, |this, cx| {
    this.count += 1;
    cx.notify();
});
}

Custom rendering

Every widget accepts .render_with() for full visual control:

#![allow(unused)]
fn main() {
button("save")
    .label("Save")
    .render_with(|state, _window, _cx| {
        div()
            .px_4().py_2()
            .rounded(px(8.0))
            .bg(if state.focused { rgb(0x2563eb) } else { rgb(0x3b82f6) })
            .text_color(rgb(0xffffff))
            .child(state.label.unwrap_or_default())
            .into_any_element()
    })
}

Next steps

Core Concepts

Application lifecycle

Every Kael app follows this flow:

Application::new().run() → cx.open_window() → cx.new(|_| View) → render loop
fn main() {
    Application::new().run(|cx: &mut App| {
        cx.open_window(WindowOptions::default(), |window, cx| {
            cx.new(|_| MyView { })
        }).unwrap();
        cx.activate(true);
    });
}

Entity<T> — reactive state containers

An Entity<T> is a handle to a value stored in the framework’s arena. When the value changes and you call cx.notify(), any view rendering that entity re-renders automatically.

#![allow(unused)]
fn main() {
struct AppState {
    user: String,
    count: i32,
}

let state: Entity<AppState> = cx.new(|_cx| AppState {
    user: "Alice".into(),
    count: 0,
});

let name = state.read(cx).user.clone();

state.update(cx, |this, cx| {
    this.count += 1;
    cx.notify();
});
}

Entity vs. direct state

If your view struct holds state directly (like struct Counter { count: i32 }), the view IS the entity — cx.new() wraps it in Entity<Counter> automatically. Use separate entities when you need shared state across views:

#![allow(unused)]
fn main() {
struct Sidebar {
    shared: Entity<AppState>,
}

struct Editor {
    shared: Entity<AppState>,
}
}

The Render trait

Any type that implements Render can be displayed in a window:

#![allow(unused)]
fn main() {
impl Render for MyView {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        div().child("Hello")
    }
}
}

Parameters:

  • &mut self — mutable access to your state
  • window: &mut Window — the window being rendered into (for window-level APIs)
  • cx: &mut Context<Self> — entity-scoped context for creating entities, subscribing to events, and notifying changes

Return: Anything implementing IntoElement — a Div, a Button, or any widget.

Context types

ContextWhere you get itWhat it does
AppApplication::new().run(|cx| { ... })Root context — open windows, set globals
Context<T>impl Render and cx.new() closuresEntity-scoped — notify, observe, subscribe
Windowimpl Render render methodWindow-level — bounds, focus, painting

Getting an entity handle inside render

#![allow(unused)]
fn main() {
impl Render for MyView {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let entity = cx.entity();

        button("click-me")
            .label("Click")
            .on_click(move |_, _, cx| {
                entity.update(cx, |this, cx| {
                    this.handle_click();
                    cx.notify();
                });
            })
    }
}
}

Global state

For app-wide values (theme, user session, config), use the Global trait:

#![allow(unused)]
fn main() {
struct AppConfig {
    dark_mode: bool,
    font_size: f32,
}

impl Global for AppConfig {}

cx.set_global(AppConfig { dark_mode: true, font_size: 14.0 });

cx.read_global::<AppConfig, _>(|config, _| {
    config.dark_mode
});

cx.update_global::<AppConfig, _>(|config, cx| {
    config.dark_mode = false;
});
}

Element composition

Views compose by nesting elements with .child():

#![allow(unused)]
fn main() {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
    div()
        .flex()
        .flex_col()
        .child(self.render_header())
        .child(self.render_content())
        .child(self.render_footer())
}

fn render_header(&self) -> impl IntoElement {
    div().h(px(48.0)).bg(rgb(0x2563eb)).child("Header")
}
}

Conditional rendering

Use .when() for conditional styling or .map() for conditional children:

#![allow(unused)]
fn main() {
div()
    .when(self.is_active, |div| div.bg(rgb(0x2563eb)))
    .when(!self.is_active, |div| div.bg(rgb(0x64748b)))
    .child(if self.show_label { "Active" } else { "Inactive" })
}

Iterating children

Use .children() with an iterator:

#![allow(unused)]
fn main() {
div()
    .flex()
    .flex_col()
    .children(self.items.iter().map(|item| {
        div().px_2().py_1().child(item.name.clone())
    }))
}

Event handling

All events pass (event_data, &mut Window, &mut App):

#![allow(unused)]
fn main() {
div()
    .id("my-element")
    .on_click(|event, window, cx| {
    })
    .on_mouse_down(MouseButton::Left, |event, window, cx| {
    })
    .on_key_down(|event, window, cx| {
    })
}

Widget events use the same pattern:

#![allow(unused)]
fn main() {
text_input("name", self.name.clone())
    .on_change(|new_value, window, cx| {
    })
    .on_submit(|value, window, cx| {
    })
}

Subscriptions and observations

Watch for changes on other entities:

#![allow(unused)]
fn main() {
cx.observe(&other_entity, |this, other, cx| {
    cx.notify();
});

cx.subscribe(&other_entity, |this, _other, event: &MyEvent, cx| {
});
}

Layout & Styling

Kael uses GPU-accelerated flexbox (powered by Taffy) with a Tailwind-inspired API. Every style is a method call on a Div.

Flexbox layout

#![allow(unused)]
fn main() {
div().flex().flex_row().gap_2()
    .child(div().child("Left"))
    .child(div().child("Right"))

div().flex().flex_col().gap_4()
    .child(div().child("Top"))
    .child(div().child("Bottom"))
}

Alignment

#![allow(unused)]
fn main() {
div().flex()
    .items_center()
    .justify_center()
    .justify_between()
    .items_start()
    .items_end()
}

Flex sizing

#![allow(unused)]
fn main() {
div().flex_1()
div().flex_grow()
div().flex_shrink_0()
div().flex_none()
}

Grid layout

Switch a container to CSS Grid with .grid(), define tracks with .grid_cols(n) / .grid_rows(n), and place children with .col_span(n) / .row_span(n) (or .col_span_full() / .row_span_full()). .gap_*() sets the gutters:

#![allow(unused)]
fn main() {
div()
    .grid()
    .grid_cols(5)
    .grid_rows(5)
    .gap_1()
    .child(div().row_span(1).col_span_full().child("Header"))
    .child(div().col_span(1).row_span(3).child("Sidebar"))
    .child(div().col_span(3).row_span(3).child("Content"))
    .child(div().col_span(1).row_span(3).child("Aside"))
    .child(div().row_span(1).col_span_full().child("Footer"))
}

See examples/grid_layout.rs for a full “holy grail” layout.

Sizing

#![allow(unused)]
fn main() {
div().w(px(200.0)).h(px(100.0))

div().w_full()
div().h_full()
div().size_full()

div().size_8()
div().w_12()
div().h_6()

div().min_w(px(200.0)).max_w(px(600.0))
}

Spacing

#![allow(unused)]
fn main() {
div().p_4()
div().px_3()
div().py_2()
div().pt_1()
div().pl(px(20.0))

div().m_4()
div().mx_auto()
div().mt_2()

div().flex().gap_2()
div().flex().gap_4()
}

Colors

#![allow(unused)]
fn main() {
div().bg(rgb(0x1E1E1E))
div().text_color(rgb(0xFFFFFF))
div().border_color(rgb(0x3C3C3C))

div().bg(rgba(0x00000080))

div().bg(kael::red())
div().bg(kael::blue())
div().bg(kael::white())
div().bg(kael::black())

use kael::hsla;
div().bg(hsla(210.0 / 360.0, 1.0, 0.5, 1.0))
}

Borders

#![allow(unused)]
fn main() {
div().border_1()
div().border_2()
div().border_t_1()
div().border_b_1()
div().border_l_1()
div().border_r_1()
div().border_color(rgb(0x3C3C3C))
div().border_dashed()
}

Corners

#![allow(unused)]
fn main() {
div().rounded_sm()
div().rounded_md()
div().rounded_lg()
div().rounded_full()
div().rounded(px(8.0))
}

By default, rounded corners use continuous (squircle) rounding to match SwiftUI’s RoundedRectangle shape on macOS. Use .circular_corners() to opt into the legacy pure quarter-circle look:

#![allow(unused)]
fn main() {
div().rounded(px(8.0)).circular_corners()
}

Shadows

#![allow(unused)]
fn main() {
div().shadow_sm()
div().shadow_md()
div().shadow_lg()
div().shadow_xl()
}

Typography

#![allow(unused)]
fn main() {
div()
    .text_xs()
    .text_sm()
    .text_base()
    .text_lg()
    .text_xl()
    .text_2xl()
    .text_3xl()

div().font_weight(FontWeight::BOLD)
div().font_family(".SystemUIFont")
}

Overflow and scrolling

Control how content behaves when it exceeds the element bounds:

#![allow(unused)]
fn main() {
div().overflow_hidden()
div().overflow_x_scroll()
div().overflow_y_scroll()
div().overflow_y_auto()
    .id("scroll-container")
}

When using overflow_y_scroll() or overflow_y_auto() with a ScrollHandle, Kael automatically renders a macOS-style scrollbar thumb when content overflows. No extra widget is needed:

#![allow(unused)]
fn main() {
let scroll_handle = ScrollHandle::new();

div()
    .id("my-scrollable")
    .overflow_y_scroll()
    .track_scroll(&scroll_handle)
    .child(long_content)
}

The auto-scrollbar appears only when content exceeds the viewport and tracks the scroll position automatically. To keep scrollbars visible at all times (instead of auto-hiding after idle):

#![allow(unused)]
fn main() {
let scroll_handle = ScrollHandle::new().always_show_scrollbars();
}

For custom scrollbar styling, use the explicit scroll_bar() widget instead (see Lists & Data).

Positioning

#![allow(unused)]
fn main() {
div().relative()
    .child(
        div().absolute()
            .top(px(10.0))
            .right(px(10.0))
            .child("Badge")
    )
}

Opacity

#![allow(unused)]
fn main() {
div().opacity(0.5)
}

Cursor

#![allow(unused)]
fn main() {
div().cursor_pointer()
div().cursor_default()
}

Conditional styling with .when()

#![allow(unused)]
fn main() {
div()
    .when(self.is_selected, |this| {
        this.bg(rgb(0x2563eb)).text_color(rgb(0xffffff))
    })
    .when(!self.is_selected, |this| {
        this.bg(rgb(0xffffff)).text_color(rgb(0x000000))
    })
}

Animations

Kael drives animations from its render-on-demand loop: an animating element requests frames only while it is in flight, then the window returns to idle (0% CPU). There are two layers — explicit, time-driven animations you attach to any element, and the framework’s built-in motion such as elastic scrolling.

Animating an element

Bring the AnimationExt trait into scope and call with_animation on any element. You give it a stable id, an Animation describing the timeline, and an animator closure that receives the element and the eased progress delta in 0.0..=1.0:

#![allow(unused)]
fn main() {
use std::time::Duration;
use kael::{Animation, AnimationExt as _, Transformation, bounce, ease_in_out, percentage, svg};

svg()
    .size_20()
    .path(ARROW_CIRCLE_SVG)
    .with_animation(
        "spinner",
        Animation::new(Duration::from_secs(2))
            .repeat_forever()
            .with_easing(bounce(ease_in_out)),
        |svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))),
    )
}

The Animation timeline

#![allow(unused)]
fn main() {
use kael::{Animation, Easing, Repeat};

Animation::new(Duration::from_millis(400))
    .delay(Duration::from_millis(100))   // wait before starting
    .easing(Easing::EaseInOut)           // pick a curve (see below)
    .repeat(Repeat::Count(3));           // Once | Count(n) | Forever
}

repeat_forever() is shorthand for repeat(Repeat::Forever), and with_easing(f) accepts any Fn(f32) -> f32 (including the helpers ease_in_out, ease_out_quint(), and bounce(inner)).

Easing curves

The Easing enum covers the common curves plus a physical spring:

VariantCurve
Easing::Linearconstant rate
Easing::EaseIn / EaseOut / EaseInOutquadratic
Easing::CubicBezier(x1, y1, x2, y2)CSS-style cubic Bézier
Easing::Spring { stiffness, damping, mass }damped spring
Easing::Custom(Rc<dyn Fn(f32) -> f32>)your own
#![allow(unused)]
fn main() {
Animation::new(Duration::from_millis(600))
    .easing(Easing::Spring { stiffness: 180.0, damping: 12.0, mass: 1.0 });
}

Keyframes and sequences

For multi-stop transitions across common styled properties, build a Keyframes set and attach it with with_keyframes:

#![allow(unused)]
fn main() {
use kael::{Animation, AnimationExt as _, Keyframes};

div().with_keyframes(
    "pulse",
    Keyframes::new()
        .at(0.0, |k| k.opacity(0.4))
        .at(0.5, |k| k.opacity(1.0))
        .at(1.0, |k| k.opacity(0.4)),
    Animation::new(Duration::from_secs(1)).repeat_forever(),
)
}

Chain whole animations with AnimationSequence::new().then(...).then_for(duration).with_overlap(...) and drive them with with_animation_sequence. For animations you may need to interrupt, with_cancellable_animation returns an (element, AnimationHandle); call handle.cancel() to jump to the final state.

Elastic scrolling

Scrollable regions — overflow_*_scroll() containers, uniform_list, and list (via ListState) — get native rubber-band overscroll automatically on macOS: content stretches past its bounds on a trackpad pull and springs back on release. Use a ScrollHandle to read or set the offset programmatically:

#![allow(unused)]
fn main() {
use kael::{ScrollHandle, point, px};

let scroll = ScrollHandle::new();
scroll.set_offset(point(px(-360.0), px(0.0)));
let current = scroll.offset();        // Point<Pixels>
let max = scroll.max_offset();        // Size<Pixels>
}

See examples/animation.rs and examples/elastic_scrolling.rs for runnable demos.

Theming

Kael’s theme system provides JSON/TOML-based theming with hot-reload support. The Theme type implements Global, making it available anywhere in your app.

Built-in themes

#![allow(unused)]
fn main() {
// Initialize theme system
Theme::init(cx);

// Switch themes
cx.set_global(Theme::dark());
cx.set_global(Theme::light());

// Match system appearance
cx.set_global(Theme::for_appearance(window));
}

Theme from JSON

{
  "name": "Ocean",
  "appearance": "dark",
  "background": "#0a1628",
  "foreground": "#e2e8f0",
  "primary": "#3b82f6",
  "secondary": "#64748b",
  "accent": "#06b6d4",
  "error": "#ef4444",
  "warning": "#f59e0b",
  "success": "#22c55e",
  "border": "#1e293b",
  "surface": "#0f172a",
  "muted": "#334155"
}

Load it:

#![allow(unused)]
fn main() {
let theme = Theme::from_json_str(json_str)?;
cx.set_global(theme);
}

Theme from TOML

name = "Forest"
appearance = "dark"
background = "#1a2e1a"
foreground = "#d4e6d4"
primary = "#22c55e"
#![allow(unused)]
fn main() {
let theme = Theme::from_toml_str(toml_str)?;
cx.set_global(theme);
}

Loading from file

#![allow(unused)]
fn main() {
let theme = Theme::from_path("themes/custom.json")?;
cx.set_global(theme);
}

Hot-reload

Automatically reload theme when the file changes:

#![allow(unused)]
fn main() {
use kael::ThemeRuntime;

ThemeRuntime::watch("themes/active.json", cx);
// Theme reloads automatically when the file is saved
}

Using theme colors in views

#![allow(unused)]
fn main() {
impl Render for MyView {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let theme = cx.global::<Theme>();

        div()
            .bg(theme.background)
            .text_color(theme.foreground)
            .border_color(theme.border)
            .child(
                div()
                    .bg(theme.primary)
                    .text_color(rgb(0xffffff))
                    .px_4().py_2()
                    .rounded(px(6.0))
                    .child("Primary button")
            )
    }
}
}

Theme properties

PropertyDescription
nameTheme name
appearance"light" or "dark"
backgroundMain background color
foregroundMain text color
primaryPrimary action color
secondarySecondary/muted action color
accentHighlight/accent color
errorError state color
warningWarning state color
successSuccess state color
borderDefault border color
surfaceElevated surface color
mutedSubdued text/element color

Accessibility

Kael’s built-in widgets are accessible by default — every form control reports its role, state, and value to screen readers and supports full keyboard navigation.

Built-in accessibility

All form controls automatically provide:

  • Roles: Button reports as button, checkbox as checkbox, etc.
  • States: Focused, disabled, checked, selected, expanded
  • Values: Slider reports its numeric value, progress reports percentage
  • Labels: Set via .label() builder method
  • Keyboard navigation: Tab between controls, Space/Enter to activate

You get this for free when using the built-in widgets.

Adding accessibility to custom elements

For custom div-based interactive elements, add accessibility attributes:

#![allow(unused)]
fn main() {
div()
    .id("custom-toggle")
    .role(AccessibilityRole::Switch)
    .aria_checked(self.is_on)
    .aria_label("Enable dark mode")
    .on_click(|_, _, cx| { /* toggle */ })
}

Keyboard navigation

Focus management

#![allow(unused)]
fn main() {
// Create a focus handle
let focus = cx.focus_handle();

div()
    .id("panel")
    .track_focus(&focus)
    .on_key_down(|event, window, cx| {
        match event.keystroke.key.as_str() {
            "enter" => { /* activate */ },
            "escape" => { /* cancel */ },
            _ => {}
        }
    })
}

Tab stops

Controls with IDs are automatically tab-focusable. Custom tab order:

#![allow(unused)]
fn main() {
div()
    .id("first-field")
    .tab_index(1)

div()
    .id("second-field")
    .tab_index(2)
}

Label association

Use the label element to associate labels with controls:

#![allow(unused)]
fn main() {
label("Email address", "email-input")
// Clicking the label focuses the associated input

text_input("email-input", self.email.clone())
}

Screen reader announcements

#![allow(unused)]
fn main() {
// Announce to screen readers
window.announce("File saved successfully");
}

Accessibility roles

RoleUsed by
Buttonbutton()
Checkboxcheckbox()
Radioradio_group() options
Sliderslider()
TextInputtext_input()
Switchtoggle()
Dialogmodal()
Tabtabs()
TabPaneltabs() panel content
ProgressBarprogress()
Menucontext menus
MenuItemmenu items
Treetree views
TreeItemtree items

Form Controls

Every form control follows the same pattern:

  1. Create with widget_name(id, value, ...)
  2. Chain builder methods for configuration
  3. Add .on_change() for state updates
  4. Optionally add .render_with() for custom visuals

All controls support keyboard navigation and accessibility out of the box.


Button

A focusable, clickable element with label support.

#![allow(unused)]
fn main() {
use kael::button;

button("save-btn")
    .label("Save File")
    .on_click({
        let entity = entity.clone();
        move |_event, _window, cx| {
            entity.update(cx, |this, cx| {
                this.save();
                cx.notify();
            });
        }
    })
}

Builder methods:

MethodDescription
.label(text)Display text
.disabled()Disable interaction
.on_click(handler)Click handler (|event, window, cx| { ... })
.render_with(renderer)Custom rendering with ButtonRenderState

ButtonRenderState fields: label: Option<SharedString>, focused: bool, disabled: bool


TextInput

Full-featured text field with selection, clipboard, undo/redo, and password masking.

#![allow(unused)]
fn main() {
use kael::text_input;

text_input("project_name", self.name.clone())
    .placeholder("Enter project name")
    .on_change({
        let entity = entity.clone();
        move |value, _window, cx| {
            entity.update(cx, |this, cx| {
                this.name = value;
                cx.notify();
            });
        }
    })
}

Builder methods:

MethodDescription
.placeholder(text)Placeholder text when empty
.multi_line()Enable multiline editing
.max_lines(n)Limit visible height
.password()Mask input characters
.mask(impl InputMask)Custom input normalization
.on_change(handler)Text change handler (|value: SharedString, window, cx|)
.on_submit(handler)Enter key handler (|value: SharedString, window, cx|)
.render_with(renderer)Custom rendering with TextInputRenderState

TextInputRenderState fields: value, display_text, placeholder, showing_placeholder, focused, hovered, multi_line, outer_bounds, field_bounds, text_bounds, line_height, lines, selection_bounds, cursor_bounds

Custom rendering helpers on state: state.paint_selection(color, window), state.paint_text(window, cx), state.paint_cursor(color, window)


Checkbox

Three-state checkbox (checked, unchecked, indeterminate) with undo/redo.

#![allow(unused)]
fn main() {
use kael::checkbox;

checkbox("notifications", self.enabled)
    .label("Enable notifications")
    .on_change({
        let entity = entity.clone();
        move |checked, _window, cx| {
            entity.update(cx, |this, cx| {
                this.enabled = *checked;
                cx.notify();
            });
        }
    })
}

Builder methods:

MethodDescription
.label(text)Label text
.indeterminate(bool)Show indeterminate state
.disabled()Disable interaction
.on_change(handler)State change (|&bool, window, cx|)
.render_with(renderer)Custom rendering with CheckboxRenderState

CheckboxRenderState fields: checked, indeterminate, label, focused, disabled


Toggle

Boolean on/off switch with undo/redo.

#![allow(unused)]
fn main() {
use kael::toggle;

toggle("dark_mode", self.dark_mode)
    .label("Dark mode")
    .on_change({
        let entity = entity.clone();
        move |on, _window, cx| {
            entity.update(cx, |this, cx| {
                this.dark_mode = *on;
                cx.notify();
            });
        }
    })
}

Builder methods:

MethodDescription
.label(text)Label text
.disabled()Disable interaction
.on_change(handler)State change (|&bool, window, cx|)
.render_with(renderer)Custom rendering with ToggleRenderState

ToggleRenderState fields: on, label, focused, disabled


RadioGroup

Mutually exclusive option selection with generic value types.

#![allow(unused)]
fn main() {
use kael::radio_group;

#[derive(Clone, Copy, PartialEq, Eq)]
enum Theme { Light, Dark, System }

radio_group("theme", self.theme, [
    (Theme::Light, "Light"),
    (Theme::Dark, "Dark"),
    (Theme::System, "System"),
])
.on_change({
    let entity = entity.clone();
    move |value, _window, cx| {
        entity.update(cx, |this, cx| {
            this.theme = *value;
            cx.notify();
        });
    }
})
}

Builder methods:

MethodDescription
.on_change(handler)Selection change (|&T, window, cx|)
.render_with(renderer)Custom rendering per option with RadioRenderState

RadioRenderState fields: value, label, index, selected, focused


Slider

Continuous or discrete value control with drag support.

#![allow(unused)]
fn main() {
use kael::slider;

slider("volume", self.volume)
    .min(0.0)
    .max(100.0)
    .step(5.0)
    .on_change({
        let entity = entity.clone();
        move |value, _window, cx| {
            entity.update(cx, |this, cx| {
                this.volume = *value;
                cx.notify();
            });
        }
    })
}

Builder methods:

MethodDescription
.min(f64)Minimum value (default: 0.0)
.max(f64)Maximum value (default: 100.0)
.step(f64)Keyboard increment (default: 1.0)
.discrete()Snap to step values
.vertical()Vertical orientation
.disabled()Disable interaction
.on_change(handler)Value change (|&f64, window, cx|)
.render_with(renderer)Custom rendering with SliderRenderState

SliderRenderState fields: value, min, max, percentage, dragging, focused, disabled


Select

Dropdown with popup menu, optional search, and generic value types.

#![allow(unused)]
fn main() {
use kael::select;

select("accent", self.accent, [
    (AccentColor::Blue, "Atlantic"),
    (AccentColor::Green, "Forest"),
    (AccentColor::Orange, "Ember"),
])
.placeholder("Choose an accent")
.searchable()
.on_change({
    let entity = entity.clone();
    move |value, _window, cx| {
        entity.update(cx, |this, cx| {
            this.accent = *value;
            cx.notify();
        });
    }
})
}

Builder methods:

MethodDescription
.placeholder(text)Placeholder when nothing selected
.searchable()Enable type-to-filter in popup
.on_change(handler)Selection change (|&T, window, cx|)
.render_with(renderer)Custom trigger rendering with SelectRenderState
.render_options_with(renderer)Custom option row rendering with SelectOptionRenderState<T>
.render_popup_with(renderer)Custom popup shell with SelectPopupRenderState
.render_search_with(renderer)Custom search field with SelectSearchRenderState

SelectRenderState fields: open, display_text, selected_label, placeholder, showing_placeholder, focused


DatePicker

Calendar-based date selection with month/year navigation.

#![allow(unused)]
fn main() {
use kael::date_picker;
use time::Date;

date_picker("delivery", self.delivery_date)
    .on_change({
        let entity = entity.clone();
        move |date, _window, cx| {
            entity.update(cx, |this, cx| {
                this.delivery_date = *date;
                cx.notify();
            });
        }
    })
}

Builder methods:

MethodDescription
.on_change(handler)Date selection (|&Date, window, cx|)
.render_with(renderer)Custom trigger rendering
.render_days_with(renderer)Custom day cell rendering with DateCellRenderState

DateCellRenderState fields: day, selected, highlighted, disabled, today

Display & Feedback

Elements for showing information and providing feedback to users.


Text

Basic text rendering. Strings passed to .child() automatically become text elements:

#![allow(unused)]
fn main() {
div().child("Hello, world!")

div().text_xl().text_color(rgb(0x2563eb)).child("Title")
}

For styled inline text, use SharedString:

#![allow(unused)]
fn main() {
use kael::SharedString;

let label: SharedString = "Click me".into();
div().child(label)
}

Label

Accessible label that forwards focus to a target control:

#![allow(unused)]
fn main() {
use kael::label;

label("Email address", "email-input")
// Clicking the label focuses the text_input with id "email-input"
}

Icon

Render named icons from the icon set:

#![allow(unused)]
fn main() {
use kael::icon;

icon("folder")
icon("file").size(px(16.0))
}

Image

Display raster images with caching:

#![allow(unused)]
fn main() {
use kael::{img, ImageSource};

img(ImageSource::from_path("photo.png"))
    .w(px(200.0))
    .h(px(150.0))
    .rounded_md()
}

SVG

Render SVG content:

#![allow(unused)]
fn main() {
use kael::svg;

svg()
    .path("icons/logo.svg")
    .w(px(24.0))
    .h(px(24.0))
    .text_color(rgb(0x2563eb))  // fills SVG with color
}

RichText

Compose text from inline styled spans, clickable entities (links, mentions, hashtags), inline code, and embedded elements:

#![allow(unused)]
fn main() {
use kael::{rich_text, FontWeight, HighlightStyle, rgb};

rich_text()
    .text("The ")
    .styled("quick brown fox", HighlightStyle {
        color: Some(rgb(0xb45309).into()),
        font_weight: Some(FontWeight::BOLD),
        ..Default::default()
    })
    .text(" jumps. See the ")
    .link("docs", "https://augani.github.io/kael/", |_window, _app| {})
    .text(" or ping ")
    .mention("@team", "team-id", |_window, _app| {})
    .text(". Run ")
    .code("cargo run")
    .selectable(true)
}

Builder methods: .text(), .styled(text, HighlightStyle), .link(text, target, on_click), .mention(text, payload, on_click), .hashtag(text, payload, on_click), .code(text), .inline_element(element), .selectable(bool), .selection_color(color). Entity click handlers have the signature Fn(&mut Window, &mut App).


Progress

Determinate or indeterminate progress indicator:

#![allow(unused)]
fn main() {
use kael::progress;

// Determinate (0.0 to 1.0)
progress("export", 0.65)

// Indeterminate
progress("loading", 0.0).indeterminate()

// Custom rendering
progress("download", self.progress)
    .render_with(|state, bounds, window, _cx| {
        // state.percentage: Option<f64>
        // state.indeterminate: bool
        // Paint track and fill bar using window.paint_quad()
        window.paint_quad(fill(bounds, rgb(0xe2e8f0)).corner_radii(px(4.0)));
        if let Some(pct) = state.percentage {
            let width = bounds.size.width * pct as f32;
            window.paint_quad(fill(
                Bounds::new(bounds.origin, size(width, bounds.size.height)),
                rgb(0x2563eb),
            ).corner_radii(px(4.0)));
        }
    })
}

ProgressRenderState fields: value, max, percentage, indeterminate


Toast

Auto-dismissing notification overlay:

#![allow(unused)]
fn main() {
use kael::{Toast, ToastStack};

// In your view, create a ToastStack entity
struct MyApp {
    toasts: Entity<ToastStack>,
}

// Create it
let toasts = cx.new(|_| ToastStack::new());

// Push a toast from anywhere with the entity handle
toasts.update(cx, |stack, cx| {
    stack.push(
        Toast::new("File saved")
            .body("changes written to disk")
            .duration(Duration::from_secs(3)),
        window,
        cx,
    );
});

// Render the stack in your view
impl Render for MyApp {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        div()
            .size_full()
            .child(/* your content */)
            .child(self.toasts.clone()) // ToastStack renders as overlay
    }
}
}

Toast positions: ToastPosition::TopRight, TopCenter, BottomRight


Canvas

GPU-accelerated custom drawing surface:

#![allow(unused)]
fn main() {
use kael::canvas;

canvas(|bounds, window, cx| {
    // Custom painting with window.paint_quad(), window.paint_path(), etc.
    window.paint_quad(fill(bounds, rgb(0x1E1E1E)));
})
.w(px(400.0))
.h(px(300.0))
}

Canvas & Graphics

Beyond the element tree, Kael gives you direct GPU drawing: an immediate-mode canvas, a vector path builder, gradients, backdrop blur, SVG, and Lottie playback. Everything renders through the same per-platform pipeline (Metal / DirectX 11 / Vulkan) with device-pixel snapping for crisp output at any DPI.

Canvas

canvas takes two closures — a prepaint pass (compute layout/state, returns a value) and a paint pass (draw into the bounds). Inside paint you call window.paint_quad and window.paint_path:

#![allow(unused)]
fn main() {
use kael::{canvas, fill, quad, px, rgb, Bounds, Pixels, Window, App};

canvas(
    move |_bounds: Bounds<Pixels>, _window: &mut Window, _app: &mut App| {
        // prepaint: return any state the paint pass needs
    },
    move |bounds: Bounds<Pixels>, _state, window: &mut Window, _app: &mut App| {
        window.paint_quad(fill(bounds, rgb(0x1e1e1e)));
        // window.paint_path(path, color);
    },
)
.size_full()
}

Vector paths

Build filled or stroked paths with PathBuilder, then hand the result to window.paint_path:

#![allow(unused)]
fn main() {
use kael::{PathBuilder, point, px};

let mut builder = PathBuilder::fill();        // or PathBuilder::stroke(px(2.0))
builder.move_to(point(px(50.0), px(50.0)));
builder.line_to(point(px(130.0), px(50.0)));
builder.curve_to(point(px(130.0), px(130.0)), point(px(160.0), px(90.0))); // quadratic
builder.close();
let path = builder.build()?;
}

Segment methods: move_to, line_to, curve_to (quadratic Bézier), cubic_curve_to, arc_to, and close. Stroked builders also accept dash_array / dash_offset.

Gradients

Gradients are backgrounds you pass to .bg(...):

#![allow(unused)]
fn main() {
use kael::{linear_gradient, linear_color_stop, rgb};

div().bg(linear_gradient(
    45.0,
    linear_color_stop(rgb(0xff0080), 0.0),
    linear_color_stop(rgb(0x7928ca), 1.0),
))
}

Also available: multi_stop_linear_gradient(angle, &[stops]), radial_gradient(cx, cy, radius, &[stops]), and conic_gradient(cx, cy, angle_offset, &[stops]).

Backdrop blur & frosted glass

backdrop_blur blurs whatever is painted behind an element — combine it with a translucent background for a frosted-glass panel:

#![allow(unused)]
fn main() {
use kael::{px, rgba};

div()
    .backdrop_blur(px(20.0))
    .bg(rgba(0xffffff20))
    .rounded_xl()
}

SVG

svg() renders a vector asset; text_color fills monochrome SVGs and with_transformation applies rotation/scale:

#![allow(unused)]
fn main() {
use kael::{svg, px, rgb};

svg().path("icons/logo.svg").size(px(24.0)).text_color(rgb(0x2563eb))
}

Lottie

lottie() plays Lottie/dotLottie animations, decoding frames on a background thread so the UI stays responsive:

#![allow(unused)]
fn main() {
use kael::{lottie, LoopMode};

lottie("animations/loader.json")
    .autoplay()
    .loop_forever()            // or .loop_mode(LoopMode::Loop) / .ping_pong()
}

Builders: .autoplay(), .loop_forever(), .loop_mode(LoopMode), .ping_pong(), .object_fit(ObjectFit), .prefetch_frames(n), .with_loading(|| element), .with_fallback(|| element).

See examples/painting.rs, gradient.rs, pattern.rs, shadow.rs, svg/main.rs, and gif_viewer.rs.

Containers & Overlays

Components for organizing content, managing layers, and showing floating UI.


Controlled dialog overlay with backdrop, escape-to-dismiss, and click-outside handling:

#![allow(unused)]
fn main() {
use kael::modal;

modal("confirm-dialog", self.is_open)
    .label("Confirm action")
    .backdrop(hsla(0.0, 0.0, 0.0, 0.5))
    .dismiss_on_escape(true)
    .dismiss_on_click_outside(true)
    .render_with({
        let entity = entity.clone();
        move |state, _window, _cx| {
            div()
                .w(px(400.0))
                .p_6()
                .bg(rgb(0xffffff))
                .rounded(px(12.0))
                .shadow_xl()
                .flex().flex_col().gap_4()
                .child(div().text_lg().child("Are you sure?"))
                .child(div().child("This action cannot be undone."))
                .child(
                    div().flex().justify_end().gap_2()
                        .child(button("cancel").label("Cancel")
                            .on_click({
                                let entity = entity.clone();
                                move |_, _, cx| {
                                    entity.update(cx, |this, cx| {
                                        this.is_open = false;
                                        cx.notify();
                                    });
                                }
                            }))
                        .child(button("confirm").label("Confirm")
                            .on_click({
                                let entity = entity.clone();
                                move |_, _, cx| {
                                    entity.update(cx, |this, cx| {
                                        this.do_action();
                                        this.is_open = false;
                                        cx.notify();
                                    });
                                }
                            }))
                )
                .into_any_element()
        }
    })
    .on_change({
        let entity = entity.clone();
        move |open, _window, cx| {
            entity.update(cx, |this, cx| {
                this.is_open = *open;
                cx.notify();
            });
        }
    })
}

ModalRenderState fields: open, label, focused


Popover

Anchored floating panel with positioning:

#![allow(unused)]
fn main() {
use kael::popover;

popover("color-picker")
    .anchor(|_window, _cx| {
        button("show-colors").label("Colors").into_any_element()
    })
    .popup(|_window, _cx| {
        div()
            .w(px(200.0))
            .p_3()
            .bg(rgb(0xffffff))
            .shadow_lg()
            .rounded(px(8.0))
            .child("Color picker content")
            .into_any_element()
    })
    .dismiss_on_escape(true)
    .dismiss_on_click_outside(true)
}

Tabs

Tabbed content switcher with keyboard navigation:

#![allow(unused)]
fn main() {
use kael::tabs;

#[derive(Clone, Copy, PartialEq, Eq)]
enum EditorTab { Code, Preview, Settings }

tabs("editor-tabs", self.active_tab, [
    TabItem::new(EditorTab::Code, "Code", |_w, _cx| {
        div().child("Code editor here").into_any_element()
    }),
    TabItem::new(EditorTab::Preview, "Preview", |_w, _cx| {
        div().child("Live preview").into_any_element()
    }),
    TabItem::new(EditorTab::Settings, "Settings", |_w, _cx| {
        div().child("Editor settings").into_any_element()
    }),
])
.on_change({
    let entity = entity.clone();
    move |tab, _window, cx| {
        entity.update(cx, |this, cx| {
            this.active_tab = *tab;
            cx.notify();
        });
    }
})
}

TabRenderState fields: value, label, index, tab_count, selected, focused


Disclosure

Collapsible section (accordion):

#![allow(unused)]
fn main() {
use kael::disclosure;

disclosure("advanced-settings", self.expanded)
    .trigger(|_w, _cx| {
        div().child("Advanced Settings ▾").into_any_element()
    })
    .panel(|_w, _cx| {
        div().p_3().child("Hidden content here").into_any_element()
    })
    .on_change({
        let entity = entity.clone();
        move |open, _window, cx| {
            entity.update(cx, |this, cx| {
                this.expanded = *open;
                cx.notify();
            });
        }
    })
}

Splitter

Draggable pane divider for resizable layouts:

#![allow(unused)]
fn main() {
use kael::splitter;

splitter("main-split", self.split_ratio)
    .on_change({
        let entity = entity.clone();
        move |ratio, _window, cx| {
            entity.update(cx, |this, cx| {
                this.split_ratio = *ratio;
                cx.notify();
            });
        }
    })
}

Use the ratio value to size adjacent panes:

#![allow(unused)]
fn main() {
let left_width = self.split_ratio * total_width;
div().flex().flex_row()
    .child(div().w(px(left_width)).child("Left pane"))
    .child(splitter("split", self.split_ratio).on_change(/* ... */))
    .child(div().flex_1().child("Right pane"))
}

Context Menu

Right-click menus via .context_menu() on any Div:

#![allow(unused)]
fn main() {
div()
    .id("file-item")
    .child("document.txt")
    .context_menu(|menu| {
        menu.item("Open", |_w, cx| { /* handle open */ })
            .item("Rename", |_w, cx| { /* handle rename */ })
            .separator()
            .item("Delete", |_w, cx| { /* handle delete */ })
    })
}

Tooltip

Hover information via .tooltip() on any Div:

#![allow(unused)]
fn main() {
div()
    .id("save-icon")
    .child(icon("save"))
    .tooltip("Save file (Cmd+S)")

// Custom tooltip content
div()
    .id("status")
    .child("●")
    .tooltip_element(|| {
        div()
            .p_2()
            .bg(rgb(0x1E1E1E))
            .text_color(rgb(0xffffff))
            .rounded(px(4.0))
            .child("Connected to server")
    })
}

Layer

Managed layer system for in-window modals and popovers:

#![allow(unused)]
fn main() {
use kael::layer;

layer("notification-layer")
    .placement(LayerPlacement::Centered)
    .child(/* floating content */)
}

Lists & Data

High-performance list components with virtualization for rendering thousands of items.


UniformList

Highest-performance list for items of equal height. Only renders visible items — handles 100K+ items smoothly:

#![allow(unused)]
fn main() {
use kael::{uniform_list, UniformListScrollHandle};

let scroll_handle = UniformListScrollHandle::new();

uniform_list(
    "log-entries",
    self.entries.len(),
    {
        let entries = self.entries.clone();
        move |range, _window, _cx| {
            entries[range.clone()]
                .iter()
                .map(|entry| {
                    div()
                        .px_3()
                        .py_1()
                        .text_sm()
                        .child(entry.message.clone())
                        .into_any_element()
                })
                .collect()
        }
    },
)
.track_scroll(scroll_handle.clone())
}

When to use: Log viewers, file lists, data tables — any list where every row has the same height.


List

Flexible list with alignment and overflow handling:

#![allow(unused)]
fn main() {
use kael::list;

// Basic list
list()
    .child(div().child("Item 1"))
    .child(div().child("Item 2"))
    .child(div().child("Item 3"))
}

RecyclingList

Virtualized list for items with different heights. Recycles DOM nodes for performance:

#![allow(unused)]
fn main() {
use kael::recycling_list;

recycling_list(
    "messages",
    self.messages.len(),
    move |index, _window, _cx| {
        let msg = &messages[index];
        div()
            .p_3()
            .child(div().font_weight(FontWeight::BOLD).child(msg.sender.clone()))
            .child(div().text_sm().child(msg.body.clone()))
            .into_any_element()
    },
)
}

When to use: Chat messages, feed items — lists where rows vary in height.


SortableList

Drag-to-reorder list with auto-scroll and insertion indicator:

#![allow(unused)]
fn main() {
use kael::sortable_list;

sortable_list(
    "layers",
    self.layers.len(),
    {
        let layers = self.layers.clone();
        move |index, _window, _cx| {
            div()
                .px_3()
                .py_2()
                .child(layers[index].name.clone())
                .into_any_element()
        }
    },
)
.on_reorder({
    let entity = entity.clone();
    move |from, to, _window, cx| {
        entity.update(cx, |this, cx| {
            let item = this.layers.remove(from);
            this.layers.insert(to, item);
            cx.notify();
        });
    }
})
}

When to use: Layer panels, playlist editors, kanban columns — anywhere users reorder items by dragging.


ScrollBar

Kael provides automatic scrollbars for any element with overflow_y_scroll() or overflow_y_auto() and a tracked ScrollHandle. The scrollbar appears as a native-style dark rounded thumb when content overflows — no extra code needed (see Layout & Styling).

For custom scroll bar rendering, use the explicit scroll_bar() widget:

#![allow(unused)]
fn main() {
use kael::scroll_bar;

scroll_bar(scroll_handle.clone())
    .render_with(|state, bounds, window, _cx| {
        // Custom scroll bar rendering
        // state.thumb_bounds, state.dragging
    })
}

Patterns

Data table with uniform_list

#![allow(unused)]
fn main() {
struct DataTable {
    rows: Vec<Row>,
    columns: Vec<Column>,
    scroll: UniformListScrollHandle,
}

impl Render for DataTable {
    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        let columns = self.columns.clone();
        let rows = self.rows.clone();

        div().flex().flex_col().size_full()
            .child(self.render_header())
            .child(
                uniform_list("table-body", rows.len(), move |range, _w, _cx| {
                    rows[range.clone()].iter().map(|row| {
                        div().flex().flex_row()
                            .children(columns.iter().map(|col| {
                                div().w(px(col.width)).px_2().py_1()
                                    .child(row.get(&col.key).clone())
                            }))
                            .into_any_element()
                    }).collect()
                })
                .track_scroll(self.scroll.clone())
            )
    }
}
}

Platform APIs

Kael provides native platform integration matching (and exceeding) Electron’s capabilities. All APIs work cross-platform on macOS, Windows, and Linux.


File Dialogs

Native open/save file pickers:

#![allow(unused)]
fn main() {
// Open file dialog
let paths = cx.prompt_for_paths(PathPromptOptions {
    files: true,
    directories: false,
    multiple: true,
    prompt: Some("Open".into()),
}).await;

// Save file dialog
let path = cx.prompt_for_new_path(
    &std::env::current_dir()?,
    Some("document.txt"),
).await;
}

Native Menus

Application menu bar (macOS menu bar, Windows/Linux window menu):

#![allow(unused)]
fn main() {
cx.set_menus(vec![
    Menu {
        name: "File".into(),
        items: vec![
            MenuItem::action("New", menu_action::New),
            MenuItem::action("Open...", menu_action::Open),
            MenuItem::separator(),
            MenuItem::action("Save", menu_action::Save),
            MenuItem::action("Save As...", menu_action::SaveAs),
            MenuItem::separator(),
            MenuItem::action("Quit", menu_action::Quit),
        ],
    },
    Menu {
        name: "Edit".into(),
        items: vec![
            MenuItem::action("Undo", menu_action::Undo),
            MenuItem::action("Redo", menu_action::Redo),
            MenuItem::separator(),
            MenuItem::action("Cut", menu_action::Cut),
            MenuItem::action("Copy", menu_action::Copy),
            MenuItem::action("Paste", menu_action::Paste),
        ],
    },
]);
}

System Tray

Tray icon with menu and click handling:

#![allow(unused)]
fn main() {
// Set tray menu
cx.set_tray_menu(vec![
    TrayMenuItem::Action {
        label: "Show Window".into(),
        id: "show".into(),
    },
    TrayMenuItem::Separator,
    TrayMenuItem::Action {
        label: "Quit".into(),
        id: "quit".into(),
    },
]);

cx.set_tray_tooltip("My App — Running");

// Handle tray menu actions
cx.on_tray_menu_action(|action_id, cx| {
    if action_id.as_ref() == "show" {
        // bring window to front
    } else if action_id.as_ref() == "quit" {
        cx.quit();
    }
});

// Handle tray icon clicks
cx.on_tray_icon_event(|event, cx| {
    match event {
        TrayIconEvent::LeftClick => { /* toggle window */ },
        TrayIconEvent::DoubleClick => { /* show window */ },
        _ => {}
    }
});
}

Clipboard

Read and write text and images:

#![allow(unused)]
fn main() {
// Write text
cx.write_to_clipboard(ClipboardItem::new_string("Hello, clipboard!".into()));

// Write text with metadata
cx.write_to_clipboard(ClipboardItem::new_string_with_metadata(
    "formatted text".into(),
    json!({"source": "my_app"}).to_string(),
));

// Read
if let Some(item) = cx.read_from_clipboard() {
    if let Some(text) = item.text() {
        println!("Got: {}", text);
    }
}
}

Global Hotkeys

System-wide keyboard shortcuts (work even when app is unfocused):

#![allow(unused)]
fn main() {
cx.register_global_hotkey(1, &Keystroke::parse("cmd-shift-k")?)?;

cx.on_global_hotkey(|id| {
    match id {
        1 => { /* Cmd+Shift+K pressed anywhere */ },
        _ => {}
    }
});
}

Notifications

OS-level notifications (not in-app toasts):

#![allow(unused)]
fn main() {
cx.show_notification("Build Complete", "All tests passed")?;

cx.show_notification_with_actions(
    "Update Available",
    "Version 2.0 is ready to install",
    &[
        NotificationAction { id: "install".into(), label: "Install Now".into() },
        NotificationAction { id: "later".into(), label: "Remind Later".into() },
    ],
    |action_id| {
        println!("User clicked: {}", action_id);
    },
)?;
}

Deep Linking

Register and handle custom URL schemes. These methods are called on Application before .run():

#![allow(unused)]
fn main() {
Application::new()
    // Handle all opened URLs
    .on_open_urls(|urls| {
        for url in urls {
            println!("Opened: {}", url);
        }
    })
    // Handle specific scheme with app context
    .on_deep_link("myapp", |url, cx| {
        // Handle myapp://path/to/resource
    })
    .run(|cx| {
        // ...
    });
}

Multi-Window

Open multiple windows with independent views:

#![allow(unused)]
fn main() {
cx.open_window(
    WindowOptions {
        window_bounds: Some(WindowBounds::Windowed(bounds)),
        ..Default::default()
    },
    |_window, cx| cx.new(|_| SettingsView::new()),
).unwrap();
}

Kael windows follow native platform conventions automatically:

  • Scroll-to-focus — scrolling over an unfocused Kael window activates it, matching standard macOS/Windows behavior
  • Smooth zoom — double-clicking the titlebar animates the window to fill the screen using native Core Animation transitions
  • Live resize — content reflows smoothly during window drag-resizing

Auto-Update

Built-in application update pipeline:

#![allow(unused)]
fn main() {
let config = AutoUpdaterConfig {
    feed_url: "https://releases.myapp.com/appcast.xml".into(),
    ..Default::default()
};

let updater = AutoUpdater::new(config, current_version, http_client);

// Check for updates
let status = updater.check_for_updates().await;
match status {
    UpdateStatus::UpdateAvailable(info) => {
        println!("New version: {}", info.version);
    }
    UpdateStatus::UpToDate => println!("Already up to date"),
    _ => {}
}
}

Printing

Native print dialog and custom rendering:

#![allow(unused)]
fn main() {
let job = PrintJob::new("Document")
    .orientation(PrintOrientation::Portrait)
    .page(PrintPage::new(size(px(612.0), px(792.0)), |ctx| {
        ctx.draw_text("Hello, printed world!", point(72.0, 72.0), style);
    }));

window.show_print_dialog(job);
}

Power Management

Prevent sleep and detect power state:

#![allow(unused)]
fn main() {
// Prevent display sleep during video playback
let blocker = cx.start_power_save_blocker(PowerSaveBlockerKind::PreventDisplaySleep);

// Check power mode
match cx.power_mode() {
    PowerMode::Performance => { /* full quality */ },
    PowerMode::LowPower => { /* reduce effects */ },
    _ => {}
}

// Detect idle time
if let Some(idle) = cx.system_idle_time() {
    if idle > Duration::from_secs(300) { /* user is away */ }
}

// Listen for sleep/wake
cx.on_system_power_event(|event, cx| {
    match event {
        SystemPowerEvent::WillSleep => { /* save state */ },
        SystemPowerEvent::DidWake => { /* refresh data */ },
        _ => {}
    }
});
}

Session Persistence

Save and restore window positions across launches:

#![allow(unused)]
fn main() {
let store = SessionStore::new("my-app")?;

// Save current window layout
store.save_window_states(&window_states)?;

// Restore on next launch
if let Ok(states) = store.load_window_states() {
    for (id, state) in &states {
        cx.open_window(WindowOptions {
            window_bounds: Some(state.bounds),
            ..Default::default()
        }, |_, cx| cx.new(|_| MyView::new()));
    }
}
}

Display Information

Enumerate monitors and get DPI:

#![allow(unused)]
fn main() {
let displays = cx.displays();
let primary = cx.primary_display();

for display in &displays {
    println!("Display {}: {:?}", display.id(), display.bounds());
}
}

Crash Reporting

Automatic crash capture with remote submission:

#![allow(unused)]
fn main() {
use kael::CrashReporter;

let mut reporter = CrashReporter {
    app_id: "my-app".into(),
    crash_dir: std::env::temp_dir().join("crashes"),
    ..Default::default()
};

reporter.install_hook();
}

App Lifecycle

Launch at login and update the dock/taskbar:

#![allow(unused)]
fn main() {
cx.set_auto_launch("com.example.app", true)?;
let enabled = cx.is_auto_launch_enabled("com.example.app");

cx.set_dock_badge(Some("3"));   // None clears it
cx.set_dock_menu(dock_menu_items);
}

Enforce a single running instance — acquire a lock at startup and forward later launches to the existing process:

#![allow(unused)]
fn main() {
use kael::{SingleInstance, send_activate_to_existing};

match SingleInstance::acquire("com.example.app") {
    Ok(instance) => {
        instance.on_activate(Box::new(|| { /* focus the existing window */ }));
        // ... run the app ...
    }
    Err(_already_running) => {
        send_activate_to_existing("com.example.app")?;
        return; // this duplicate launch exits
    }
}
}

Biometric Authentication

Gate sensitive actions behind Touch ID / Face ID / Windows Hello. Check availability, then prompt with a reason string and a completion callback:

#![allow(unused)]
fn main() {
use kael::BiometricStatus;

if let BiometricStatus::Available(_kind) = cx.biometric_status() {
    cx.authenticate_biometric("Unlock your vault", |success| {
        if success { /* proceed */ }
    });
}
}

BiometricStatus is Available(BiometricKind) or Unavailable; BiometricKind identifies the method (Touch ID, Face ID, fingerprint, Windows Hello).

Screen & Media Capture

Enumerate capturable displays/windows and stream frames (build with the screen-capture feature):

#![allow(unused)]
fn main() {
if cx.is_screen_capture_supported() {
    // enumerate sources via cx.screen_capture_sources(..), then start a
    // capture stream whose frames arrive as ScreenCaptureFrame values
}
}

See examples/capture_demo.rs.

Examples Gallery

Kael ships with 40+ runnable examples. Clone the repo and run any example:

git clone https://github.com/Augani/kael.git
cd kael
cargo run -p kael --example <name>

Getting started

ExampleCommandWhat it shows
Hello Worldcargo run -p kael --example hello_worldMinimal window with styled text and colored boxes
Form Controlscargo run -p kael --example form_controlsEvery form widget: text input, checkbox, toggle, slider, radio, select, date picker, modal
Inputcargo run -p kael --example inputText input with custom rendering

Layout & styling

ExampleCommandWhat it shows
Grid Layoutcargo run -p kael --example grid_layoutCSS Grid-style layouts
Gradientcargo run -p kael --example gradientLinear and radial gradients
Shadowcargo run -p kael --example shadowBox shadow effects
Opacitycargo run -p kael --example opacityTransparency and blending
Patterncargo run -p kael --example patternRepeating pattern fills
Windowcargo run -p kael --example windowWindow options and configuration
Window Positioningcargo run -p kael --example window_positioningMulti-display window placement

Lists & data

ExampleCommandWhat it shows
Data Tablecargo run -p kael --example data_tableVirtual data table with sorting and selection
Uniform Listcargo run -p kael --example uniform_listHigh-performance uniform-height list
Recycling Listcargo run -p kael --example recycling_listVariable-height virtualized list
Treecargo run -p kael --example treeExpandable tree view
Scrollablecargo run -p kael --example scrollableScroll containers with elastic scrolling
Elastic Scrollingcargo run -p kael --example elastic_scrollingMomentum and bounce scrolling

Text & rendering

ExampleCommandWhat it shows
Textcargo run -p kael --example textText rendering and font features
Text Layoutcargo run -p kael --example text_layoutText measurement and line breaking
Text Wrappercargo run -p kael --example text_wrapperWord wrap and text overflow
Paintingcargo run -p kael --example paintingCustom GPU painting
SVGcargo run -p kael --example svgSVG rendering
Crispness Showcasecargo run -p kael --example crispness_showcasePixel snapping, hairline strokes, sRGB gradients, corner shapes
Native Comparisoncargo run -p kael --example native_comparisonSide-by-side comparison with native AppKit rendering

Media & animation

ExampleCommandWhat it shows
Animationcargo run -p kael --example animationKeyframe and spring animations
GIF Viewercargo run -p kael --example gif_viewerAnimated GIF playback
Imagecargo run -p kael --example imageImage loading and display
Image Gallerycargo run -p kael --example image_galleryGallery with lazy loading
Image Loadingcargo run -p kael --example image_loadingAsync image loading patterns

Platform integration

ExampleCommandWhat it shows
Set Menuscargo run -p kael --example set_menusNative application menus
Tray Testcargo run -p kael --example tray_testSystem tray icon with menu
Platform Featurescargo run -p kael --example platform_featuresPlatform capability detection
Print Democargo run -p kael --example print_demoNative printing
WebView Democargo run -p kael --example webview_demoEmbedded web content
Capture Democargo run -p kael --example capture_demoScreen/media capture
Drag & Dropcargo run -p kael --example drag_dropFile drag-and-drop

Advanced

ExampleCommandWhat it shows
Plugin Hostcargo run -p kael --example plugin_hostExtension loading and management
Daemon Appcargo run -p kael --example daemon_appBackground daemon with tray
Tab Stopcargo run -p kael --example tab_stopKeyboard focus navigation
Window Shadowcargo run -p kael --example window_shadowCustom window chrome
On Window Close Quitcargo run -p kael --example on_window_close_quitWindow lifecycle handling

Benchmarks

ExampleCommandWhat it shows
Perf Benchcargo run -p kael --example perf_bench --releaseRendering performance measurement
Paths Benchcargo run -p kael --example paths_bench --releasePath rendering performance

Gestures

Kael provides built-in gesture recognizers for touch and pointer interactions.

Pan gesture

Detect drag/pan movements with velocity tracking:

#![allow(unused)]
fn main() {
use kael::gesture::PanGesture;

let pan = PanGesture::new()
    .min_distance(px(5.0))
    .on_start(|position, _window, _cx| { /* drag started */ })
    .on_update(|delta, velocity, _window, _cx| { /* dragging */ })
    .on_end(|velocity, _window, _cx| { /* drag ended */ });
}

Swipe gesture

Detect directional swipes:

#![allow(unused)]
fn main() {
use kael::gesture::SwipeGesture;

let swipe = SwipeGesture::new()
    .on_swipe(|direction, _window, _cx| {
        match direction {
            SwipeDirection::Left => { /* swipe left */ },
            SwipeDirection::Right => { /* swipe right */ },
            SwipeDirection::Up => { /* swipe up */ },
            SwipeDirection::Down => { /* swipe down */ },
        }
    });
}

Pinch gesture

Zoom/scale with pinch-to-zoom or Ctrl+scroll:

#![allow(unused)]
fn main() {
use kael::gesture::PinchGesture;

let pinch = PinchGesture::new()
    .on_pinch(|scale, center, _window, _cx| {
        // scale: f64 (1.0 = no change, >1 = zoom in, <1 = zoom out)
        // center: Point<Pixels> (pinch center point)
    });
}

Drag and drop

File drop (from OS)

#![allow(unused)]
fn main() {
div()
    .id("drop-zone")
    .on_file_drop(|event, _window, _cx| {
        match event {
            FileDropEvent::Entered(paths) => { /* files hovering */ },
            FileDropEvent::Submit(paths) => { /* files dropped */ },
            FileDropEvent::Exited => { /* drag cancelled */ },
            _ => {}
        }
    })
}

Sortable reordering

See SortableList for drag-to-reorder within lists.

Scroll events

#![allow(unused)]
fn main() {
div()
    .id("canvas")
    .on_scroll_wheel(|event, _window, _cx| {
        // event.delta: ScrollDelta (Pixels or Lines)
        // event.modifiers: Modifiers (detect Ctrl for zoom)
    })
}

Actions & Keybindings

Kael separates what happens (an action) from how it’s triggered (a keybinding or click). Actions are dispatched up the focused element tree, so a keystroke is routed to the nearest handler in the currently focused context — the same model that powers editor-grade keyboard UX.

Defining actions

The actions! macro generates zero-field action types in a namespace:

#![allow(unused)]
fn main() {
use kael::actions;

actions!(editor, [Save, Undo, Redo, Tab, TabPrev]);
}

Each entry becomes a type (Save, Undo, …) implementing the Action trait, with a stable name like editor::Save used for keymaps and dispatch.

Binding keys

Register bindings once at startup with cx.bind_keys. KeyBinding::new takes the keystroke string, the action, and an optional key context that scopes the binding:

#![allow(unused)]
fn main() {
use kael::{Application, App, KeyBinding};

Application::new().run(|cx: &mut App| {
    cx.bind_keys([
        KeyBinding::new("cmd-s", Save, None),
        KeyBinding::new("cmd-z", Undo, None),
        KeyBinding::new("cmd-shift-z", Redo, None),
        KeyBinding::new("tab", Tab, Some("Editor")),       // only in the "Editor" context
        KeyBinding::new("shift-tab", TabPrev, Some("Editor")),
    ]);
    // ... open windows ...
});
}

Keystroke syntax uses cmd / ctrl / alt / shift modifiers joined with -, and a space separates multi-key sequences (e.g. "cmd-k cmd-s"). Use cmd on macOS and ctrl on Windows/Linux.

Handling actions

In render, mark the element that owns a focus context with track_focus, then register handlers with on_action(cx.listener(...)). Handlers take &mut self, a reference to the action, the window, and the context:

#![allow(unused)]
fn main() {
use kael::{div, prelude::*, Context, FocusHandle, Render, Window};

struct Editor { focus_handle: FocusHandle }

impl Editor {
    fn on_save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
        // ... persist ...
        cx.notify();
    }
}

impl Render for Editor {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        div()
            .track_focus(&self.focus_handle)
            .on_action(cx.listener(Self::on_save))
            .on_action(cx.listener(Self::on_undo))
            .child("editor surface")
    }
}
}

Focus & tab order

Create focus handles from the context and arrange tab order with tab_index / tab_stop. Move focus from the window:

#![allow(unused)]
fn main() {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
    let items = vec![
        cx.focus_handle().tab_index(1).tab_stop(true),
        cx.focus_handle().tab_index(2).tab_stop(true),
        cx.focus_handle().tab_index(3).tab_stop(true),
    ];
    let focus_handle = cx.focus_handle();
    window.focus(&focus_handle);
    Self { focus_handle, items }
}
}

Window focus methods: window.focus(&handle), window.focus_next() (Tab), and window.focus_prev() (Shift-Tab). Query state with handle.is_focused(window) and style focused elements with .focus(|s| s.border_color(...)).

See examples/tab_stop.rs for a complete focus-navigation demo, and crates/kael/docs/key_dispatch.md for the dispatch internals.

Plugins & Extensions

Kael ships an extension system with a contribution-point architecture. Extensions run out-of-process and can target one of two execution models: a sandboxed WASM module, or an external process that speaks the extension RPC protocol. The host loads extensions from a manifest, mediates their capabilities through a permission broker, and dispatches commands and notifications to them.

Defining a manifest

Build a PluginManifest with the builder. The positional arguments are id, name, version, api_version, entry_point, and execution_model; contribution points and capabilities are added fluently.

#![allow(unused)]
fn main() {
use kael::{ContributedCommand, ExecutionModel, PluginManifest};

let manifest = PluginManifest::builder(
    "com.example.mock",   // id
    "Mock Plugin",        // display name
    "1.0.0",              // plugin version
    "1.0.0",              // host API version it targets
    "mock.wasm",          // entry point
    ExecutionModel::Wasm,
)
.command(ContributedCommand {
    id: "mock.hello".to_string(),
    title: "Say Hello".to_string(),
    keybinding: None,
})
.build()?;
}

Manifests can also be loaded from disk with PluginManifest::from_json, PluginManifest::from_toml, or PluginManifest::load(path).

Loading and activating

ExtensionHostRuntime owns the installed extensions for an app. Load a manifest, then activate it — activate_with_broker runs the extension’s capability requests through a PermissionBroker first.

#![allow(unused)]
fn main() {
use kael::{ExtensionHostRuntime, PermissionBroker};

let mut runtime = ExtensionHostRuntime::new(&extensions_dir, "my-app");
runtime.load(manifest)?;

let broker = PermissionBroker::new();
for ext in runtime.all().iter().map(|e| e.manifest.id.clone()).collect::<Vec<_>>() {
    runtime.activate_with_broker(&ext, &broker)?;
}
}

Other runtime operations: load_from_directory (dev mode), install_from_path, uninstall, activate / deactivate, unload, send_command, broadcast_notification, and all (returns &ExtensionInfo with manifest, is_active, process_id, load_path, dev_mode).

Contribution points

Extensions extend the host by contributing entries through the builder:

Builder methodContributes
.command(ContributedCommand)A command (id, title, optional keybinding)
.menu_item(ContributedMenuItem)A menu entry (target_menu, label, command_id)
.panel(ContributedPanel)A panel/view (id, title, default_position)
.settings_schema(json)A JSON schema for configurable settings
.capability(Capability)A requested capability (e.g. Capability::Notification)

Panels position with PanelPosition::{Left, Right, Bottom, Floating}.

Execution models & RPC

ExecutionModel::Wasm runs the entry point as a sandboxed WebAssembly module. ExecutionModel::ExternalProcess launches a separate binary that connects over the platform transport and exchanges messages with the host.

The host and an external extension first complete a handshake (ExtensionHandshake, version-checked against EXTENSION_RPC_VERSION), then exchange a typed envelope:

DirectionTypeKey variants
host → extExtensionRequestActivate, Deactivate, Shutdown, GetContributions, ExecuteCommand { command_id, args }
ext → hostExtensionResponseAck, Contributions(Contributions)
host → extExtensionNotificationSettingsChanged { key, value }

Broadcast a notification to every active extension:

#![allow(unused)]
fn main() {
use kael::ExtensionNotification;

runtime.broadcast_notification(ExtensionNotification::SettingsChanged {
    key: "theme".to_string(),
    value: serde_json::json!("dark"),
});
}

See examples/plugin_host.rs for a complete host UI that loads both a WASM and an external-process extension, activates them through a broker, and streams a live log.

Multi-Process & IPC

Kael supports an Electron-style multi-process architecture: the UI runs in the main process while heavy or untrusted work runs in supervised child processes that communicate over typed IPC. Transport is platform-native — Unix domain sockets on macOS/Linux, named pipes on Windows — and the framework handles framing, request/response correlation, progress streaming, and crash reporting for you.

Process model

Every process has a class describing its role:

#![allow(unused)]
fn main() {
use kael::ProcessClass;

ProcessClass::Ui;        // the main UI process
ProcessClass::Worker;    // background compute
ProcessClass::Media;     // media decode/playback
ProcessClass::Extension; // sandboxed plugins (see Plugins & Extensions)
}

A child process is described by a ProcessInfo, built fluently:

#![allow(unused)]
fn main() {
use kael::{ProcessId, ProcessInfo};

let info = ProcessInfo::worker(ProcessId(0), "thumbnailer")
    .executable("/path/to/worker-binary")
    .arg("--quiet")
    .env("RUST_LOG", "warn");
}

Constructors exist for each role: ProcessInfo::worker, ProcessInfo::media, and ProcessInfo::extension.

Spawning a worker (host side)

WorkerHost owns the socket directory and supervises spawned children. request sends a typed payload and blocks for the response; fire_and_forget sends without waiting; health_check pings the child.

#![allow(unused)]
fn main() {
use kael::{ProcessClass, ProcessId, ProcessInfo, WorkerHost};

let mut host = WorkerHost::with_temp_dir();
let info = ProcessInfo::worker(ProcessId(0), "thumbnailer")
    .executable(worker_binary_path);

let worker = host.spawn_worker(ProcessClass::Worker, info)?;

worker.health_check()?; // round-trip ping

let response: serde_json::Value = worker.request(serde_json::json!({
    "op": "echo",
    "message": "hello from host",
}))?;
assert_eq!(response["message"], "hello from host");
}

The worker child

The child binary connects back to the host with WorkerClient::connect_from_env (it reads the GPUI_WORKER_SOCKET / GPUI_WORKER_PIPE environment variable the host sets) and serves requests with run. The handler receives a WorkerRequest and a progress callback for streaming intermediate updates, and returns a WorkerResponse or WorkerError.

use anyhow::Result;
use kael::{WorkerClient, WorkerProgress, WorkerRequest, WorkerResponse};

fn main() -> Result<()> {
    let client = WorkerClient::connect_from_env()?;
    client.run(|request, progress| match request {
        WorkerRequest::Ping => Ok(WorkerResponse::Pong),
        WorkerRequest::Execute { payload } => {
            progress(WorkerProgress::Update(serde_json::json!({ "step": 1 })));
            // ... do work ...
            Ok(WorkerResponse::Result(payload))
        }
    })
}

The message types:

TypeVariants
WorkerRequestPing, Execute { payload: serde_json::Value }
WorkerResponsePong, Result(serde_json::Value)
WorkerProgressUpdate(serde_json::Value)
WorkerErrorExecution(String), Cancelled

Supervision & crash handling

Register an event callback to observe lifecycle events. A child that crashes is reported as a SupervisorEvent::Exited rather than taking down the host:

#![allow(unused)]
fn main() {
use kael::SupervisorEvent;

host.on_event(|event| match event {
    SupervisorEvent::Exited { id, .. } => eprintln!("worker {id:?} exited"),
    _ => {}
});
}

Each WorkerHandle exposes id() to correlate it with supervisor events.

Extension processes

Extension children use the same transport but a richer RPC envelope (handshake, contribution discovery, command dispatch). They are managed by ExtensionHostRuntime rather than WorkerHost — see Plugins & Extensions.

Security & Permissions

Kael provides a capability-based security model for controlling what extensions and child processes can access.

Permission system

#![allow(unused)]
fn main() {
use kael::security::*;

let mut manager = PermissionManager::new();

// Request permission
let request = PermissionRequest::new(
    PermissionKind::FileSystem,
    "Read project files",
);

match manager.check(&request) {
    PermissionStatus::Granted => { /* proceed */ },
    PermissionStatus::Denied => { /* blocked */ },
    PermissionStatus::Prompt => { /* ask user */ },
}
}

Network policy

Control outbound network access:

#![allow(unused)]
fn main() {
let policy = NetworkPolicy {
    allowed_hosts: vec!["api.myapp.com".into()],
    blocked_hosts: vec![],
    allow_localhost: true,
};
}

Process capabilities

Limit what child processes can do:

#![allow(unused)]
fn main() {
let limits = ProcessLimits {
    max_memory_mb: 512,
    max_cpu_percent: 50,
    max_open_files: 256,
};

let capabilities = vec![
    ProcessCapability::FileRead,
    ProcessCapability::Network,
];
}

Credential storage

Secure credential management via OS keychain:

#![allow(unused)]
fn main() {
let keychain = KeychainStore::new("my-app");
keychain.write("api-token", "secret-value")?;
let token = keychain.read("api-token")?;
keychain.delete("api-token")?;
}

For LLMs

This page explains how to use Kael’s LLM integration features.

llms.txt

Kael provides an llms.txt file at the site root following the llms.txt standard. This file contains a structured overview of the entire Kael API — widget primitives, layout system, platform APIs, and code patterns — optimized for LLM consumption.

Use it when:

  • Pasting into ChatGPT, Claude, or other LLMs as context for building Kael apps
  • Integrating with AI coding assistants that support llms.txt
  • Building MCP servers or tool definitions that reference Kael

Copy for LLM button

Every page on this site has a “Copy for LLM” button in the bottom-right corner. Click it to copy the page content as clean markdown, ready to paste into any LLM conversation.

https://augani.github.io/kael/llms.txt