Quick-start for AI coding agents working with SLT.
- SLT is immediate mode: the closure is the app.
- Application state usually lives in normal Rust variables or structs.
- Widget-local persistent state uses
use_state()/use_memo(). - Interactive widgets usually return
Response. - Layout is mostly
row(),col(),container(),scrollable().
README.mddocs/QUICK_START.mddocs/WIDGETS.mddocs/PATTERNS.mddocs/EXAMPLES.mddocs/ARCHITECTURE.mdfor internals
text().bold() chains after the last text command. styled() applies a pre-built Style in one call. Use styled() when reusing styles across widgets.
ui.bg() modifies the LAST TEXT element, not the container. For container background, use container().bg(color).col(...).
Animation types (Tween, Spring, Keyframes, Sequence, Stagger) are standalone structs, not Context methods. Compute values with tick() and pass them to style/layout methods. See docs/ANIMATION.md.
Use palette::tailwind::{BLUE, RED, ...}.c500 for Tailwind-style colors. See docs/THEMING.md.
Both are horizontal. row() is a full layout container returning Response. line() is for inline rich text returning &mut Self with zero gap between children.
Pass WidgetColors::new().fg(color).bg(color) as the last argument. See docs/THEMING.md.
use_state() and use_memo() must be called in the same order every frame. Never put them inside conditionals.
Exception (v0.19.0): ui.use_state_named(id) is the id-keyed variant and IS safe inside if/match branches because it keys by the supplied &'static str instead of call order. Reach for it when you genuinely need a hook in a conditional path.
// Wrong: order-based hook inside a conditional drifts the call order.
if expanded {
let count = ui.use_state(|| 0); // BAD — frame N has it, frame N+1 may not.
}
// Right: id-keyed variant is safe inside conditionals.
if expanded {
let count = ui.use_state_named::<i32>("sidebar.count"); // OK
}The original use_state / use_memo order rule still holds — only the id-keyed *_named variants opt out of it.
Use key(c), key_code(code), or key_mod(c, mods). For modal-aware shortcuts use the regular versions. For global shortcuts that bypass modals, use raw_key_code() or raw_key_mod(). For multi-key chords that span frames (vi gg, leader keys like <space>ff) use key_chord("gg") — it buffers partial input across frames and times out on inactivity. (key_seq is a deprecated alias that now delegates to key_chord.)
Implement the Widget trait with type Response and fn ui(&mut self, ctx: &mut Context) -> Self::Response. Use register_focusable() for keyboard support and interaction() for click/hover.
Yes, for custom backends with Backend, AppState, and frame().
Only if it helps. SLT does not require one.
No. Built-in interactive widgets usually do. Custom widgets can return (), bool, or Response.
Because layout feedback often uses previous-frame data in immediate-mode UI. For the frame timeline and prev-frame rect rules, see Previous Frame Guide.
That is the immediate-mode contract: stream.push(delta) then the whole
closure runs again. Use ui.container().cached(version_key, f) to wrap the
static chrome (chat history, sidebar, status bar) keyed off a value you
already own — e.g. a hash of the non-streaming inputs, or
StreamingTextState::version() of the other panes. Leave the stream itself
uncached; it changes every token. Note the current semantics are honest: f
still runs every frame (output is byte-identical to .col(f)), and cached
records a hit/miss stability signal (Context::region_cache_hits() /
region_cache_misses()) rather than skipping the body — this is an
author-controlled cache, not reactive binding. See
Performance — Pattern 7 for the full rationale and the
Phase-0 streaming benchmark.
- Prefer
ui.container().p(1).col(...)over inventing new layout patterns. - Prefer existing
*Statetypes over custom ad-hoc input structs. - Use
Response.changed/.clickedinstead of inventing parallel booleans. - Use
register_focusable()andinteraction()in custom widgets. - Respect hook ordering rules.
- Animation types are standalone structs, not Context methods — compute values separately.
- Use
palette::tailwindcolors instead of hardcoding RGB values. - Check
docs/FEATURES.mdbefore using feature-gated APIs.
When a value is read by many nested render functions (theme, current tick, current user, toast bus), do not thread &theme, &tick, &mut toasts parameters through every signature. Use ui.provide(value, |ui| ...) once near the root, then have nested code read it back with ui.use_context::<T>() (panics if missing) or ui.try_use_context::<T>() (returns Option<&T>). Added in v0.19.0.
// `provide` boxes the value as `dyn Any`, so the type must satisfy `T: 'static`.
// `Theme` is `Copy`, so deref-copy from `ui.theme()`. Use `&'static str` for
// string literals; switch to `String` if the value comes from runtime input.
struct AppCtx {
theme: slt::Theme,
tick: u64,
user: &'static str,
}
slt::run(|ui: &mut slt::Context| {
let ctx = AppCtx { theme: *ui.theme(), tick: ui.tick(), user: "subin" };
ui.provide(ctx, |ui| {
render_header(ui);
render_card(ui);
});
});
fn render_card(ui: &mut slt::Context) {
let ctx = ui.use_context::<AppCtx>();
ui.text(format!("hi {} (tick {})", ctx.user, ctx.tick));
}Reserve explicit parameters for writes (e.g. &mut MyDocState) — those should still be passed in, not read out of the context bag. See PATTERNS.md for the full pattern.
with_if(cond, modifier) and with(modifier) (v0.19.0) let you fold conditional styling into a single fluent chain on text and ContainerBuilder. Replace if cond { t.bold(); t.fg(Color::Red); } style branching with ui.text(...).with_if(is_error, |t| t.bold().fg(Color::Red)). Cleaner diffs, no broken chains.
When editing SLT built-in widgets inside src/context/*, prefer the internal interaction helpers instead of hand-rolling event scans again.
These are internal pub(crate) helpers, not stable public API for external widgets.
- Use
begin_widget_interaction(focused)for marker +Responsesetup. - Use
available_key_presses()/available_pastes()to iterate unconsumed inputs. - Use
left_clicks_for_interaction()ormouse_events_in_rect()for hit-tested mouse handling. - Finish by calling
consume_indices(...)instead of mutatingself.consumed[...]across the widget body.
Public custom widgets should still use the stable public surface (register_focusable(), interaction(), Response).
When an agent changes a public API, it should update:
- crate rustdoc in
src/lib.rsor the defining file - the most relevant guide in
docs/ - example coverage if the new API needs a runnable reference
If the behavior is hard to explain cleanly, the API may still need refinement.