# RavenRS > RavenRS is an open-source Rust GUI framework: one declarative, reactive UI for Windows, macOS, Linux and WebAssembly, with GPU rendering, semantic design tokens, records, forms and reporting. Framework version: 27.1.27. License: MIT OR Apache-2.0. Start with `npx create-ravenrs minha-loja`. The npm preparer, Rust CLI and framework share this release version and use a pinned Git tag. For exact APIs, check the project's Cargo.toml and use its version-matched RavenRS MCP server (`cargo ravenrs mcp`). Do not infer APIs from other GUI frameworks. ## Official guides - [Introduction](https://alexkads.github.io/RavenRS/docs/index.md) - [Installation](https://alexkads.github.io/RavenRS/docs/getting-started/index.md) - [CLI & AI assistants](https://alexkads.github.io/RavenRS/docs/cli/index.md) - [AI-assisted development](https://alexkads.github.io/RavenRS/docs/ai/index.md) - [Architecture](https://alexkads.github.io/RavenRS/docs/architecture/index.md) - [Design system](https://alexkads.github.io/RavenRS/docs/design-system/index.md) - [Records & forms](https://alexkads.github.io/RavenRS/docs/applications/index.md) - [Component coverage](https://alexkads.github.io/RavenRS/docs/components/index.md) - [Create reports](https://alexkads.github.io/RavenRS/docs/reporting/index.md) - [Report server](https://alexkads.github.io/RavenRS/docs/report-server/index.md) - [Quality & accessibility](https://alexkads.github.io/RavenRS/docs/quality/index.md) - [Performance](https://alexkads.github.io/RavenRS/docs/performance/index.md) - [Versions & upgrades](https://alexkads.github.io/RavenRS/docs/versioning/index.md) ## Source and tools - [Repository](https://github.com/alexkads/RavenRS) - [Published Rust API](https://docs.rs/ravenrs/27.1.27/ravenrs/) - [npm preparer](https://www.npmjs.com/package/create-ravenrs) - [Combined guides](https://alexkads.github.io/RavenRS/llms-full.txt) These files provide readable context; they do not guarantee AI indexing or training. --- # Introduction URL: https://alexkads.github.io/RavenRS/docs/ Source: https://github.com/alexkads/RavenRS/blob/master/docs/README.md # RavenRS documentation Build native and browser applications with one declarative UI in Rust. RavenRS draws on the GPU; views share the same components, reactive state and design tokens on macOS, Windows, Linux and the web. ## Start with an application ```bash npx create-ravenrs minha-loja ``` The guided setup prepares Rust, generates your project and configures coding assistants with the API, skill and instructions from the same RavenRS release. [Create your first project](GETTING_STARTED.md) to choose a template, run your app and build for the web. Already have a project? [Configure the CLI and AI tools](../crates/cargo-ravenrs/README.md). ## Follow a path - **Getting started:** install the tools, understand the project and make a window. - **Build applications:** compose screens, use design tokens and generate forms from your records. - **Reports:** design, render, export and serve reports. - **Reference:** inspect component APIs, quality requirements and release compatibility. Use the navigation on the left to explore a topic. Search with `Ctrl+K` or `⌘K`. Each page includes an index of its sections, copyable examples and a link to its Markdown source. ## One source for every platform A `View` is a typed Rust builder. `State` drives updates, the layout engine places controls, and the renderer draws the result. The same UI opens in a native window or on a browser canvas. You can start with high-level components and implement a custom view when you need more control. [Explore the architecture](../ARCHITECTURE.md), [browse the components](PARITY.md), or [build a record application](APP-FRAMEWORK.md). ## Build with an AI assistant Read the [AI development guide](AI_DEVELOPMENT.md) for official context, version-matched MCP tools and machine-readable documentation. Start with the project skill and verify APIs against its release. Site maintainers: see [SEO and publication](SITE_SEO.md) for canonical URLs, social cards, sitemaps and local validation. --- # Installation URL: https://alexkads.github.io/RavenRS/docs/getting-started/ Source: https://github.com/alexkads/RavenRS/blob/master/docs/GETTING_STARTED.md # Getting started with RavenRS This guide builds a small app step by step: a window, then the same app in the browser, then state, your own components, themes and the data grid. ## Create your project With Node.js 20 or newer, start with one command: ```bash npx create-ravenrs minha-loja ``` The guided setup prepares Rust and the matching RavenRS CLI, then asks for a template and whether to build for desktop, the browser or both. It creates application code, a screen test, Git and the files your coding assistant needs. You can also use `npm create ravenrs@latest minha-loja`. For an unattended setup, choose the model and target explicitly: ```bash npx create-ravenrs minha-loja --yes --template app --target both cd minha-loja cargo run cargo test cargo ravenrs doctor ``` Without `--yes`, installing Rust, a missing toolchain component or the CLI requires your confirmation. Existing matching installations are reused. Repeating the command preserves application code. The first CLI installation compiles Rust sources locally and takes a few minutes. The setup never runs `sudo`: it prints the Linux packages or Windows MSVC Build Tools you need to install yourself. Windows requires the C++ workload and Windows SDK even when the application targets the browser, because the generator itself is a native program. Already have Rust? Install the CLI directly. Use the release tag to keep the CLI, MCP, examples, skill and framework crates on the same version: ```bash cargo install --git https://github.com/alexkads/RavenRS --tag v27.1.27 --locked cargo-ravenrs cargo ravenrs new minha-loja --template hello --target both --yes ``` ## Choose a template and target | Template | Starting point | Storage | | --- | --- | --- | | `hello` | A card, a counter and buttons | Reactive state | | `app` | A task list with navigation, forms and commands from `ravenrs-app` | In memory | | `app-sqlite` | The same task application with persistence | SQLite on desktop; IndexedDB in the browser | | `relatorios` | The RavenRS report designer | Report layouts | Use `--target desktop`, `--target web` or `--target both`. The default with `--yes` is `hello` for `desktop`. The SQLite model requires Rust 1.89 or newer; the bootstrap installs the stable toolchain for you. Templates and the skill are embedded in the CLI from its release; they are not fetched from the changing `master` branch. | Flag | Effect | | --- | --- | | `--yes` | Accept prerequisite installations and use defaults for unanswered choices | | `--no-git` | Skip `git init` | | `--no-ai` | Skip assistant configuration | | `--no-check` | Skip the final application compilation; keep the MCP handshake | | `--plugin` | Use the version-pinned Claude marketplace plugin instead of a copied local skill | ## Your project structure ```text minha-loja/ ├── Cargo.toml # published crate versions, no checkout paths ├── rust-toolchain.toml ├── src/ │ ├── main.rs # native window │ └── lib.rs # app() and the shared UI ├── tests/screen.rs # main-screen render test ├── web/index.html # when web or both is selected ├── AGENTS.md # commands and project rules └── CLAUDE.md # points to AGENTS.md ``` Start changing `app()` in `src/lib.rs`. The desktop entry point opens that view in a native window. A web target includes its browser entry point and HTML canvas. Run `cargo test` to check that the main screen still renders. ## Work with a coding assistant The generator registers the same offline MCP server for Claude Code (`.mcp.json`), VS Code / Copilot (`.vscode/mcp.json`), Cursor (`.cursor/mcp.json`) and Codex (`.codex/config.toml`). Every configuration launches `cargo ravenrs mcp`. Open the folder with `claude`, `code .` or your preferred assistant. The project includes the version-matched RavenRS skill and `AGENTS.md` commands. The instructions tell the assistant to consult `component_api` and `component_examples` before inventing a method, use design tokens and existing components, and build record applications with `ravenrs-app`. Claude settings allow the project's Cargo commands and read-only RavenRS MCP tools. Your editor may still require its normal workspace trust approval. For a project that already exists, run: ```bash cargo ravenrs ai cargo ravenrs doctor ``` After updating framework versions, install the matching CLI tag and run `ai` again. It updates marked blocks and RavenRS configuration entries, preserving custom instructions outside those blocks. Keep personal notes outside the generated markers. It does not migrate application source. Use `cargo ravenrs ai --plugin` for the marketplace mode. `doctor` checks explicit dependency versions, generated AI files and real MCP handshakes. It exits with an error when a version or configuration is stale. With the npm entry point, `npx create-ravenrs doctor` also checks prerequisites and never installs anything. Workspace-inherited dependency versions are tracked separately in [issue #209](https://github.com/alexkads/RavenRS/issues/209). ## Run in the browser Choose `web` or `both` when creating the app. In the generated project: ```bash cargo check --target wasm32-unknown-unknown cargo build --release --lib --target wasm32-unknown-unknown cargo install wasm-bindgen-cli --version 0.2.130 --locked wasm-bindgen --target web --out-dir web/pkg target/wasm32-unknown-unknown/release/minha_loja.wasm python3 -m http.server -d web 8080 ``` Open `http://localhost:8080`. Replace `minha_loja` with your package name, using underscores for hyphens. `wasm-bindgen-cli` must match the framework's pinned `wasm-bindgen` version. Serve the page over HTTP rather than opening the HTML file directly. ## Understand the pieces The following walkthrough shows how to build these pieces yourself: a window, browser entry point, state, components, themes and a data grid. For the CLI's behavior and validation details, see [cargo-ravenrs](../crates/cargo-ravenrs/README.md) and [create-ravenrs](../npm/create-ravenrs/README.md). ## 1. A window ```bash cargo new hello-raven && cd hello-raven ``` ```bash cargo add ravenrs ``` ```rust // src/main.rs use ravenrs::prelude::*; fn app() -> impl View { VStack::new(( Text::new("Hello, RavenRS").style(TextStyle::Display), Text::new("Drawn on the GPU.").foreground(Semantic::InkMuted), )) .padding(32.0) } fn main() -> Result<(), ravenrs::native::PlatformError> { ravenrs::native::run(app(), ravenrs::native::WindowOptions::new("Hello").size(800.0, 500.0)) } ``` ```bash cargo run ``` ## 2. The same app in the browser Put the UI in `src/lib.rs` so both targets share it, and add a web entry point: ```toml [lib] crate-type = ["cdylib", "rlib"] [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "=0.2.130" wasm-bindgen-futures = "0.4" ``` ```rust // src/lib.rs use ravenrs::prelude::*; pub fn app() -> impl View { Text::new("Hello from the browser").style(TextStyle::Display).padding(32.0) } #[cfg(target_arch = "wasm32")] #[wasm_bindgen::prelude::wasm_bindgen(start)] pub fn start() { wasm_bindgen_futures::spawn_local(async { // Keep the returned app alive for as long as the page shows it. if let Ok(app) = ravenrs::web::start(app(), ravenrs::web::WebOptions::new("raven-canvas")).await { std::mem::forget(app); } }); } ``` A page with a `` sized by CSS hosts it; [`examples/hello-world/web/index.html`](../examples/hello-world/web/index.html) is a complete one, and [`scripts/build-web.sh`](../scripts/build-web.sh) shows the build (`cargo build --target wasm32-unknown-unknown` followed by `wasm-bindgen --target web`). ## 3. State `State` holds a value. Views that read it rebuild when it changes; nothing else does. ```rust let count = State::new(0); let inc = count.clone(); VStack::new(( Text::bound(&count, |n| format!("{n} clicks")), Button::new("Click").on_click(move || inc.update(|n| *n += 1)), )) ``` Controls take a `Binding`, a read/write view of a state: `Toggle::new(enabled.binding())`, `TextField::new(name.binding())`, `Slider::new(volume.binding())`. `Binding::map` focuses on one field of a struct, and `Derived` caches a value computed from other states. ## 4. Your own components A `Component` keeps its state in fields and composes views in `body`: ```rust struct Counter { count: State } impl Component for Counter { fn body(&self) -> impl View { let c = self.count.clone(); Button::new("Add").on_click(move || c.update(|n| *n += 1)) } } ``` For full control, implement `View` and use `BuildCx`. This badge draws itself, reacts to hover and handles clicks: ```rust struct Pill(&'static str); impl View for Pill { fn build(&self, cx: &mut BuildCx<'_>) -> NodeId { let theme = *cx.theme(); let fill = if cx.is_hovered() { theme.color(Semantic::Accent) } else { theme.color(Semantic::Control) }; let label = cx.child(&Text::new(self.0)); let node = cx.container( ravenrs::layout::LayoutStyle { padding: (6.0, 12.0).into(), ..Default::default() }, &[label], Paint::Quad(QuadPaint { corner_radius: 999.0, ..QuadPaint::new(fill) }), ); cx.on_click(node, std::rc::Rc::new(|| println!("clicked"))); cx.cursor(node, CursorIcon::Pointer); node } } ``` `cx.use_state` gives a view instance its own state (open/closed, scroll position), `cx.overlay` opens a floating layer, `cx.wake_at` schedules a rebuild (animations, timers) and `cx.shortcut` registers app-wide keys. ## 5. Themes Pick a theme when you start the app, or switch at runtime: ```rust ravenrs::native::run(app(), ravenrs::native::WindowOptions::new("App").theme(Theme::raven_light())) ``` `ThemeSwitcher::new()` toggles Raven Dark, Light and Glass, or the app's own three with `.themes(dark, light, glass)`, `ThemePicker` does it from a small menu, and `ThemeEditor` changes the tokens of the theme on screen and hands them over as JSON (`Theme::to_tokens_json`, `Theme::from_tokens_json`); any component can do the same with `cx.theme_state().set(...)`. Components use semantic roles (`Semantic::Ink`, `Semantic::Accent`, `Semantic::SurfaceElevated`…) so they follow the theme automatically. The tokens are documented in [docs/design-system](design-system/README.md). ## 6. The data grid ```rust use ravenrs::widgets::grid::Value; let data = GridData::new(vec![ vec![Value::from("Northwind"), Value::from("Brazil"), Value::from(240)], vec![Value::from("Aurora"), Value::from("Chile"), Value::from(86)], ]); DataGrid::new(data, [ GridColumn::text("Customer").editable(), GridColumn::text("Country"), GridColumn::number("Seats").summary(Summary::Sum), ]) .selection_mode(SelectionMode::Multiple) .height(480.0) ``` Sorting, the filter row (`>100`, `10..20`, `yes`, or an expression such as `[Seats] > 10 And [Country] = 'Brazil'`), search, grouping, resizing, the column chooser, editing and CSV export work out of the box; only the visible rows are built, so 100,000+ rows stay smooth. ## 7. Quality checks `ravenrs-lint` checks what the compiler cannot: colors from `Semantic` roles, text from `TextStyle`, the 4px grid, named icon buttons, one primary action, and no state or blocking work in `body`. Set it up once and it fails the build of anyone, person or coding agent, who breaks a rule: ```bash cargo install ravenrs-lint --locked cargo ravenrs-lint --init --language en # or pt-BR cargo ravenrs-lint --gate ``` `--init` turns on the strictest rustc and Clippy levels, adds a test so `cargo test` runs the rules, and writes the same instructions for Codex, Claude, Gemini and Copilot. See [its README](../crates/ravenrs-lint/README.md) for every rule. ## Next - Browse every component in the [live gallery](https://alexkads.github.io/RavenRS/) and its [source](../examples/component-gallery/src/lib.rs). - Install [Raven Finanças](../examples/family-finance), a complete family finance app built with RavenRS, to see it all in everyday use. - Read the [architecture](../ARCHITECTURE.md) to see how build, layout, paint and render fit together. - The [API documentation](https://alexkads.github.io/RavenRS/api/ravenrs/) lists every type and method. ## Official context for AI assistants Read the [AI development guide](AI_DEVELOPMENT.md) for the RavenRS proposal, official Markdown sources, version-matched MCP tools and a starting prompt. The site publishes `llms.txt` and `llms-full.txt` from these same guides so assistants can consult authoritative context. ## Interactive installation ![Interactive RavenRS project creation](media/create-ravenrs-cli.png) The npm preparer asks for your target and template before starting, with arrow-key selection and descriptions. During installation, animated tasks show elapsed time and current build activity. Full Cargo/Git output is saved to a temporary installation log; a failing task prints its last output and the log path. Use `npx create-ravenrs minha-loja --verbose` to stream all output. Escape cancels a menu; Ctrl+C cancels an active task. `NO_COLOR` disables colors. Redirected output and simple terminals show plain logs; narrow terminals use text prompts. `--yes` still authorizes prerequisite installation and selects defaults for unanswered choices. ## Repeating creation in an existing folder Repeating creation preserves application source only when the original template and target match. A desktop project cannot be turned into a web project, or a hello project into an application, by reusing its folder in the creator. Choose another folder for a different template or target. The creator reports incompatible choices before installing prerequisites or changing files. --- # CLI & AI assistants URL: https://alexkads.github.io/RavenRS/docs/cli/ Source: https://github.com/alexkads/RavenRS/blob/master/crates/cargo-ravenrs/README.md # cargo-ravenrs Create a RavenRS app and give coding assistants the API, examples, design tokens and instructions for the exact same release, without a separate MCP installation. ```sh cargo install --git https://github.com/alexkads/RavenRS --tag v27.1.27 --locked cargo-ravenrs cargo ravenrs new minha-loja --template app --target both --yes cd minha-loja cargo run cargo test cargo ravenrs doctor ``` For a released version, add `--tag vYY.R.P` to `cargo install`. This crate embeds workspace sources and is installed from Git, rather than crates.io. From a checkout use `cargo install --path crates/cargo-ravenrs --locked`. `new` asks for the template and target in a terminal. With `--yes` or redirected input, it defaults to `hello` and `desktop`. Templates are `hello`, `app`, `app-sqlite`, and `relatorios`; targets are `desktop`, `web`, and `both`. SQLite is used on the desktop; the SQLite template uses IndexedDB in the browser. Source files are embedded from `examples/hello-world`, `tutorial-tasks`, `tasks-sqlite`, and `report-designer`. Flags: `--no-git`, `--no-ai`, `--no-check`, and `--plugin`. The last flag writes a version-pinned Claude marketplace registration instead of copying the local skill. A repeated `new` recognizes projects generated by this version and preserves their code; it refuses existing directories it does not own and rejects a different template or target before changing any file. It never migrates application source. `cargo ravenrs mcp` serves the existing RavenRS MCP implementation over stdio. `cargo ravenrs ai` configures an existing project. It merges only RavenRS entries in JSON files, preserving other keys, and updates marked blocks in Markdown and Codex TOML. JSON whitespace is normalized. Invalid files are rejected before any configuration is written. Keep personal instructions outside the managed blocks. The local skill is copied byte-for-byte from the versioned plugin, and its marked body can be updated while preserving notes outside the block. An unmarked existing skill is preserved and reported. `doctor` compares explicit RavenRS dependency versions with the CLI, checks managed files, and runs initialize + tools/list through all four registered commands. It reports stale files and versions with a nonzero exit code. Projects made with `--no-ai` are checked without requiring assistant files. Workspace-inherited dependency versions must be made explicit before using doctor on an existing project. Web projects include `web/index.html` and commands in `AGENTS.md` and `README.md` for building with the matching wasm-bindgen CLI and serving the result over HTTP. Validation: ```sh cargo test -p cargo-ravenrs -p ravenrs-mcp -p ravenrs-project cargo test -p cargo-ravenrs --test cli -- --ignored scripts/check.sh ``` The compilation matrix checks every template/target with local workspace patches, sharing a build cache. Set `RAVENRS_TEST_PUBLISHED=1` to check against crates.io after publishing the corresponding framework release. The regular tests start the real MCP binary from each configuration and verify idempotence, version checks, and the embedded sources. CLI, framework, templates and MCP use the same release version. The current Git tag is `v27.1.27`. --- # AI-assisted development URL: https://alexkads.github.io/RavenRS/docs/ai/ Source: https://github.com/alexkads/RavenRS/blob/master/docs/AI_DEVELOPMENT.md # AI-assisted development RavenRS is an open-source Rust GUI framework for native desktop applications and the web. Its proposal is **one declarative, reactive UI in Rust**, shared across Windows, macOS, Linux and WebAssembly, with GPU rendering, reusable components and semantic design tokens. It includes tools for business applications: records, forms, SQLite templates and reporting. The repository is licensed under MIT OR Apache-2.0. ## Start a project with the matching tools ```bash npx create-ravenrs minha-loja cd minha-loja cargo run ``` The Node.js preparer installs the pinned Rust CLI and generates a project. It can configure Claude Code, Cursor, VS Code / GitHub Copilot and Codex with a version-matched MCP server, project instructions and a RavenRS skill. Read the [installation guide](GETTING_STARTED.md) for prerequisites, consent prompts, templates and platform requirements. The npm wrapper, CLI and framework use the same release version. Its `release.json` pins the Rust CLI and Git tag; use that framework version when looking up APIs. Run `cargo ravenrs doctor` inside the project to check the setup. ## Give an assistant authoritative context Use these public sources when introducing RavenRS to an assistant: - [Documentation index](https://alexkads.github.io/RavenRS/llms.txt): a concise overview and links to official guides. - [Full documentation in Markdown](https://alexkads.github.io/RavenRS/llms-full.txt): the guides combined into one text file. - [Source repository](https://github.com/alexkads/RavenRS): implementations, examples, release tags and issue tracking. - [Versioned Rust API](https://docs.rs/ravenrs): reference for published releases; select the version used by the project. Every guide also has a **View Markdown** link and a **Copy page** button. The text endpoints are generated from the same repository documents as this site. They improve access to context; they do not guarantee indexing, inclusion in training data or recommendations by an AI. A useful starting prompt: ```text I am building a RavenRS application in Rust. Read the project's AGENTS.md and RavenRS skill first. Check Cargo.toml for the framework version and use the configured RavenRS MCP server to inspect APIs and examples before writing code. Use the existing design tokens and high-level application components where they fit. Do not invent builder methods or assume compatibility with another GUI framework. Explain any platform or feature limitations and validate the changes. ``` ## Inspect APIs instead of guessing The MCP server is started by `cargo ravenrs mcp`. It provides documentation and source lookup from the matching RavenRS release. Consult the [CLI reference](../crates/cargo-ravenrs/README.md) for configuration and the project's generated skill for the available tools and workflow. Before implementing a screen, an assistant should inspect the relevant component, its example and feature requirements. Prefer semantic design tokens to ad hoc values. For record-oriented screens, inspect [records and forms](APP-FRAMEWORK.md) before building all controls manually. Consult [architecture](../ARCHITECTURE.md) for reactive state, layout and rendering, and [reporting](REPORTING.md) for report-specific APIs. ## Keep context current After upgrading RavenRS, refresh managed assistant configuration with `cargo ravenrs ai` and run `cargo ravenrs doctor`. Review changes to generated instructions alongside the application. Custom instructions should describe your domain, conventions and validation; keep official API claims tied to the installed release. The supported platforms and installation limitations are documented in the [getting-started guide](GETTING_STARTED.md). For missing capabilities or incorrect guidance, [open an issue](https://github.com/alexkads/RavenRS/issues). --- # Architecture URL: https://alexkads.github.io/RavenRS/docs/architecture/ Source: https://github.com/alexkads/RavenRS/blob/master/ARCHITECTURE.md # RavenRS Architecture RavenRS is a declarative GUI framework written in Rust. One UI description runs in a native window (Windows, macOS, Linux) and in the browser (WebAssembly + WebGPU), and RavenRS draws every pixel itself with `wgpu`. This document records the decisions behind the first milestone, why they were made, and what is implemented, experimental or planned. ## 1. Goals and constraints | Principle | What it means in practice | |---|---| | Rust-first | All framework logic is Rust. No code from the original Raven GUI (C) is used. | | GPU-first | Rendering goes through `wgpu` and WGSL shaders on every platform. | | WebAssembly-first | Every crate that is not platform glue compiles for `wasm32-unknown-unknown`. No threads, no `SharedArrayBuffer`, no blocking. | | Canvas-first | On the web the UI is drawn into one ``. The DOM hosts the canvas and delivers input, nothing else. | | Declarative-first | Views are small values composed with builders; state changes trigger rebuilds of what depends on them. | | Beautiful by default | Components read semantic design tokens; nothing is styled by hand in app code unless the app wants to. | | Performance-first | Frames are produced only on demand; buffers and allocations are reused; there is no GPU→CPU readback in normal rendering. | ## 2. Ecosystem survey The survey was done against the official repositories and docs (October 2026). Nothing was copied from these projects; they informed design choices only. | Project | License | Rendering | State model | Layout | Text | Web | Lesson for RavenRS | |---|---|---|---|---|---|---|---| | [Raven](https://github.com/oscar-soirey/Raven) (C) | Apache-2.0 | Vulkan behind a function table, separate render thread | UI thread + job system | Two-pass measure/arrange | unverified | none | Keep the renderer behind a narrow interface. *This is the closest match to "the original Raven GUI"; the identification is probable, not confirmed.* | | [GPUI](https://github.com/zed-industries/zed/tree/main/crates/gpui) | Apache-2.0 | Metal; wgpu on Linux | Entities owned by the app, handles + contexts | Taffy | Platform text stacks | Experimental `gpui_web` | Context-passing ownership avoids `Rc` sprawl. | | [Xilem / Masonry](https://github.com/linebender/xilem) | Apache-2.0 | Vello on wgpu | View tree diffed into a retained widget tree | Masonry's own | Parley | DOM backend (`xilem_web`) | Separating the API layer from a retained widget core lets several API styles share it. | | [Iced](https://github.com/iced-rs/iced) | MIT | wgpu, tiny-skia fallback | The Elm Architecture | Own | cosmic-text (fork) | Yes, WebGL feature | Bundle a font for WASM; the browser gives you no system fonts. | | [Slint](https://github.com/slint-ui/slint) | GPL-3.0 / royalty-free / commercial | femtovg, Skia, software, optional wgpu | Compiled DSL with property bindings | Own + Taffy | Parley (optional) | Yes | Trim text features per target to keep WASM small. | | [Blinc](https://github.com/project-blinc/Blinc) | Apache-2.0 | wgpu, SDF primitives, glass | Signals + state machines | Flexbox builder | Own atlas | WebGPU preview | Closest prior art to RavenRS; validates the SDF + atlas approach. | | [Vello](https://github.com/linebender/vello) | Apache-2.0 OR MIT | Compute-shader 2D renderer; `vello_cpu`, `vello_gpu` variants | — | — | — | Needs WebGPU compute | Do not make compute-only rendering the only path. | | [Taffy](https://github.com/DioxusLabs/taffy) | MIT | — | — | Flexbox, Grid, Block | Measure callback | Yes | Wire text into layout through the measure function from day one. | | [wgpu](https://github.com/gfx-rs/wgpu) | MIT OR Apache-2.0 | Vulkan, Metal, DX12, GL, WebGPU | — | — | — | WebGPU and WebGL2 | Pin the major version; breaking releases arrive roughly quarterly. | | [Parley](https://github.com/linebender/parley) | Apache-2.0 OR MIT | — | — | — | HarfRust shaping, Skrifa outlines, ICU4X | Yes | Mature rich-text layout without platform dependencies. | ## 3. Crate graph ``` ravenrs-core ─────────────┬──────────────┬──────────────┬─────────────┐ (geometry, color, │ │ │ │ events, Scene) ▼ ▼ ▼ ▼ ravenrs-layout ravenrs-text ravenrs-style ravenrs-render (Taffy) (Parley) (tokens) (wgpu, swash) ravenrs-reactive ───┐ │ │ │ │ (State, Binding, │ └──────┬───────┴──────────────┘ │ Derived) └────► ravenrs-widgets │ (View, Component, BuildCx, Ui) │ │ │ ┌───────────────┴───────────────┐ │ ▼ ▼ │ ravenrs-platform-native ravenrs-platform-web ◄──────────┘ (winit + wgpu) (canvas + WebGPU, wasm32 only) └───────────────┬───────────────┘ ▼ ravenrs (facade + prelude) ravenrs-mcp ──► ravenrs-criteria (tool for coding assistants; nothing depends on it) ravenrs-lint (lints for applications; reads sources with syn, depends on no RavenRS crate) ravenrs-criteria (expression language, no RavenRS dependencies) ▲ ├── ravenrs-widgets (DataGrid filter expressions, number layout) │ ravenrs-graphics ──► ravenrs-core (shapes, chart layout, barcodes; shared, knows no engine) ▲ │ ravenrs-lottie ──► ravenrs-core (reads Lottie files into curves; no GPU, no widgets) ▲ └── ravenrs-widgets (`Lottie`, with the `lottie` feature) ravenrs-report ──► ravenrs-core, ravenrs-text (no GPU, no window) ▲ │ ravenrs-report-pdf, ravenrs-report-xlsx (exporters: Document ──► file) ravenrs-report-export ──► the exporters (one entry point: format, name, media type) ravenrs-grid-export ──► ravenrs-widgets, ravenrs-report-export (what a DataGrid shows ──► file) ravenrs-app ──► ravenrs-criteria, ravenrs-reactive (entities, application model, object spaces, actions, frames; no GUI) and ravenrs-app-spec with `source` (entities read from Rust source while running) ravenrs-app-spec ──► ravenrs-criteria, syn (what the derives read from a struct or enum, as data) ravenrs-app-derive ──► ravenrs-app-spec (the derives, re-exported by ravenrs-app) ravenrs-live ──► ravenrs-reactive, syn (interprets the Rust of examples; host types through a Registry) ravenrs-project ──► syn, zip (code of the playground ──► a Cargo project, as files or a .zip) ravenrs-live-derive (#[expose]: registers an impl block, struct, enum or module; behind `live` in core, style, layout, graphics, lottie, widgets, movie, app, app-ui) ravenrs-app-sqlite ──► ravenrs-app, rusqlite (records in SQLite) ravenrs-app-ui ──► ravenrs-app, ravenrs-widgets, ravenrs-grid-export, ravenrs-report (the screens, from the model) ravenrs-app-library ──► ravenrs-app (ready-made records) ravenrs-app-report ──► ravenrs-app-ui, ravenrs-grid-export, ravenrs-report-designer (reports and panels) ravenrs-report-viewer ──► ravenrs-widgets (Document ──► screen) ravenrs-sheet (spreadsheet engine from GridCraft: formulas, recalculation, XLSX; depends on nothing in RavenRS) ravenrs-spreadsheet ──► ravenrs-sheet, ravenrs-widgets (`Spreadsheet`: the engine ──► screen) ravenrs-movie ──► ravenrs-widgets (recorded draw lists ──► screen) ravenrs-ai ──► ravenrs-widgets (models on the machine; desktop only) ▲ ├── ravenrs-report-server (HTTP, schedules, delivery; only this crate has a runtime) │ ravenrs-report-designer ──► ravenrs-report-viewer, ravenrs-report-export, ravenrs-widgets (the designer) examples/report-designer ──► ravenrs, ravenrs-report-designer ``` `ravenrs-criteria` (the expression language) depends on nothing else in RavenRS, so any module can use it for filters, rules and validation. `ravenrs-report` (the report engine) depends only on `ravenrs-core`, `ravenrs-text` and `ravenrs-criteria`, so it also runs on servers. `ravenrs-widgets` uses `ravenrs-criteria` without its JSON feature, for `DataGrid` filter expressions and number layout. The `ravenrs` facade re-exports neither of them; apps depend on them directly. See [§6.3](#63-criteria-language), [§6.4](#64-reporting) and [docs/REPORTING.md](docs/REPORTING.md). There are no cycles. `ravenrs-render` also uses `ravenrs-text`, and the platform crates are where widgets and the renderer come together. `ravenrs-widgets` never touches the GPU and `ravenrs-render` knows nothing about widgets; they meet only through `ravenrs_core::Scene`, a plain display list. That boundary keeps widgets testable without a GPU (the widget tests run headless) and keeps the renderer replaceable. ## 3.1 Where things live | Crate | Modules | |---|---| | `ravenrs-core` | `geometry`, `color`, `event`, `scene` (the display list), `access` (the accessibility tree), `csv` (field quoting for every CSV export) | | `ravenrs-animation` | `spring`, `easing`, `oklab`, `animated` (pure functions of time) | | `ravenrs-reactive` | `state`, `binding`, `derived`, `observer` | | `ravenrs-render` | `renderer` (batching and passes), `gpu` (surface and device), `atlas` (glyphs), `images` | | `ravenrs-platform-native` | `lib` (winit loop), `access` (AccessKit) | | `ravenrs-wayland-gestures` | Touchpad pinches on Wayland, until winit delivers them; the only `unsafe` call | | `ravenrs-wayland-gestures` | Touchpad pinches on Wayland, until winit delivers them; the only `unsafe` call | | `ravenrs-platform-web` | `app` (canvas, events, frames), `access` (ARIA mirror), `error` | | `ravenrs-criteria` | `ast`, `parse`, `format`, `eval`, `functions`, `validate`, `sql`, `value` | | `ravenrs-report` | `model`, `engine`, `document`, `base64`, `paint`, `wizard`, `samples` | | `ravenrs-graphics` | `vector`, `chart`, `plot`, `sankey`, `barcode`; also the optional layout behind `Chart` and `Barcode` in `ravenrs-widgets` | | `ravenrs-lottie` | `document` (reading a file, and what it asks for that is not done yet), `property` (keyframes), `geometry` (trim, dash, stroke), `player` (one moment as `PathFill`s) | | `ravenrs-report-export` | `lib` | | `ravenrs-grid-export` | `lib` | | `ravenrs-app-spec` | `lib` | | `ravenrs-project` | `lib` | | `ravenrs-app-derive` | `lib` | | `ravenrs-live` | `value` (values, host objects, conversions), `registry` (what the host offers), `eval` (the interpreter), `bridge` (what `#[expose]` calls through) | | `ravenrs-live-derive` | `lib` | | `ravenrs-app-sqlite` | `lib` | | `ravenrs-app-library` | `lib` | | `ravenrs-app-report` | `lib`, `stored`, `analytics` | | `ravenrs-report-designer` | `lib`, `editor`, `bands`, `surface`, `props`, `preview` | | `ravenrs-app-ui` | `app`, `pane`, `form`, `session`, `shell`, `list_view`, `detail_view`, `nested`, `dashboard`, `editors`, `container`, `system`, `values`, `security`, `model_editor`, `list_editors`, `reminders` | | `ravenrs-app` | `entity`, `object`, `model` (`node`, `layers`, `store`, `generate`, `captions`), `space`, `action`, `controller`, `frame`, `module`, `functions`, `files`, `workflow`, `security` (`space`, `directory`, `password`, `records`, `audit`) | | `ravenrs-report-pdf` | `lib` (pages, fonts) | | `ravenrs-report-xlsx` | `lib` (grid, cells, CSV), `format` (Excel number formats) | | `ravenrs-report-viewer` | `lib` (`DocumentViewer`), `search`, `parameters` (`ParameterPanel`) | | `ravenrs-sheet` | `base` (addresses, values, dates), `numfmt`, `formula`, `functions`, `math`, `model`, `calc`, `xlsx`, `chart`, `pdf`, `engine` (`Session`: every action a command with undo). Kept as it came from GridCraft, with its own `rustfmt.toml` and `clippy.toml` | | `ravenrs-spreadsheet` | `lib` (`Spreadsheet`, labels), `state` (`SpreadsheetState`), `surface` (the painted grid, pointer and keys), `geometry` | | `ravenrs-ai` | `machine` (memory and disk), `models` (`LocalModel`, `ModelFolder`: download checked by hash, remove), `catalog`, `manager` (`ModelManager`). Desktop only | | `ravenrs-movie` | `movie` (the film: draw lists, each thing stored once), `project` (non-destructive edits), `composition` (one moment drawn), `record` (`Recorder`), `player` (with sound and the boxes that explain each step), `search` (`MovieLibrary`), `assistant` (`MovieAssistant`), `chat` (`MovieChat`), `editor`, `flight` (`FlightRecorder`, `BugReporter`) | | `ravenrs-report-server` | `config`, `store`, `cron`, `job`, `render`, `deliver`, `scheduler`, `http`, `main` | | `ravenrs-mcp` | `api` (index of the public API, parsed from embedded sources), `docs`, `expression`, `server` | | `ravenrs-lint` | `rules` (the syntax-tree walk and `allow` comments), `config` (`[metadata.ravenrs-lint]`), `workspace` (`cargo metadata`), `report` (compiler-style and JSON), `init` (strict lints, agents' instructions) | `ravenrs-widgets` has two parts. The runtime is `view`, `context` (`BuildCx`), `ui` and `ui/access`, `modifiers`, `clipboard`, `sound` and `util`. The components are grouped by family: | Module | Components | |---|---| | `stack`, `card`, `canvas`, `fill`, `text`, `icon`, `icon_set`, `image` | Layout primitives, `ZStack`, `ScrollEdgeArrow`, `PropertyRow`, `PageHeader`, `TitleBar`, `Footer`, cards, custom drawing, text, icons, images | | `button`, `inputs`, `text_field`, `choice`, `calendar`, `date` | Buttons, `ButtonGroup`, form controls, text input, `ToggleGroup`, `FavoriteToggle`, `SwatchGrid`, `NinePointAnchor`, `ColorPair`, `Pagination`, `Label`, `AspectRatio`, `Calendar`, `DatePicker`, `EventCalendar`, `RecurrenceEditor` | | `files` | `FileUpload`, `DropZone`, `ImageField` and the service that brings files in | | `quick_edit` | `ValueSlider`, `InlineRename`: controls that stay small until they are used | | `dock` | `DockArea`: the arrangement in `dock::layout` (`DockLayout`: a tree of ids, its moves and its saved text) and `dock::geometry` (rectangles and what lies under a drag), both without `Ui`, and the drawing in `dock::area` | | `lists` | Long lists and what is built on them: `VirtualList`, `Filmstrip`, `PagedDocumentView`, `FileBrowser`, `YearView`, `SortableList`, `DocumentTabs`, with the arithmetic of a reorder drag in one module (`lists::reorder`) | | `imaging` | Instruments for pictures, fed by the caller's data: `Histogram`, `Ruler`, `ColorWheel`, `CurveEditor`, `ZoomControl`, `ZoomPanView`, `Minimap`, `RuleOfThirdsGrid`, `RubberBand`, `Marquee`, `TransformHandles`, `Protractor`, `CropOverlay`, `Eyedropper`, `CompareView`, `ImageLightbox` | | `scheduler`, `recurrence` | `Scheduler` and its time axis, and `RecurrenceRule`, which needs no UI | | `nav`, `ribbon` | `Tabs`, `Sidebar` and its parts, `Breadcrumb`, `Disclosure`, `Ribbon` | | `overlay`, `floating`, `shortcuts` | `Tooltip`, `Popover`, `DropdownMenu`, `Select`, `Dialog`, then `ContextMenu`, `HoverCard`, `Sheet`, `SplashScreen`, `Toaster`, `Combobox`, `CommandPalette`, then `ShortcutSheet`, `ShortcutHintBar` | | `chat`, `rich_text`, `conversation` | `Chat`, `ChatMedia`, `FloatingPanel`; `RichText`; `AttachmentStrip`, `AudioPlayerButton`, `SelectionBar` | | `display`, `status`, `data`, `extras` | Badges and alerts; `Dot`, `OverlayBadge`, `Rating`, `IconTile`, `EmptyState`, `HeroBanner`, `MultiProgress`, `ShimmerText`, `DaySeparator`; lists, tables and charts; `Spinner`, `Carousel`, `Splitter`, `SplitPanes`, `SplitHandle`, `Menubar`, `InputOtp`, `FormField` | | `grid/` | `DataGrid`: `query` (pure pipeline), `model`, `view` | Examples use the public API, among them `feature-center`, every feature of the application framework, and: `hello-world`, `component-gallery` (one demo per component), `report-designer` and `family-finance`, a desktop app on SQLite that also finds what is missing in the framework. The design tokens are defined in [docs/design-system](docs/design-system), and `ravenrs-style` mirrors them value for value. ## 4. The frame pipeline ``` input event ──► Ui::handle_event ──► hit test ──► handler ──► State::set │ Observer marks the UI dirty, calls the waker ▼ platform schedules one frame (winit request_redraw / requestAnimationFrame) ▼ Ui::update: build (views → elements, tracked) ──► layout (Taffy) ──► paint (Scene) ▼ Renderer::render: batch quads/glyphs ──► upload instances ──► one render pass ──► present ``` * **Build** walks the view tree. Each `View::build` creates layout boxes and paint instructions through `BuildCx`. The whole build runs inside a reactive `Observer`, so every `State` read is recorded. * **Layout** runs Taffy over the boxes. Text leaves are measured by Parley through Taffy's measure callback, so wrapping participates in flexbox sizing. * **Paint** walks the element tree in order and appends primitives (`Quad`, `GlyphRun`, `PathFill`) to the `Scene`. Focus rings are painted after the focused element's children so they sit on top. An element outside its clip adds nothing, though it still gets a place in the order, because it can take focus and its children can stand outside it. * **Render** turns the scene into instanced draws. Consecutive primitives of the same kind are merged into one draw call; painter's order is preserved. Every rebuild runs from the root, so its cost follows the size of the screen, not the size of the change. What keeps that cheap: text is shaped once (`TextSystem` keeps recent layouts), the pointer moving over views that never asked about it builds nothing, and drawing that moves repeats only the paint through `Paint::Live`. Nothing runs between frames. An idle app sleeps on the desktop (`ControlFlow::Wait`) and schedules no animation frames on the web. Work on another thread sends its result through a channel and calls a `UiWaker`, the one handle that may leave the UI thread; state itself stays on that thread. While an animation runs, frames repeat only the paint phase ([§6.5](#65-motion)). ## 5. Reactive system `ravenrs-reactive` is single-threaded by design: `Rc`, `Cell`, `RefCell`, no locks. UI state lives on the UI thread on every platform, including the browser main thread. * `State` owns a value. `get`/`with` record a read in the innermost tracking `Observer`; `set`/`update` notify only the observers that read it during their latest run. * `Binding` is a read/write view of a value owned elsewhere (SwiftUI's `@Binding`). `Binding::map` projects onto a field. * `Derived` is a lazily recomputed, cached value over other state (a memo). It notifies its own readers when its inputs change. * `Observer` runs a closure and calls back once when any dependency changes. **Dependency bookkeeping without back-references.** Each observer run increments an *epoch*. A source stores `(Weak, epoch)` pairs; on notification, entries from older epochs or dropped observers are discarded. This keeps ownership one-directional (sources never keep observers alive), makes re-tracking O(reads), and makes dropping either side safe at any time. **What is rebuilt.** In this milestone the `Ui` has one observer for the whole build, so a change to any state the UI read triggers a full rebuild + relayout + repaint, and a change to state the UI never read triggers nothing (covered by `unrelated_state_does_not_invalidate`). Per-component observers and incremental reconciliation (rebuilding only the subtrees whose dependencies changed, reusing their layout nodes) are the next step (Phase 3); the `WidgetId` identity scheme below is already what that needs. **Identity.** Every view gets a `WidgetId` derived from its parent's id and its position (or an explicit key via `BuildCx::keyed_child`). Hover, press and focus survive rebuilds because they are stored per `WidgetId`, not per element. ## 6. Extensibility: making your own components Components can be created at three levels, and the built-in widgets use the same public API as apps: 1. **Functions** returning `impl View` for stateless composition. 2. **`Component`**: a struct that holds its state and implements `body() -> impl View`. Every `Component` is a `View` through a blanket implementation. 3. **`View`** implemented directly, with full access to `BuildCx`: * `cx.child(&view)`, `cx.children(&seq)`, `cx.keyed_child(key, &view)` to build children; * `cx.container(LayoutStyle, &children, Paint)` and `cx.text(...)` to create layout boxes; * `Paint::Quad(QuadPaint)` or `Paint::Custom(DrawFn)` to draw anything into the `Scene`, or `Paint::Rich(RichFn)` when the drawing also shapes text or needs the device scale; * `cx.on_click`, `cx.focusable`, `cx.cursor` for behavior; * `cx.is_hovered()`, `cx.is_pressed()`, `cx.is_focused()`, `cx.theme()` to style by state and tokens. `Canvas` wraps `Paint::Custom` for custom drawing inside normal layout, `view_fn` turns a closure into a reactive view, and `Option` renders conditionally. ## 6.1 Runtime services for components Rich components need more than layout and paint; the runtime provides them generically, so third-party components get the same powers as built-in ones: * **Widget-local state.** `cx.use_state(init)` returns a `State` owned by that widget instance, keyed by its `WidgetId` and call order. It survives rebuilds and is dropped when the widget is no longer built. `Keyed::new(key, view)` resets it on purpose. * **Overlays.** `cx.overlay(anchor, placement, &view, on_dismiss)` builds a floating layer after the main tree. It is laid out as its own Taffy root, placed relative to the anchor's final frame (flipped and clamped to the viewport), painted last, hit-tested first, and dismissed on outside clicks or Escape. * **Scrolling and clipping.** `cx.scrollable(node)` makes a vertical scroll container (`cx.scroll_horizontally` adds the other axis, which `DataGrid` uses): offsets are kept per `WidgetId`, wheel events move the innermost scrollable under the pointer, children are clipped with scissor rectangles, and a thin scrollbar is drawn. Scrolling only re-places and repaints; it does not rebuild or re-run layout. When Tab or `cx.request_focus` moves focus, the containers around the focused element scroll just far enough to show it, innermost first, stopping at the edge of a floating layer. A click focuses without scrolling. * **Scroll-driven pages.** `cx.pin(node, top)` keeps a node `top` pixels below the top of its scroll container while its parent passes, and lets the parent's bottom take it away (`Sticky`). It is moved when frames are placed, before hit testing, so input and the accessibility tree see it where it shows. `cx.scroll_track(node, top, length)` marks a scene track (`ScrollScene`), and `cx.scroll_linked(node, link)` paints a subtree with the `Effect` a `ScrollLink` computes from scroll progress (`ScrollLinked`): progress through the nearest scene, or the node's own pass across the view (`ScrollRange`). Both are worked out from the placed frames and the visible area of each node's scroll container (`Ui::views`), so scrolling still only re-places and repaints. Linked effects, like motion effects, change neither layout nor hit testing; under reduced motion they keep only their opacity. * **Showcase pages.** `cx.links()` opens addresses through a provider each platform installs (`Ui::set_links`: the browser navigates, the desktop opens the system's browser); `#name` instead glides the scroll container to the node marked with `cx.anchor(node, name, offset)` (`Anchor`), over a few paint frames. `cx.on_appear(node, handler)` calls a handler once, after the first frame in which a node shows in its scroll container (`CountUp`). `cx.time_linked(node, link)` paints a subtree with an `Effect` of the clock and its frame, keeping frames coming only while it shows (`Ticker`). `cx.scroll_progress(node, range, cell)` writes the scroll progress into a cell before each paint, for live drawing that follows the scroll itself (`QuadField`). * **Input.** `cx.on_key` (focused element first, default handling otherwise), `cx.on_drag` (start/move/end with the element frame), `cx.on_click`, `cx.focusable`, `cx.cursor`, and `cx.is_hover_within()` for hover over a subtree. * **Clipboard.** `cx.clipboard()` reads and writes the system clipboard: `arboard` on desktop, `navigator.clipboard` on the web (where reading is asynchronous, so a paste arrives through the browser's `paste` event as typed text). `Ui::set_clipboard` swaps the provider; tests use an in-memory one. * **Sound.** `cx.sound()` plays short clips and speaks. On the desktop, `cpal` plays on a thread that starts with the first sound, and its output closes once quiet; the voice is the system's own program (`say`, Windows speech through PowerShell, `espeak-ng`), which gets the text on stdin. On the web it is Web Audio and `speechSynthesis`. `Ui::set_sound` swaps the provider; the default is silence. * **Theme as state.** The theme is a `State`; `ThemeSwitcher` or any component can change it through `cx.theme_state()` and the whole UI rebuilds with the new tokens. * **Icons.** A subset of Lucide (ISC), about 200 icons in 68 KB, is embedded as a font, so icons are glyphs: sharp at any size, colored like text, and batched with text. `scripts/gen-icons.py` regenerates the subset and the `IconName` enum. ## 6.2 DataGrid `DataGrid` is split in two layers. `grid::query` is a pure pipeline over rows (filter → search → sort → group → summarize) with no UI dependency: it is unit-tested on its own, including a 100,000-row sort-and-group run. The view caches the query result per (query, data version) and builds only the rows inside the viewport plus a small overscan, with spacers above and below; scrolling rebuilds just that window (`BuildCx::scrollable_virtual`, `scroll_viewport`, `set_scroll_offset`). Data lives in `GridData`, shared copy-on-write so edits invalidate the cache exactly once. Parity with the feature set of commercial data grids: | Feature | RavenRS | |---|---| | Virtual scrolling (100k+ rows), vertical and horizontal | ✓ | | Sorting, multi-column (Shift/Ctrl-click), with priority badges | ✓ | | Filter row with operators (`>`, `>=`, `<`, `<=`, `=`, `<>`, `a..b`), text and yes/no | ✓ | | Filter row expressions over column titles (`[Seats] > 10 And [Country] = 'Brazil'`), from `ravenrs-criteria` | ✓ | | Search panel across visible columns | ✓ | | Grouping, nested, expand/collapse (click or arrows), group panel chips | ✓ (by dragging headers to the panel, reorder or ungroup by dragging chips, expand and collapse all) | | Group summaries and footer totals (sum, average, min, max, count) | ✓ | | Column resizing (drag), columns that keep their width, column chooser (show/hide), min widths | ✓ | | Selection: single, multiple with check boxes, Shift ranges, Ctrl toggle, select all | ✓ | | Keyboard navigation (arrows, Home/End, Page Up/Down, Space, Enter/F2) with auto-scroll | ✓ | | Cell editing (double-click, Enter, F2; Enter commits, Escape cancels), typed parsing | ✓ | | Cell templates (any view) and custom formats; number/currency formats with separators (digits from `ravenrs_criteria::NumberStyle`) | ✓ | | CSV export (RFC 4180, raw numbers; quoting from `ravenrs_core::csv`, shared with reports) | ✓ | | Copying rows as tab-separated text (Cmd/Ctrl+C) | ✓ | | Column reordering by drag, menu on each column, fit width | ✓ | | Columns pinned to the left | ✓ | | Multi-level (banded) headers, columns pinned to the right | planned | | Filter by value (checklists) with a filter panel | ✓ | | Filter builder | ✓ `FilterBuilder`, also in the toolbar with `filter_builder` and behind "Edit filter" on the filter panel, over the whole filter | | Paging UI, infinite scroll and rows on a server (`RemoteRows`: bounded memory, settled requests, no totals needed, keyset by `after`) | ✓ | | Summaries changed at run time, with formulas over `ravenrs-criteria` | ✓ | | Row add and delete (`on_add_row`, `on_delete_rows`), buttons on rows (`row_action`), edits that can be refused (`on_cell_edit`) | ✓ | | Keys that survive data changes (`key_column`, `selected_keys`), dates (`GridColumn::date`), field names (`GridColumn::field`) | ✓ | | Master-detail rows (`row_detail`, a fixed height for each, so the rows stay virtualized) | ✓ | | Row drag and drop | planned | | Saved layouts and named scenarios (`GridLayout`, `GridScenario`, JSON) | ✓ | | Excel and PDF export (`on_export_file`, with `ravenrs-grid-export`) | ✓ | | Cell range copy | planned | ## 6.3 Criteria language `ravenrs-criteria` is one expression language for filters, report totals, rules and SQL `WHERE` clauses, with the syntax and behavior business developers already know from filter languages of commercial suites. It knows nothing about reports or UI, and nothing specific to a module belongs in it. * **Tree.** `Criteria` is public: parse it from text (`parse`), build it in code, walk it, and print it back to text that parses to the same tree (`format`). * **Evaluation.** `Scope` interprets the tree in memory over any `Record`, such as JSON, a map or your own rows. `Environment` holds parameters and custom functions. * **Function catalog.** One table in `functions` drives parsing, arity checks and expression editors. * **Validation.** `validate` checks an expression against a `Schema` before anything runs. * **SQL.** `to_sql` translates to parameterized SQL per `Dialect`. * **Formatting.** `format_value` and `format_composite` use .NET-style format strings per `Culture`. `DataGrid` filter rows use this crate for expressions. Text that parses, whose fields are all column titles and that needs nothing beyond the row (no aggregates, parameters or custom functions), runs here over raw cell values. Anything else keeps the short syntax in `grid::query`, which stays a native fast path: it is checked against a frozen copy of its old behavior, and costs no parsing unless the text has a `[`. `NumberStyle` lays out digits for both `Culture` and the grid's `NumberFormat`. ## 6.4 Reporting `ravenrs-report` turns a definition plus data into a paginated document. Layout happens once, and every consumer draws the same result, so a preview and an export cannot drift apart. ``` Report (model: JSON or builder) + data ──► ReportEngine (engine) ──► Document (pages in points) │ paint_page ──► Scene ──► on-screen preview ``` Expressions in a report come from `ravenrs-criteria`, and text is measured with the same Parley stack as the UI. `wizard` builds a finished table report from a data source, and `samples` holds the templates. The designer is `examples/report-designer`, built on `Ribbon` and the other public components: `editor` (shared state), `surface` (bands and controls), `props` (property grid), `preview` (document viewer). Phase R1 is done. `ravenrs-report-pdf`, `ravenrs-report-xlsx` export the `Document` to PDF, XLSX and CSV, also in the browser. `ravenrs-report-server` stores layouts, renders over HTTP and delivers scheduled reports ([docs/REPORT-SERVER.md](docs/REPORT-SERVER.md)). `ravenrs-report-viewer` shows documents with thumbnails and search, and asks for parameters; [docs/REPORTING.md](docs/REPORTING.md) has the design and phases, and [docs/REPORTING-PARITY.md](docs/REPORTING-PARITY.md) tracks each feature. ## 6.5 Motion `ravenrs-animation` holds the mathematics and knows nothing about the UI. `ravenrs-style` carries the tokens in `Theme::motion`. The runtime is `motion.rs` in `ravenrs-widgets`. **Declared, not driven.** A view says how a node moves with `cx.motion(node, Motion::new()...)`. Builds only set targets, on tracks that live outside the view tree and are keyed by identity. Then `Ui::update` has three cases: state changed, so build, lay out and paint; only animations are due, so paint alone; nothing is due, so no frame is requested. An animation therefore costs a repaint, not a rebuild. `Scene` stays the only meeting point with the renderer, so motion works headless and needed no GPU changes. | Technique | Where | Why | |---|---|---| | Closed-form springs | `Spring::sample` | The position depends only on time, so results do not vary with frame rate and a long frame cannot blow up. | | Interruption without a jump | `Animated::set_target` | A spring restarts from its current position and velocity. Tweens are additive: each one eases in its share of the change and the sum has no corner. | | OKLab with premultiplied alpha | `Animatable for Color` | Midpoints keep lightness and hue, and a fade from transparent does not pass through gray. At rest the exact token comes back, with no rounding. | | Layout animation | `Motion::layout` | The node's old frame eases to its new one while children travel with it. Its size is redrawn rather than scaled, so nothing distorts. Input uses the final frame, and frames are compared without scroll offsets, so scrolling never animates. | | Shared layout | `Motion::shared_layout` | Nodes of different widgets count as one object, which is how a tab underline slides. `underlay` paints it behind its siblings. | | Exit by snapshot | `Motion::exit` | A view that stops being built is gone at once for the app: state, input and accessibility. Its last primitives stay on screen as a *ghost* that plays the exit. Reopening revives it from where it was. A nested exit (a dialog's card in its backdrop) plays inside the outer one. | | Anchored popups | `Motion::anchored` | Scale around the point nearest the anchor; offsets flip when the popup flipped. | | Height to fit | `Collapse` | The runtime reports the content's natural size to the next build. This one does rebuild on each frame, because it changes layout. | | Live drawing | `Paint::Live` | Spinners and pulses repaint at no more than 60 Hz with no rebuild. | | Theme crossfade | `Ui::paint` | The old picture fades over the new one, in a single layer, instead of every color moving at its own pace. | | Lag smoothing | `Ui::set_time` | Animations have their own clock, in which a gap above 250 ms counts as 50 ms: after a stall they continue instead of jumping to the end, and a very slow machine still gets through them. | **Reduced motion.** It comes from `Ui::set_reduced_motion` (the browser's `prefers-reduced-motion`, live) or from the theme's token. Things stop traveling and bouncing, colors and opacity fade briefly, and live drawing stops. **No clock, no animation.** Motion starts with the first `Ui::set_time`. A `Ui` without a platform lands everything at once, which keeps headless tests and reference images deterministic. **Limits.** Effects run on the display list (`Scene::apply_effect`), so there is no rotation, and opacity applies per primitive: overlapping shapes show through each other while partly faded. Real group opacity and rotation need layers and transforms in the renderer. Text snaps to pixels vertically, so a slow move of a text block advances in device pixels. ## 7. Rendering `ravenrs-render` is a small 2D renderer on `wgpu` 30. * **Quads** are instanced. The WGSL fragment shader evaluates a signed-distance function of a rounded box with per-corner radii, so edges are anti-aliased analytically (one device pixel of coverage ramp) at any scale factor. Borders, two-stop linear gradients and fill share the same shader. * **Soft shadows** are separate instances drawn under their shape: the coverage is an `erf` approximation over the same SDF, which gives a Gaussian-looking falloff in one pass with no blur texture. * **Text** is shaped by Parley, rasterized per glyph by `swash` (outline, alpha mask, 4 horizontal sub-pixel positions, no hinting) into an R8 atlas packed with shelves, and drawn as textured quads snapped to the device pixel grid. The atlas grows (×2 up to the device limit) when it fills; at the limit it is cleared. * **Color.** Tokens are sRGB. The renderer prefers non-sRGB surface formats so blending happens in sRGB space, as in browsers and design tools; on `*Srgb` targets the shaders linearize colors first. Output is premultiplied alpha. * **Resources** (pipelines, atlas, instance buffers) are created once and reused; instance buffers only grow. GPU→CPU readback exists only in tests: `crates/ravenrs-render/tests/offscreen.rs` checks pixels of real scenes and `crates/ravenrs/tests/visual.rs` compares golden images. GPU timing is off unless asked for, and its answers arrive frames later through buffers mapped in the background, so it never makes a frame wait (`timing.rs`). **Why not Vello (yet).** Vello's main renderer requires compute shaders and is still labelled experimental; its hybrid variant is promising but young. RavenRS needs a handful of primitives that an SDF quad pipeline draws in one pass, with a small WASM footprint. Arbitrary vector paths have their own pipeline, `Primitive::Path`: edges add up the exact area they cover in each pixel of a coverage texture, in one pass, and the main pass paints from it in scene order. It needs no compute, tessellation or multisampling. **Glass and blur (planned).** Raven Glass's backdrop blur needs a layer compositor: render the content behind a glass surface to an intermediate texture, blur it (separable Gaussian, downsampled), then composite. Until that lands, glass surfaces render their translucent fill; this is the documented degradation path for platforms where the extra passes are too costly. ## 8. Layout `ravenrs-layout` exposes a small typed subset of flexbox (`LayoutStyle`: axis, spacing, padding, alignment, justification, sizes, growth) over Taffy. The UI root is a box sized to the viewport; stacks are flex containers; text leaves have zero minimum width so they wrap instead of overflowing. The `Grid` view uses Taffy's Grid layout with equal-width columns (`grid_columns`). ## 9. Text Parley is used with system fonts disabled and Inter (SIL OFL 1.1) embedded at weights 400 and 600, so text is identical on every platform and works in browsers, which expose no system fonts to WebAssembly. `TextSystem::register_font` adds more fonts. Layout happens in logical pixels; glyphs are rasterized at the device scale factor, so HiDPI text is sharp without reshaping. ## 10. Platforms **Desktop (`ravenrs-platform-native`).** winit 0.30 (`ApplicationHandler`), one window, `ControlFlow::Wait`. Surface reconfiguration on resize and scale-factor changes, logical-pixel input, cursor icons, keyboard focus traversal, and GPU device-loss recovery (the device, surface and renderer are recreated on the next frame). **Diagnostics (desktop only for now).** The native platform wraps every root in `DiagnosticsHost`, which adds no element and no wake-up while closed. Ctrl+Shift+D (Cmd+Shift+D on a Mac) or the `RAVENRS_DIAG` variable opens a panel with the GPU in use, the alternatives, what is wrong and what recent frames cost, with text to copy. `ravenrs::native::diagnose` gives the same GPU report without a window, for a command line option. The data is plain types in `ravenrs-core`, so widgets and the renderer still only meet through it. Its performance test redraws the window for 5 seconds and judges the frames per second, which do count the GPU's work, since a busy GPU holds back the next frame. Each frame asks for the next only while it runs. The other durations are CPU time: there are no GPU timestamps, so a busy GPU only shows as waiting in acquire or present. The web platform does not record frames yet. With `AppIdentity::new_issue_url`, the panel also has a button that starts a bug report: `ravenrs_core::new_issue_url` puts the report in the URL of a new issue, percent-encoded and cut to `MAX_ISSUE_URL`, and `ravenrs::native::open_url` opens it in the browser. The app sends nothing; the person reviews and submits on that page. The report is made to be shared: `VK_DRIVER_FILES` and `VK_ICD_FILENAMES` keep only file names, the panel passes everything it shows through `redact_user_paths`, and no app data reaches it. The panel says this next to the button, above the exact text. **Tray (desktop, `tray` feature).** With `AppIdentity::close_to_tray`, closing the window hides it, or destroys it on Wayland, which cannot hide one, and keeps the `Ui` and its state behind an icon: `tray-icon` on macOS and Windows, `ksni` on Linux, which asks for no GTK. Where nothing hosts the icon, as on plain GNOME, closing the window closes the app. A copy of the app started meanwhile finds the first through a local TCP port, asks it to open its window and leaves. **Web (`ravenrs-platform-web`).** Plain `web-sys` on a ``: * async adapter and device creation (`wasm-bindgen-futures`), never blocking the main thread; * a friendly message next to the canvas when `navigator.gpu` is missing (no WebGL fallback in this phase); * canvas backing store sized from CSS size × `devicePixelRatio`, re-checked every frame and on `ResizeObserver` callbacks; * pointer events with pointer capture, keyboard events with Tab/Space/Enter kept inside the app; * `requestAnimationFrame` scheduled only when the UI is invalidated (at most one pending); * GPU device-loss recovery: the device is re-requested asynchronously; * `WebApp` removes every listener and observer and releases GPU resources when dropped. `winit`'s web backend was not used so that DPR handling, frame scheduling and (later) IME and accessibility bridges stay explicit. **Accessibility.** After layout, `Ui::access_tree` walks the element tree and builds a platform-neutral `AccessTree` (`ravenrs-core`). * **Which elements become nodes.** An element becomes a node when a widget described it with `Semantics` or when it is focusable (it then defaults to a button). Text is a label, except inside roles that take their name from content, where it becomes the name ("Save" on a button). Other boxes are transparent. * **Ids.** Node ids derive from `WidgetId`, so they stay stable across rebuilds. * **Modal layers.** While one is open, only it is exposed. * **Actions.** `Ui::perform_action` maps click, focus and increment/decrement back to the element's handlers. Assistive technology therefore drives the same code paths as the mouse and keyboard. * **Desktop.** The tree goes to AccessKit (`accesskit_winit`) only while a screen reader is listening, and is resent when it changes. The root transform scales logical to physical pixels. * **Web.** It is mirrored as invisible, absolutely positioned ARIA elements over the canvas. The hidden text input that holds keyboard focus points at the focused node with `aria-activedescendant`. New status and alert nodes are announced through a live region, and clicks on mirror elements become click actions. **WASM size (hello-world, `wasm-release` profile + `wasm-opt -Oz`).** ≈2.4 MB raw, ≈1.06 MB gzipped, of which about 830 KB raw are the two embedded Inter faces. Shipping a subsetted font is the first size win available. ## 11. Status | Area | Implemented | Experimental | Planned | |---|---|---|---| | Workspace, local checks (`scripts/check.sh`: desktop + wasm) | ✓ | | Hosted CI (intentionally not used for now) | | `State`, `Binding`, `Derived`, dependency tracking | ✓ | | | | Full rebuild on tracked change | ✓ | | Incremental reconciliation, per-component observers | | `View`, `Component`, `BuildCx`, `view_fn`, `responsive`, `Option`, `Keyed`, `ViewExt` modifiers, `use_state` widget-local state | ✓ | | Environment values, `#[component]` macro (`ravenrs-macros`) | | Layout: `VStack`, `HStack`, `ZStack`, `Grid`, `ScrollView`, `Sticky`, `ScrollScene`, `ScrollLinked`, `Anchor`, `Ticker`, `CountUp`, `Link`, `QuadField`, `Spacer`, `Footer`, `PropertyRow`, `PageHeader`, `Section`, `Card`, `Splitter`, `SplitPanes`, `SplitHandle`, `DockArea`, `AspectRatio`, `VirtualList` (rows of any height, measured and kept by key), `SortableList` | ✓ | | | | Inputs: `Button` (4 styles, 3 sizes), `IconButton`, `ButtonGroup`, `TextField`, `TextArea`, `CodeEditor`, `Playground` (feature `code`), `NumberField`, `MaskedField`, `TimePicker`, `DateTimePicker`, `LookupField`, `FormLayout`, `Toolbar`, `FloatingToolbar`, `TitleBar`, `SplitButton`, `ConfirmDialog`, `Toggle`, `Checkbox`, `RadioGroup`, `Slider`, `ValueSlider`, `InlineRename`, `Stepper`, `SegmentedControl`, `Select`, `Combobox`, `ToggleButton`, `ToggleGroup`, `InputOtp`, `Calendar`, `DatePicker`, `ThemeSwitcher`, `FavoriteToggle`, `SwatchGrid`, `NinePointAnchor`, `ColorPair`; text selection in `TextField` | ✓ | | multi-line text | | Navigation: `Tabs` (underlined, segmented), `DocumentTabs`, `FileBrowser`, `YearView`, `Sidebar`, `Breadcrumb`, `Disclosure`, `Accordion`, `Pagination`, `Menubar`, `Ribbon` | ✓ | | `NavigationView` | | Overlays: `Tooltip`, `Popover`, `HoverCard`, `DropdownMenu`, `ContextMenu`, `Dialog`, `Sheet`, `SplashScreen`, `Toaster`, `CommandPalette`, `ShortcutSheet` | ✓ | | | | Data grid: `DataGrid` (see §6.2) | ✓ | | see the parity table in §6.2 | | Display: `Image` (PNG/JPEG, cover/fit/fill, mipmaps), `Filmstrip`, `PagedDocumentView`, `ImageLightbox`, `CompareView`, `Eyedropper`, `CropOverlay`, `Protractor`, `TransformHandles`, `Marquee`, `RubberBand`, `RuleOfThirdsGrid`, `Minimap`, `ZoomPanView`, `ZoomControl`, `CurveEditor`, `ColorWheel`, `Ruler`, `Histogram`, `Text`, `RichText`, `AttachmentStrip`, `AudioPlayerButton`, `SelectionBar`, `Icon` (about 200 Lucide icons), `Badge`, `Dot`, `OverlayBadge`, `Rating`, `IconTile`, `EmptyState`, `HeroBanner`, `MultiProgress`, `ShimmerText`, `DaySeparator`, `Avatar`, `Kbd`, `Divider`, `ProgressBar`, `Spinner`, `Skeleton`, `Carousel`, `ColorSwatch`, `Alert`, `Stat`, `List`, `Table`, `Chart` (line, area, bar, pie, doughnut, radar, radial, scatter, sparkline, candlestick), `SankeyChart`, `BarChart`, `AreaChart`, `Barcode` (QR Code and six linear formats), `Canvas`, `Lottie` | ✓ | | image loading from URLs, funnel charts | | `GlassCard` | | translucent fill only | backdrop blur | | Rounded rects, borders, gradients, soft shadows, text, images (mipmapped, rounded), rectangular clipping | ✓ | | rounded clipping, radial gradients, transforms, layers | | Raven Dark / Light / Glass tokens, runtime theme switching, reduced motion from the theme token, `Ui::set_reduced_motion` and the browser's `prefers-reduced-motion` | ✓ | | reading the setting on the desktop (winit does not expose it) | | Motion (see §6.5): analytic springs, additive tweens, OKLab colors, implicit paint and layout transitions, shared layout, enter and exit, paint-only frames; scroll-driven `Sticky`, `ScrollScene`, `ScrollLinked` | ✓ | | rotation and true group opacity (renderer transforms and layers), gestures, momentum scrolling | | Desktop: window, HiDPI, resize, mouse, wheel/trackpad, keyboard, clipboard, sound and voice, device loss | ✓ | | multiple windows, IME | | Web: canvas, WebGPU or WebGL2 (`WebBackend`), DPR, pointer, wheel, touch scrolling with momentum, keyboard, text input through a hidden textarea (IME commit, dead keys, paste), clipboard copy, sound and voice, rAF on demand, cleanup, device loss | ✓ | IME preedit is not drawn yet | pinch | | Accessibility | keyboard focus and focus ring, WAI-ARIA keys, accessibility tree, AccessKit on desktop, ARIA mirror on the web | | caret and selection for screen readers, right-to-left | | Criteria language: parsing, evaluation, validation, SQL, formatting (see §6.3) | ✓ | | `DataGrid` filters on this crate | | Spreadsheet: `Spreadsheet` over `ravenrs-sheet` (500+ functions, dynamic arrays, XLSX/XLS/ODS/CSV in, XLSX/CSV out), editing in the cell and the formula bar, selection by pointer and keys, clipboard, undo, fonts, fills, borders, merges, number formats, column widths, sheet tabs | ✓ | | ribbon of every command, frozen panes, charts and pictures on the sheet, conditional formats drawn, fill handle, find, the culture's separators | | Reporting: engine, wizard, designer with preview, viewer, pictures, links, barcodes, charts, PDF, XLSX and CSV export, server with scheduled delivery (see §6.4) | ✓ | | Other exports, parameter lookups, SQL data sources (phases R2 to R5) | | Benchmarks and profiling | `tracing` spans per phase, criterion benches (`crates/*/benches`), a gate of exact counters and frame times (`crates/ravenrs-perf`, budgets in [docs/QUALITY.md](docs/QUALITY.md)), the diagnostics panel with GPU timestamps ([docs/PERFORMANCE.md](docs/PERFORMANCE.md)) | | stepping through a frozen frame, a map of what was rebuilt, invalidation by subtree | | Application framework: `ravenrs-app`, `ravenrs-app-ui`; see [docs/APP-FRAMEWORK.md](docs/APP-FRAMEWORK.md) | derive, model, actions (also declared as data), frames, rules, SQLite, generated lists and forms, permissions, sign-in, audit trail | the API | relations | ## 12. Decisions log | # | Decision | Alternatives considered | Why | |---|---|---|---| | 1 | Own SDF quad renderer on wgpu | Vello, tiny-skia | One pass per primitive, small WASM, no compute requirement. | | 2 | Parley + swash + own atlas | glyphon/cosmic-text | Parley is the most complete pure-Rust rich text layout; owning the atlas keeps text in the same batching and pass as quads. | | 3 | Embedded Inter, no system fonts | System fonts on desktop | Identical rendering everywhere; the web has no system fonts. | | 4 | `web-sys` directly on the web | winit web backend | Explicit control of DPR, frame scheduling, IME and accessibility bridges. | | 5 | Single-threaded reactivity with epoch subscriptions | Signals with back-references, `Arc` | No locks on the UI thread; safe drops; matches the browser model. | | 6 | Display list (`Scene`) as the widget/renderer boundary | Widgets calling the renderer | Headless widget tests; swappable renderers. | | 7 | Builders before macros | `VStack!{}` proc macros first | Real, type-checked API first; macros later as sugar over it. | | 8 | Non-sRGB surface formats preferred | Linear blending | Matches how designers and browsers blend, so tokens look as designed. | | 9 | The spreadsheet engine is GridCraft's, brought in as `ravenrs-sheet` | Writing an engine; a git dependency on GridCraft | GridCraft is pure Rust under MIT OR Apache-2.0, runs in wasm32 and has 1,800 tests for formulas, recalculation and XLSX. It is not on crates.io, so a git dependency would stop RavenRS publishing; the code lives here with `NOTICE` naming the commit it came from. Only the view is ours (`ravenrs-spreadsheet`), built on `ravenrs-widgets`. | --- # Design system URL: https://alexkads.github.io/RavenRS/docs/design-system/ Source: https://github.com/alexkads/RavenRS/blob/master/docs/design-system/README.md # RavenRS Beautiful native interfaces, powered by Rust. RavenRS draws every pixel itself on the GPU (wgpu), on the desktop and in the browser (WebAssembly + WebGPU), so this system is the single source of truth for how a RavenRS app looks everywhere. The Rust crate `ravenrs-style` mirrors these tokens value for value; when a token changes here, it changes there. ## Principles - **Calm surfaces, one sheen.** Raven feathers read black until light hits them and a blue-violet sheen appears. Interfaces work the same way: deep neutral surfaces (`bg-000` to `bg-300`) and a single accent (`accent`) reserved for the one action or selection that matters on a screen. - **Depth from light, not lines.** Cards separate from the canvas through fill (`bg-200`) and a soft shadow (`shadow-soft`). The `border` hairline supports that edge; it is never the only cue. - **Legible first.** Text meets 4.5:1 on its ground in every theme (`ink` 15:1+, `ink-muted` 5.5:1+), focus is always visible (`focus-ring`), and hit targets are at least 32px tall (44px on touch). - **Motion that explains.** Springs move things that change place; color tweens (`duration-fast`) acknowledge input. With reduced motion requested, springs collapse to instant changes and fades. ## Themes | Theme | Use it for | Notes | |---|---|---| | Raven Dark | The default. Tools, editors, dashboards used for hours. | Neutrals carry a slight blue bias toward the accent. | | Raven Light | Documents, forms, bright environments. | Shadows are fainter; `border` does more of the edge work. | | Raven Glass | Overlays, sidebars and hero cards over rich backgrounds. | Surfaces are translucent white over `bg-000`. Background blur is applied by the renderer where the platform supports it and degrades to the translucent fill otherwise. | A generated application has a theme menu in the footer of its navigation, unless it asks for `AppShell::without_theme_menu`. `ThemeEditor` changes colors, corners and text sizes by the token names below, and warns when text falls under 4.5:1. What it changes is kept as JSON holding only the differences from the base theme. Spacing is not offered: components still take it from a shared constant, so editing it would change only part of a screen. ## Color - `bg-000` is the canvas. `bg-100` holds panels and sidebars, `bg-200` cards, `bg-300` controls. - `ink` for content, `ink-muted` for labels and metadata. Never put `ink-muted` on `bg-300` for body text. - `accent` fills the primary button and marks selection. Text on it is always `on-accent`. Hover and press shift lightness (`accent-hover`, `accent-pressed`), not hue. - `success`, `warning` and `danger` signal state only and always come with a label or icon. ## Typography Inter, embedded in every build (the web has no system fonts to fall back on), at two weights: 400 and 600. Scale: `metric` 42, `display` 32, `title` 22, `headline` 17, `body` 15, `callout` 13, `caption` 12. Numbers in `metric` use tabular figures. `caption` is uppercase with +0.4px tracking. ## Space and shape Spacing is a 4px grid: `space-1` (4) through `space-12` (48). Screens pad by `space-8`, cards by `space-6`, stacks default to `space-4`. Radii grow with the size of the object: `radius-md` (10) for buttons, `radius-xl` (24) for cards, `radius-full` for pills. ## Motion | Use | Token (`theme.motion`) | Rust API | Parameters | |---|---|---|---| | Hover, press, focus color | `fast` | `Transition::fast()` | `duration-fast`, ease-out | | Small layout and opacity changes | `base` | `Transition::base()` | `duration-base`, ease-in-out | | Things changing place or size | `snappy` | `Transition::snappy()` | stiffness 380, damping 30 | | Panels and sheets | `gentle` | `Transition::gentle()` | stiffness 170, damping 26 | Rules for components: - Use `theme.motion`, not numbers of your own, and declare motion with `cx.motion`. - What people fold and unfold many times a day, as a sidebar and its submenus, takes `base`. A spring's long tail reads as hesitation there. - While a sidebar narrows its edge covers the content, which does not reflow: names never squeeze letter by letter. - Exits are shorter than entrances: the user has already moved on. - Scale subtly (0.96 and up). Popups grow from their anchor, with `Motion::anchored`. - A surface with content comes in solid: opacity applies to each shape on its own, so a panel fading in shows the page through its fields. Leaving, they fade as one surface with `Motion::surface_exit`, content first and panel after. They take `fast`, without a spring, whose tail makes text shiver. - A row in a virtualized list has no enter animation, because scrolling would set it off. ## Components | Family | Components | |---|---| | Text and icons | `Text`, `Icon`, `Label`, `Kbd`, `Badge` | | Themes | `ThemeSwitcher`, `ThemePicker`, `ThemeEditor` | | Actions | `Button`, `IconButton`, `ToggleButton`, `ToggleGroup`, `DropdownMenu`, `Ribbon`, `Menubar` | | Forms | `TextField`, `Select`, `Combobox`, `DatePicker`, `Calendar`, `Checkbox`, `RadioGroup`, `Toggle`, `Slider`, `Stepper`, `SegmentedControl`, `InputOtp`, `ColorSwatch`, `FormField` | | Surfaces | `Card`, `GlassCard` (translucent fill; blur needs the compositor), `Section`, `Divider`, `Splitter`, `Accordion`, `Disclosure` | | Navigation | `Sidebar`, `SidebarItem`, `Tabs`, `Breadcrumb`, `Pagination`, `CommandPalette` | | Overlays | `Dialog`, `Sheet`, `Popover`, `Tooltip`, `HoverCard`, `ContextMenu`, `Toaster` | | Data | `DataGrid`, `Table`, `List`, `Stat`, `Chart`, `AreaChart`, `BarChart`, `Barcode`, `Avatar`, `Image`, `Carousel` | | Feedback | `Alert`, `ProgressBar`, `Skeleton`, `Spinner` | Charts take their series from `chart-1` to `chart-5`, which each theme defines to read on its background. They mean nothing on their own, unlike `success` and `danger`, so a series never looks like a warning. Use a status color for a value only when it says that status, such as a budget over its limit. Their gridlines take `chart-grid`, which follows `border`. Modules of RavenRS itself, such as the report designer, follow the same rule as any app: they use these components and tokens, and what is missing joins the library first. Printed paper is the one surface that stays light in every theme, drawn with Raven Light tokens. ## Screens These rules came out of reviewing `examples/feature-center` screen by screen, and they hold for every component and application. - **One action stands out.** A window accents the one action that matters most: Save on a form, New on a list. Everything else in the bar stays a ghost button. - **One page margin.** Title, commands, description and content all start on the same line, `space-4` from the page's edge. - **No space by accident.** Stacks default to `space-4`. Put spacing in on purpose where pieces of a window meet, or it shows up as an empty band under a toolbar or a gap beside the navigation. - **Title or trail.** One level is a page title in `headline`. A breadcrumb appears only when there is somewhere to go back to. - **Fields fill their column.** References, colors and date with time take the same width as the text fields around them. A fixed width is a choice made at the call site. - **Say it once.** A collection in a tab has no heading or label of its own, and a nested list leaves out the column of its owner. An `Alert` has no title when one would only repeat where it is: use `Alert::message`. - **Long lists are striped.** `DataGrid::striped` tints every other row with `bg-300` at 40%, under the pointer at 85%, and the accent stays for the selection at 13%, so the three never get confused. Lists generated from a model do it by themselves. - **Grids show data first.** The filter row is flat, with each filter's text starting where its column's does, at the end for numbers. The group panel shows only when something is grouped or when asked for with `group_panel(true)`; the Group menu is always in the bar. - **Nothing moves under the pointer.** Selecting a row does not change the height of what is above it. Prefer replacing a status for adding to it. - **One piece, no frame.** A dashboard with a single item has no tile around it. What only helps while handling something, such as a tile's resize corner, shows only under the pointer. - **One language.** Every label follows the `Labels` of the application, including those in report viewers. ## Writing Name things the way people see them: "Get started", "Save changes", "Revenue". Buttons start with a verb. Errors say what happened and how to fix it. --- # Records & forms URL: https://alexkads.github.io/RavenRS/docs/applications/ Source: https://github.com/alexkads/RavenRS/blob/master/docs/APP-FRAMEWORK.md # Application framework Declare what a record is, and get the screens: navigation, a list, a form, commands and rules. Everything else changes through metadata, actions and controllers. The behavior follows what is documented for model-driven application frameworks; the code and the words are our own. > **Status:** in progress, usable from code, with the API still moving. Nothing here is in the `ravenrs` facade yet. ## What exists | Crate | What it has | |---|---| | `ravenrs-app` | No GUI. `#[derive(BusinessObject)]` and `#[derive(BusinessEnum)]`, from `ravenrs-app-derive`; `EntityInfo` and `Object`; `generate_model`, `ModelLayers`, `Diff`, `ModelStore`, `check_model`; `ObjectSpace` and `MemoryDatabase`; `Action`, `TargetScope`, `ViewController`, `WindowController`, `Frame`; `Module`, `Application` | | `ravenrs-app-a11y` | `AccessibleText`, `Announcer` and `LiveRegion`, `AccessWords`, `Audit`: names from the model, announcements, and an audit that walks an application | | `ravenrs-app-ui` | `App`, `AppShell`, `EntityPane`, `ListView`, `DetailView`, `EditorRegistry`, `ActionContainer`, `FrameTemplate`, `SystemModule`, `Words` | | `ravenrs-app-i18n` | `Language`, `Catalog` and `Texts`: every word of the screens and of the components by key, in English and Brazilian Portuguese, with the translations of the model that come with the framework | | `ravenrs-app-sqlite` | `SqliteDatabase`, `TableMap`: on tables it creates or tables that exist, on a connection the app shares | | `ravenrs-app-report` | `ReportModule`: an action that lays out a list as a report and shows it in the viewer | | `ravenrs-app-library` | `LibraryModule`: people, organizations, addresses, tasks, events, notes and files, to use or extend, with `PT_BR` for their words | | `ravenrs-grid-export` | What a `DataGrid` shows, as Excel, PDF or CSV | ## In five minutes Add `ravenrs`, `ravenrs-app` and `ravenrs-app-ui` to a project, and declare your records: ```rust,ignore use ravenrs_app::{BusinessEnum, BusinessObject}; use ravenrs_app_ui::{App, Language}; #[derive(BusinessEnum)] enum Kind { #[option(caption = "Corrente")] Checking, #[option(caption = "Poupança")] Savings, } #[derive(BusinessObject)] #[object(caption = "Contas", navigation = "Cadastros", icon = "wallet")] struct Account { #[field(key)] id: i64, #[field(caption = "Nome", default_property)] name: String, #[field(caption = "Tipo")] kind: Kind, #[field(caption = "Saldo inicial", money)] opening: i64, } fn main() -> Result<(), ravenrs::native::PlatformError> { let window = App::new("Raven Contas") .language(Language::BrazilianPortuguese) .entity::() .into_view(); ravenrs::native::run(window, ravenrs::native::WindowOptions::new("Raven Contas")) } ``` That is the whole application: navigation, a list with search, filters and export to Excel, PDF and CSV, a form, and New, Save, Save and close, Save and new, Cancel, Delete, Refresh, Back, Previous and Next. [`examples/quick-app`](../examples/quick-app) is this with a second record and a reference between them, in under 100 lines, in a window with `cargo run -p quick-app` and in the browser with `scripts/build-web.sh quick-app`. Records live in memory for now, so they are gone when the application closes. `seed` puts some in when it opens. A wrong attribute is a compile error that points at it. ### What a field can say | On `#[field(...)]` | Means | |---|---| | `key` / `typed_key` | Identifies the record: numbered by the application, or typed by people. Exactly one field has one | | `caption = "..."` | What people see | | `default_property` | What names a record, in lists to pick from and references | | `read_only`, `no_list`, `no_detail`, `lookup` | Where it shows, and whether it changes | | `money` | A whole number of cents | | `long`, `color`, `date`, `decimals = 2` | What it holds, when the Rust type does not say | | `reference = "Entity"` | Holds the key of another record | | `calculated = "[a] + [b]"` | Follows from the others, checked at compile time | | `default = ...` | What a new record starts with | | `required`, `unique`, `min = 1`, `max = 10`, `matches = "..."` | What it has to meet on saving, and `message = "..."` what to say then | | `accessible_name = "..."`, `accessible_description = "..."` | What screen readers call it instead of the caption, and say after it instead of the hint | On the struct, `#[object(focus_order = "name,amount")]` puts these fields first on the form, in this order, on the screen and for Tab. `String`, `bool`, `i32`, `i64`, `u32`, `u64`, `usize`, `f64` and `Option` of them work as they are. Any other type implements `FieldValue`, which `#[derive(BusinessEnum)]` does for an enum. ### Reading the same structs while the program runs With the `source` feature, `entities_from_source` reads Rust text with these structs and enums and gives their `EntityInfo`s without compiling, as the playground does. The derive and this function share `ravenrs-app-spec`, so they accept the same attributes and refuse the same mistakes with the same words, each with its line and column (`SourceError`). Only what a compiler would have to work out is refused on top: a type other than the ones above or an enum of the same file, and a `default`, `min` or `max` that is not a literal or an enum option. ```rust,ignore let entities = ravenrs_app::entities_from_source(source)?; // Err holds every SourceError ``` ### Rules On the struct, as many as needed: ```rust,ignore #[rule(criteria = "[amount] <= [limit]", message = "Above the limit")] #[rule(unique = "bank,number")] #[rule(criteria = "[paid] = False", context = "Delete", message = "A paid bill stays")] #[appearance(names = "amount", criteria = "[amount] > 1000", font = "danger", bold)] #[appearance(names = "notes", criteria = "[paid] = True", visible = false)] #[appearance(names = "Pay", action, criteria = "[paid] = True", enabled = false)] #[appearance(names = "Long", layout, criteria = "[paid] = True", visible = false)] struct Bill { /* ... */ } ``` A form that fails its rules keeps nothing, and says what is wrong on each field and in a list on top, which follows as people fix it. Rules are in the `Save` context unless they say otherwise. `Delete` applies on deleting, and an action names any other with `validation_contexts`. Rules do not run by themselves below the screens: `validate` is there for anything else that writes. Appearance is about looks, and does not protect data. A column of a list hides only by a rule with no condition, since it is for all its rows. An action hidden by a rule on a list only fades, as hiding would move the buttons beside it. ### Actions declared as data An action that only changes fields of the records selected needs no code: ```rust #[derive(BusinessObject)] #[action(caption = "Concluir", selection = "many", enabled = "[state] <> 'done'", set = "state = 'done'; finished = Today()", confirm = "Concluir as tarefas?")] struct Task { /* ... */ } ``` `set` is `field = expression; ...`, in the criteria language, and every expression reads the record as it was before any of them. Each record changed is checked against its rules in the `Save` context, and then all are kept, or none: one record that breaks a rule keeps every record as it was, and says what is wrong. `enabled` is what every record selected has to meet for the action to be on, as `target_criteria` with `TrueForAll`. `selection` is `one`, `many`, the default, or `any`. The id is `Entity.Caption` unless `id = "..."` gives one. The derive checks that both expressions parse and name only fields of the struct; `EntityInfo::action(RecordAction::new(...))` does the same at run time, and what is wrong comes out when the application is put together. `Action::assign(&entity, "field = expression; ...")` gives any action the same work, for one made in code with `on_execute` left out. State machines still guard what is kept, so an action that sets a state goes only by the ways declared. ### Inside an application that already exists `EntityPane` is a list beside its form, as a piece of a screen an application made itself, and `SqliteDatabase` maps onto its tables without touching their schema, on the connection it already has. `examples/family-finance/src/ui/records.rs` does this for its accounts, categories and family members: the tables and their migrations stay where they were. `EntityForm` is a form in a window for a record the application keeps in its own way: it checks the rules and hands over the record, and the application's own code writes it. `ListView` takes a `filter`, a `sort`, a `column_shape` and a `grid_shape`, and `ActionButton` shows one action as a button, so a screen made by hand can keep its look with lists and commands from the framework: `Session::embedded` is what they share. `examples/family-finance/src/ui/ledger.rs`, `bills.rs` and `forms.rs` do all of that. ### Relations ```rust EntityInfo::new("Customer") .field(FieldInfo::new("id", FieldKind::Integer).key()) .field(FieldInfo::new("name", FieldKind::Text).default_property()) // On the form: the orders that point here, with Link and Unlink, kept right away. .field(FieldInfo::new("orders", FieldKind::Collection("Order".into()))); EntityInfo::new("Order") .field(FieldInfo::new("id", FieldKind::Integer).key()) .field(FieldInfo::new("customer", FieldKind::Reference("Customer".into()))) // `Select` lists every choice; without it, a reference is looked up as people type. .field(FieldInfo::new("tag", FieldKind::Reference("Tag".into())) .editor("Select") .lookup_filter("[side] = ?side") .none_caption("None")) // With two or more, a menu picks among them, and the choice stays in the person's layer. .variant(ViewVariant::new("all", "All")) .variant(ViewVariant::new("open", "Open").filter("[status] = 'open'")); ``` `Action::popup` on a list view asks for a record to be picked, and on a detail view for a form to be filled in, which `on_prepare` starts and the action receives as `ActionInput::Record`. An entity no storage knows works for that. `Application::builder(...).dashboard(DashboardInfo::new("Home", "Home").view("Orders", "Order_ListView").control("Chart", "chart"))` puts views and controls together, and `Session::register_control` says what a control is. ### Beyond the grid, and beyond the code - `register_list_editor` adds a way to show a list, picked by `EditorType` on a view in the model, or by `DefaultListEditor` on `Views` for all of them. - `ListFilter` and `list_criteria` put filters in the model. `ViewVariant::columns` changes what a way to see a list shows. `register_function` adds a function to every expression; a filter that uses one runs in memory. - `Rules` under an entity in the model add rules with no code. `translation` gives the model's words in another language, and captions follow to the views unless one has its own. - `checks_on_leave` makes a form speak about a field as it is left. `transient` marks a record no storage keeps, and `ListView::source` gives a list its records. - `cloneable` with `CloneModule` offers cloning. What a record holds comes along, a reference points to the same record, `non_cloneable` stays behind, and only a collection marked `aggregated` is cloned with it. - `use_attachment_store` keeps files apart from their records, with `MemoryAttachmentStore` or `SqliteAttachmentStore`. - `SqliteBuilder::migration` runs something once in a database's life, `managed` adds columns for new fields, and `entities_from_schema` starts from a database that came first. `SqliteModelStore` keeps a person's settings in it, through `use_model_store`. - `ApplicationBuilder::service` hands actions what the application has, by type, through `ActionContext::service`. `Module::initial_records` puts in place what a module needs. ### When things meet in real use - `Rule::warning` asks and lets people go on, and `Rule::information` saves and leaves a note on the field. `Rule::message` can say `{field}`, `{reference.field}`, `{$field}`, `{$entity}`, `{$min}` and `{$max}`. - Saving a form puts what it changed together with what someone else kept since it opened: `merge_changes` keeps both when the fields differ, and the form asks when they are the same. A record that is gone says so. - `list_editable` lets people change plain values right on a list, refusing what breaks a rule on that field or on the whole record. A cell already wrong shows it, leaving out rules that read other records, so a long list stays quick. - `data_access` picks how a list reads: `Client`, all of it; `Server`, as needed; `DataView`, only its columns and with no editing, finding the whole record when one is selected. - A difference can add fields under `Members`, each with `"$new": true`, `Kind` and `Caption`, `Expression` for one worked out, and `Options` for a list. They come in order of name, and get a column and a place on the form. `App::administrator_settings` gives them before storage opens, and `App::storage_for` hands a database the entities with them. - `FieldKind::Guid` makes up a key. `FieldKind::Tags` holds several options. `predefined`, `edit_mask`, `criteria_of`, `display_format` and `group` shape a field, and the `Progress` editor shows a number from 0 to 100. - `ActionContext::message` tells people something, with `Message::action` and `Message::on_close`. `ActionCategory::InForm` with `EntityInfo::form_actions` puts an action inside a form's layout. - `use_records` and `use_collection`, or the same on `App`, give records no storage keeps. A navigation item can open a form directly: on a new record, or with `ObjectKey` on one by its key. - `DashboardInfo::variant` lets people change a dashboard to another; it lists itself too, since a menu takes two. - `export_captions` writes what people see as CSV, with an empty column to fill in, and `import_captions` takes it back as a language. ### Going further `App` covers the usual case. Under it, `EntityInfo` declares a record at run time, `Application::builder` and `AppShell` take everything apart, `module` adds actions and controllers, `editors` changes a field's control, and `storage` keeps records elsewhere. `examples/component-gallery/src/samples/navigation.rs` has `AppShell` used directly. ## How it fits together - **The model** is a tree. `generate_model` makes its lowest layer from the entities. Modules, an administrator and a user add layers on top, each a difference in JSON; the highest to set a value wins, and clearing a value brings back the layer below. A difference that finds no node is kept, and works again if the node comes back. - **Object spaces** are units of work. Each view has its own, so canceling a form throws away only that form's changes. - **Actions** are on or off by reasons, and every reason has to say yes. A view that does not fit takes an action away; a selection or criteria that does not fit fades it. - **A frame** holds a view. Changing it tells whoever watches, turns the controllers off, changes the view, turns on those that fit, and tells again. - **`Application::build`** says what is wrong with the declaration before any screen opens. ## Rules - Screens use only `ravenrs-widgets`. What is missing goes there first, with tests and a gallery entry, and never into these crates. - `ravenrs-widgets` knows nothing of `ravenrs-app`, and `ravenrs-criteria` nothing of either. - `ravenrs-app-i18n` depends on nothing of RavenRS, so the components read their words from it too. A new word is a key in each language's JSON, never a text written in code. - Money is a whole number of cents, kept in a number, and never a fraction. - Tests drive whole applications through `Ui::access_tree`, with no window: see `crates/ravenrs-app-ui/tests/e2e.rs`. ## Trying everything [`examples/feature-center`](../examples/feature-center) has every feature, one small demonstration each, with what to try on each list. Sign in as `ana`, `bia` or `caio`, with the password `raven-demo`, and compare. Its `tests/tour.rs` goes through it all, in memory and in SQLite, so a new feature gets a demonstration there and a place in that tour. ## Showing a list other ways A view picks how it shows its records by name in the model, with nothing in code: ```json { "Views": { "Task_ListView": { "EditorType": "Chart", "ChartSettings": { "$new": true, "Argument": "owner", "Series": "cost:Sum, hours:Average", "Kind": "Line" } } } } ``` Or people pick it from a menu, with `ViewVariant::new("chart", "Chart").editor(CHART_EDITOR)`. In code, `EntityInfo::editor_setting` writes the same settings. Every setting is optional, as what is not said follows from the kinds of the fields, and a setting that names no field says so on the screen. | Editor | Settings | |---|---| | `Chart` | `Argument`, `Series` as `field:Sum`, `Count`, `Average`, `Min` or `Max`, `Kind` as `Bar`, `Line`, `Area`, `Pie` or `Doughnut`, `Interval` as `Day`, `Month` or `Year` for a date | | `Calendar` | `Start`, `Title`, `Done`, `Tone`, an option field that picks the color | | `Scheduler` | `Start`, `End`, `Resource`, a reference with a row per record it can point to, `Title`, `Done`, `Tone` | | `Tree` | `Parent`, a reference to the same entity, `Title` | These read through the view's unit of work, so permissions, filters and the way picked all count. See `examples/quick-plan`, in a window and in the browser. ## Records that stay `App::keep_in(storage)` keeps the records and the person's own settings between runs, with no database to set up. The records stay in memory while the app runs and are written after every commit; the `seed` runs only the first time, while nothing was kept yet. ```rust,ignore // Desktop: a folder with one file for the records and one for the settings. App::new("Raven Contas").entity::().keep_in(FileStorage::new(data_folder)); // Web: the browser's IndexedDB, read before the app opens (see examples/quick-app). let storage = ravenrs::web::IndexedDbStorage::open("raven-contas").await?; App::new("Raven Contas").entity::().keep_in(storage); ``` A `Storage` (in `ravenrs-core`) keeps named pieces of data: `FileStorage` on the desktop, `IndexedDbStorage` on the web, `MemoryStorage` for tests. Under `keep_in`, `MemoryDatabase::stored_in(storage, name)` reads what was kept and writes after each commit, `was_restored` says whether something was there, and `storage_problem` says what went wrong the last time it wrote, while the records stay in memory. `StoredModel` keeps the settings the same way. `MemoryDatabase::export_json` gives every record as JSON (dates as `{"days", "seconds"}`), and `import_json` puts such JSON in place of all records, all or nothing: an entity it does not have, a record without its key or a value it cannot read leaves everything as it was. Records of an entity removed from the code are set aside when the database opens, not refused. This is for one person's records on one machine. Several people sharing records need SQLite on a shared disk or a server. ## Work away from the screen ```rust App::new("Raven Sweep") .entity::() .action(|| { Action::background("Scan", "Varrer", |job| { for (done, path) in roots().iter().enumerate() { if job.cancelled() { return Ok(()); } job.progress(done as f32 / roots().len() as f32, format!("lendo {path}")); job.save(folder_of(path)?); } Ok(()) }) .target_entity("Folder") }) ``` `App::action` adds an action with no module or controller to write. It is made again for each window, so each turns it on and off by itself. `Action::background`, or `in_background` on any action, runs its work on a thread of its own once the action runs, after its question and `on_execute`. The work sees what was selected and what people gave it, and hands back through its `JobContext`: - `progress(0.5, "lendo Caches")` says how far it got. `doing("…")` says what it is doing when it cannot tell how far. - `save` and `delete` hand back records. They are kept in the frame's unit of work, on the screen's thread, and committed as they come; a change that fails undoes the ones that came with it and is told. - `tell` shows a message, and an error it returns is shown too, as is a panic. - `cancelled()` says someone pressed Cancel. It stops by returning, and what it handed back stays. While it runs the action is off, by the reason `REASON_RUNNING`, so it never runs twice at once. The navigation shows each running job with its bar and Cancel. Lists read again as records come, and `on_executed` runs once the work ended. Nothing polls: the work wakes the screen. In the browser, where there are no threads, the work runs right away and the screen freezes while it does. ## States and reminders ```rust EntityInfo::new("Task") // ...fields, with `state` among them... .state_machine( StateMachine::new("state") .state("draft", "Draft") .state("review", "In review") .state("done", "Approved") .start("draft") .transition("draft", "review", "Send to review") .transition("review", "done", "Approve") .when("[cost] < 1000"), ) .state_appearance("state", "done", AppearanceRule::new("locked", ["cost"]).enabled(false)) .reminder(Reminder::new("remind_at").criteria("[owner] = ?CurrentUserId")) ``` Each way gets an action, on when what is selected stands where it starts. The field is read-only on forms and starts in the first state. Any other change is refused at the commit, in memory and in SQLite, with no screen around it; several saves in one unit of work count as one change, from what is stored to what is kept. An `ObjectSpace` of your own calls `check_transitions`. A record with a time in its reminder field comes up when that is due, for whoever can read it and meets the criteria, with what to do: open it, put it off 15 minutes, or dismiss it. The time is kept on the record, so it is one for everyone who sees it. Nothing runs while waiting: the screen asks to be built again at the next time due. This is not a business process engine: there is no routing, approval by several people or timing out. ## Reports and panels ```rust App::new("Shop") .entity_info(order) .module(StoredReportsModule::new(ReportWords::pt_br())) .module(AnalyticsModule::new(ReportWords::pt_br())) .translation("pt-BR", ravenrs_app_report::PT_BR) ``` A report is a record: its name, the entity it is about and its layout. A new one starts from a table of what the entity's list shows, and **Design** opens the designer inside the application, over a sample of the records. **Show** asks for its parameters and shows its pages, from where it prints and exports. A list has **Show in report**, for the records selected or all it shows. Permissions on `ReportDefinition` say who sees or changes a report, like any other record. Whoever runs one gets only what they can read: rows come from the view's unit of work, and whatever a layout says about addresses, queries or data of its own does not run. What is kept is the layout alone, with nobody's data. A panel is a record with tiles, edited on its own form: a title, an entity, a kind, what goes along the axis, values as `field:Sum`, and a filter. A tile that cannot be drawn says why, and the others still show. ## Languages and words Every word the framework shows has a key in `ravenrs-app-i18n`: `ui.save` for the screens, `report.show` for reports, `rules.required` for rules, `widgets.close` for the components, `viewer.print` for the document viewer, and so on, with `format.decimal`, `format.thousands` and `format.money_prefix` for numbers. They come in English and Brazilian Portuguese, in `crates/ravenrs-app-i18n/texts//.json`. A language without a key falls back to English, and a key nobody has shows as itself. An application picks the language and, when it wants other words, gives a flat JSON of keys. Nothing of it is code of the screens: ```rust App::new("Raven Contas") .language(Language::BrazilianPortuguese) .texts("pt-BR", r#"{ "ui.refresh": "Recarregar", "shop.hello": "Olá, {0}!" }"#) ``` What it replaces reaches every screen and every component inside them, as `Labels` comes from the same catalog. Keys of its own, for a module or an action, are read with `Session::texts()`, and `format` fills in `{0}`, `{1}`. `SecuredApp::texts` does the same for the screen of signing in and the application of each person. A module that brings words of its own ships them as a JSON constant for the application to give to `texts`. Each set of words has `from_texts(&Texts)`: `Words`, `RuleWords`, `ReportWords` (and `ReportWords::of(language)`), `Labels`, `ViewerLabels`, `ParameterLabels`, the labels of `ravenrs-movie` and `ravenrs-ai`, and `SqlWords`. `default()` is English and `pt_br()` Brazilian Portuguese, both from the catalog, so a new language is a folder of JSON. `PaneWords::with_texts` takes the words a pane shares with the others. The words of the model are a translation, not texts: `App::translation` takes a difference in JSON, and `export_captions` and `import_captions` make and read a sheet for whoever translates. The ones that come with the framework are in `ravenrs_app_i18n::model`, and `ravenrs_app_report::PT_BR` is one of them. `Catalog::missing` lists the keys a language does not have yet. In the feature center, *Textos por chave* replaces a word of every list and has an action that reads a key of the center's own. ## Security ```rust let mut entities = security_entities(); entities.push(audit_entity()); entities.push(account.clone()); let database = MemoryDatabase::new(entities)?; let window = SecuredApp::new("Accounts", move || database.space(), move |_person| { App::new("Accounts").entity_info(account.clone()) }) .role(Role::new("Sales").policy(PermissionPolicy::ReadOnlyAll).object_permission( "Account", "[owner] = ?CurrentUserName", [Operation::Write], Access::Allow, )) .audit(AuditSettings::new(AuditMode::Full)) .build()?; ``` A new installation asks for the first person, who becomes the administrator. Administrators manage users and roles on generated screens, under Administration. How a question is answered, for each role: a field by criteria, then a field, then a record by criteria, then the entity, then the owner of an aggregated collection, then the role's policy. A denial beats an allowance at the same level. Creating takes `Create` and `Write`. A write is checked against the record before and after the change, so nobody can take a record over or give one away past what they can write. `SecuredObjectSpace` is what protects records. It works on any `ObjectSpace` with no screen around it, which is what a service will use. A record that cannot be read is not there. A field that cannot be read reads as empty, also for filters, sorting and calculated fields that use it, and saving it back empty leaves what is stored. A list with a permission that depends on the record reads the whole table to answer, as the database does not know the permission. Records of users and roles are for administrators only, the audit trail can only be read, and a `secret` field is kept from everyone. What the screens hide on top of this, navigation among it, is courtesy. Keep these in mind: - **A database file on someone's machine protects nothing from whoever holds the file.** They can read it, change it and rewrite the trail with any SQLite tool. Real protection takes a server between people and the data. - The audit trail only sees what goes through it. Changes made to the database some other way are not there, and neither is what `SecuredApp::seed` writes. - A permission on an aggregated collection's owner that depends on the record cannot be told for the lines, so they stay closed until they have permissions of their own. - Five wrong passwords make an account wait 15 minutes, so someone who knows a name can keep it waiting, the only administrator included. Answers take about the same time for any name, but a wrong password on a real account also writes to storage, which careful measuring could tell. - Inserting a record with a key that exists says so, even for a record that cannot be read, so with keys that can be guessed, someone can tell which exist. GUID keys avoid this. - Who someone is, is read again at each new view, so what someone else changed about them takes a moment to count. Two administrators demoting each other at the same moment from two programs can leave nobody, as the check happens in each one's unit of work. - Whoever opens a new installation first becomes the administrator. That suits a program on someone's own machine; a server has to create its first user in its setup. - Argon2id runs on the UI thread, about a fraction of a second, when signing in or changing a password. - A web build keeps records in memory for now, so users last as long as the page. ## Accessibility Every generated application reaches screen readers and the keyboard with nothing written for them. `ravenrs-app-a11y` holds what the framework does about it, and `ravenrs-app-ui` uses it everywhere. - **Names.** A field is called by its caption, and an action by its caption, unless the declaration says otherwise: `FieldInfo::accessible_name` and `accessible_description`, `Action::accessible_name` and `accessible_description`, or the same on `#[field(...)]`. The model copies a field's words to its form item as `AccessibleName` and `AccessibleDescription`, so a translation or an administrator changes them as they change the caption. A field's description replaces its hint for screen readers; an error or a note on the field still comes first. - **Focus order.** `EntityInfo::focus_order` puts fields first on the form, and the form shows them in the same order, so the eye and Tab go the same way. A focus order that names a field that is not there stops `Application::build`. - **Shortcuts.** An action with a `hotkey` says its keys after its description. - **Announcements.** Saving says `Saved: ` and the record's name, a save that fails its rules says why at once, deleting says how many, and opening a list says its title. They come from `Session::announcer`, which an application uses for its own news, or silences with `set_quiet`. The region takes no room, and keeps the last three, so one said right after another is not lost. Messages and toasts were already statuses and alerts. - **Words.** The words of the announcements have keys under `a11y.`, in English and Brazilian Portuguese, and follow `App::language`. `AccessWords::from_lookup` takes them from wherever the application keeps its words, which is where the framework's catalog of words plugs in. ### The audit ```rust,ignore let mut ui = Ui::new(quick_app::app(), Theme::raven_dark()); ui.set_viewport(Size::new(1200.0, 800.0), 1.0); Audit::new().theme(Theme::raven_light()).open("Novo").app(&mut ui).assert_clean(); ``` `Audit::app` walks the application with no window: the screen it opens on, every item of the navigation, and on each, the buttons given to `open`. On every screen it reports a control with no name, an id that appears twice, focus on a node that is not there, Tab that does not move or goes round some controls and never reaches the others, a window over the screen that Escape does not close, and two commands on the same keys. On the theme on screen and on every theme given, it checks text and the focus ring against the WCAG AA contrast. `Audit::screen` looks at one screen, and `check_tree`, `check_focus`, `check_theme` and `check_shortcuts` are there one by one. `quick-app`, `quick-plan` and `feature-center` run it in their tests, and any example can. ## Known gaps - The audit checks the contrast of a theme's pairs of tokens, not of each pixel drawn: a color a rule or a screen chose for itself is not checked. It does not look at the size of targets, at text resized, or at the reading order against the screen. - A focus order reorders fields within their group of the form. Moving a field to another group is still done in the model. - A field has no keys of its own to reach it; shortcuts are for actions. - The words of the announcements live in `ravenrs-app-a11y` until the framework keeps all its words in one catalog. - The region of announcements is in the window, so while a window is open over it, what changes behind is heard once that window closes. - A declared action sets fields and nothing else: it does not open views, ask for input or reach other records. One that needs to does it in `on_execute`. - State machines are declared in code. People cannot define states or ways while the application runs. - A reminder someone else set or changed shows the next time records are read here, as nothing polls. Reminders do not go anywhere outside the application. - `DocumentViewer` shows what the report engine lays out. It does not open PDF files, so an attached PDF is saved or opened with the system. - Stored reports are not sent on a schedule yet: `ravenrs-report-server` reads its own sources, not entities, and a job has to run as someone. That takes the service of F12. - The designer's own words are in Portuguese whatever the application's language, and its field list shows field names rather than captions. - A report reads every record of its entity into memory, as its engine works over rows. - A chart, month, timeline or tree has no search, column filters or export, which are `DataGrid`'s own. Filters in the model still count. Settings have to name fields the list reads, so `DataView` needs them among its columns. - Moving a bar on a timeline snaps to what the axis shows, so at the scale of days a record loses its hour. - The model editor changes settings, one at a time, in the person's own layer. It does not add nodes or edit the administrator's layer. - A field a person cannot read shows as Protected on a form, but as an empty cell on a list, and its column stays. - `SecuredApp` makes an application anew at each sign-in, with storage made before that: fields a layer adds have to be declared to storage by hand, so `App::storage_for` is not for it. - Search, filters and export of a list are `DataGrid`'s own, not actions the model can move. - Background work shows its progress in the navigation of `AppShell`. A pane embedded in a screen of your own keeps what the work hands back, but shows no bar. - Background work hands back records and messages; it does not open views or ask questions. - Where an action shows comes from its category. There is no mapping of actions to places in the model yet. - Without an attachment store, a picture or file stays as text in its field, and the whole record is read with it. With one, replacing a file does not yet remove the old content from the store. - What links and unlinks in a list inside a form is kept right away, and not with the form's save. A new record has to be saved before its list opens. - A remote list searches only fields with text, and has no grouping. A column of a reference shows the name, but is not searched by it. - A reference is picked in its form, and not in a window of its own. - Used directly, `AppShell` needs `Words` given to it and to `SystemModule`, and `Localized` around it. `App` does both. - On a form, a font color from a rule reaches the caption and not the value inside the editor, and a group's caption takes no color. - Clippy's `duplicated_attributes` flags `#[appearance]` or `#[rule]` attributes that repeat a condition or a context. Allow it where they do, as a derive cannot do it for you. - Only text and number fields tell when they are left. Validation contexts do not reach into collections yet. - `register_function` is per thread, as all of an application's state is. A function of the application is not translated to SQL, so a filter with one reads the whole table. - A translation changes the model's words; `Words` and the texts follow `App::language` and do not change at run time. - `Culture` in `ravenrs-criteria`, which formats numbers and dates inside expressions and reports, is apart from the catalog. So are the words of the report designer. - A pane finds a row's key by the order of the entity's fields, so a layer that reorders its columns breaks its buttons. - Putting two people's changes together reads and then writes, in two steps. Two programs on one SQLite file can still meet between them, until there is a server. - Editing on a list needs a list that holds its records, so not `Server`, `DataView` or a given source. A cell that fails is marked, and its message shows only when it is edited. - A `display_format` on a date makes its cells text, so they sort as text. An `edit_mask` has no regular expressions: `Rule::matches` checks those on saving. - Predefined values are picked and not typed. A Rust struct does not expand into a group of fields: `group` puts plain fields together. - Fields added in the model hold plain values, not references or lists. Used without `App`, whoever opens storage has to give it `Application::entities`. ## What comes next | Step | What it brings | |---|---| | **F12** | An API over the same permissions, a remote client and other databases | Before more work on editors or templates, read what is documented on template customization and on each property type: it was not read when these screens were written. The same goes for the built-in node generators and list view column generation, before more of the model is generated. --- # Component coverage URL: https://alexkads.github.io/RavenRS/docs/components/ Source: https://github.com/alexkads/RavenRS/blob/master/docs/PARITY.md # Component parity with reference libraries RavenRS aims to be more complete than the three reference libraries. This table tracks every component family they offer and where RavenRS stands. It is updated with each batch. Legend: ✅ available · 🟡 partial (what is missing is noted) · ⬜ planned. Column S marks whether shadcn/ui ships it; D and T mark two commercial component suites for desktop and web. ## Summary | | Families tracked | ✅ | 🟡 | ⬜ | |---|---|---|---|---| | All | 122 | 82 | 7 | 33 | ## Layout and structure | Component | S | D | T | RavenRS | |---|---|---|---|---| | Stack (VStack/HStack), Spacer | | | | ✅ | | Overlap (ZStack) | | | | ✅ `ZStack`: as large as the largest layer | | Page header, property row | | | | ✅ `PageHeader`, `PropertyRow` | | Grid layout | | ✓ | ✓ | ✅ `Grid` | | Card | ✓ | ✓ | ✓ | ✅ `Card` with a selected state, `GlassCard` (blur planned) | | Separator | ✓ | | | ✅ `Divider` | | Aspect ratio | ✓ | | | ✅ `AspectRatio` | | Scroll area | ✓ | ✓ | ✓ | ✅ `ScrollView` (vertical; horizontal inside DataGrid) | | Sticky content and scroll-driven effects | ✓ | ✓ | ✓ | ✅ `Sticky`, `ScrollScene`, `ScrollLinked` with `ScrollLink` (ranges, windows, easing, keyframes) | | Links, anchors, counters, marquees | ✓ | ✓ | ✓ | ✅ `Link`, `Anchor`, `CountUp` (also counts to a new value, with even digits), `Ticker` with `TimeLink`, `QuadField` | | Resizable panels / Splitter | ✓ | ✓ | ✓ | ✅ `Splitter` for two panes, `SplitPanes` for any number with limits, `SplitHandle` for a bar of your own; all move with the keyboard | | Tile layout / Dashboard layout | | ✓ | ✓ | ✅ `TileLayout`: drag and resize, also from the keyboard; fewer columns when narrow; a layout to keep; controls in a tile's header (`Tile::accessory`) | | Docking / Dock manager | | ✓ | ✓ | ✅ `DockArea`: a tree of splits and tabs in four places; moves by a drag with a mark on the target, by keys or by a menu; bars with limits and keys; docks that collapse and panels that close and come back; an arrangement saved as versioned text, where an unknown id is left out. Floating panels are left out: the app this was modeled on does not float them either | | Window (floating) | | ✓ | ✓ | 🟡 `FloatingPanel::movable`: moves and reduces, does not resize | | App bar / Toolbar | | ✓ | ✓ | ✅ `Toolbar`, `ToolbarAction`: what does not fit folds into a menu | | Toolbar floating over content | | | ✓ | ✅ `FloatingToolbar`: translucent, placed by side, moved by a handle or the arrows | | Arrows on the ends of a scrolling strip | | | ✓ | ✅ `ScrollEdgeArrow`: only where there is more to show, with the keyboard kept at the end | | Custom title bar | | | ✓ | 🟡 `TitleBar`: title, actions and the window's buttons, drawing and calling back only. Dragging the window and hiding the system's frame belong to the platform layer, which offers neither yet | | Footer | | | | ✅ `Footer` | | Status bar | | ✓ | ✓ | ✅ `StatusBar` | | Tool rail | | | | ✅ `ToolRail`: variants in a menu at the side, lines between groups, icons only, several columns | | Timeline with tracks | | | ✓ | ✅ `Timeline` | ## Navigation | Component | S | D | T | RavenRS | |---|---|---|---|---| | Tabs / TabStrip | ✓ | ✓ | ✓ | ✅ `Tabs`: underlined or segmented, sizes, the bar alone, disabled while waiting | | Document tabs | | ✓ | ✓ | ✅ `DocumentTabs`: close, reorder by drag or keys, rename, color, a dot for what was not saved, context menu, scrolling with arrows | | Sidebar / Drawer navigation | ✓ | ✓ | ✓ | ✅ `Sidebar`: closes to icons or off the screen, a rail along its edge, a brand, keys on a row, header and footer, groups, submenus, actions, badges, skeleton, sheet in narrow windows, floating and inset variants. Drag to resize: `Splitter` | | Breadcrumb | ✓ | ✓ | ✓ | ✅ `Breadcrumb`: locations can be links, an icon at the root | | Pagination / Pager | ✓ | ✓ | ✓ | ✅ `Pagination` | | Accordion / PanelBar | ✓ | ✓ | ✓ | ✅ `Accordion`, `Disclosure`: any view as title, a mark, an on/off switch, open state kept by the app | | Menu / Menubar | ✓ | ✓ | ✓ | ✅ `Menubar` | | Ribbon (Office-style command bar) | | ✓ | ✓ | ✅ `Ribbon` (app menu, quick access, contextual tabs, collapse; key tips and gallery items planned) | | Navigation menu (mega menu) | ✓ | | ✓ | ⬜ | | Context menu | ✓ | ✓ | ✓ | ✅ `ContextMenu`: submenus, check marks and any view in a row, like `DropdownMenu` | | Stepper / Wizard | | ✓ | ✓ | ✅ `Wizard`: a step can refuse to be left or be marked with a problem; steps done can be reopened. `StepIndicator`, `StepBadge` for an app's own layout | | Bottom navigation | | | ✓ | ⬜ | | TreeView | | ✓ | ✓ | ✅ `TreeView`: keyboard, selection, three-state checks, filter, items on demand, virtualized | | Command palette | ✓ | | | ✅ `CommandPalette` | ## Buttons and actions | Component | S | D | T | RavenRS | |---|---|---|---|---| | Button (variants, sizes, icons) | ✓ | ✓ | ✓ | ✅ `Button`, `IconButton`: loading that holds the width, selected, key cap in the label's color | | Toggle button | ✓ | | ✓ | ✅ `ToggleButton`: count and pill shape | | Favorite mark | | | | ✅ `FavoriteToggle`: heart or star | | Swatch grid, color pair, nine-point anchor | | | | ✅ `SwatchGrid` (the handler gets the modifiers), `ColorPair`, `NinePointAnchor` | | Toggle group / Button group | ✓ | ✓ | ✓ | ✅ `ToggleGroup`, and `ButtonGroup` for buttons glued side by side | | Dropdown button | ✓ | ✓ | ✓ | ✅ `DropdownMenu`: submenus opened by hover, click or the Right arrow, check marks and any view in a row | | Split button | | ✓ | ✓ | ✅ `SplitButton` | | Floating action button | | ✓ | ✓ | ⬜ | | Speed dial | | ✓ | ✓ | ⬜ | | Floating action button with a panel | | ✓ | ✓ | ✅ `FloatingPanel`: a round button that drags between corners, or placed anywhere by its header with the place kept by the app; reduces to its header | ## Inputs and editors | Component | S | D | T | RavenRS | |---|---|---|---|---| | Text box | ✓ | ✓ | ✓ | ✅ `TextField` (selection, clipboard, views before and after the text, clear button, sizes) | | Text area (multi-line) | ✓ | ✓ | ✓ | ✅ `TextArea`: wraps, scrolls, builds only the lines in view, grows with the text between two limits | | Code editor | | ✓ | | ✅ `CodeEditor` (feature `code`): Fira Mono, line numbers, Rust colors by token, Tab and Shift+Tab indent, Enter keeps the indentation, diagnostics mark the line and underline the columns. No horizontal scrolling or folding yet | | Code playground | | | | ✅ `Playground` (feature `code`): one bar, code beside its result, an Output panel with Console (`println!`) and Problems (also from closures that fail later), examples that run when picked, Restore, Ctrl/Cmd+Enter runs through a `Runner`, problems marked on their lines, last result kept and faded, Code/Result/Output tabs with a ⋯ menu below 768 px, and `download` to save what the code makes. The gallery downloads each example as a Cargo project (`ravenrs-project`) that `cargo run`s on the desktop and builds for the browser; `scripts/check.sh --full` builds all of them. `LiveRunner` (feature `live`) runs edited Rust without a compiler through `ravenrs-live`: all 181 gallery examples, each building as many elements as compiled; what it cannot read yet would be listed in `examples/component-gallery/live-pending.txt` | | Label | ✓ | ✓ | ✓ | ✅ `Label` | | Form, field validation | ✓ | ✓ | ✓ | ✅ `FormField` + validators | | Form layout (groups, columns, tabs, error list) | | ✓ | ✓ | ✅ `FormLayout`, `FormGroup` | | Checkbox | ✓ | ✓ | ✓ | ✅ | | Radio group | ✓ | ✓ | ✓ | ✅ | | Switch | ✓ | ✓ | ✓ | ✅ `Toggle`, with a small size | | Slider | ✓ | ✓ | ✓ | ✅ `Slider`: marks, colored track, fill from a neutral value that a double click returns to, value at the right | | Value in small that opens a slider | | | | ✅ `ValueSlider`: a button, or a field with `field`, and the keyboard goes into the slider | | Rename in place | | | ✓ | ✅ `InlineRename`: double click, F2 or Enter; Enter and leaving confirm, Escape cancels | | Range slider | | ✓ | ✓ | ⬜ | | Select / Drop-down list | ✓ | ✓ | ✓ | ✅ `Select`: by index or by value (`Select::of`), search, sizes | | Combobox / Autocomplete | ✓ | ✓ | ✓ | ✅ `Combobox` | | Filter builder | | ✓ | ✓ | ✅ `FilterBuilder`: lines and groups over the expression language; "is any of" and "is none of" with values to pick; what it cannot show stays as text; also in the `DataGrid` toolbar and behind "Edit filter" on its filter panel, which opens every column's expression as one | | Lookup (records loaded on demand) | | ✓ | ✓ | ✅ `LookupField`: from memory or a callback, ignores stale answers | | Multi-select / Tag box | | ✓ | ✓ | ✅ `TagBox`: search, new values, a limit | | Numeric input / Spin editor | | ✓ | ✓ | ✅ `NumberField` (range, step, separators, money in whole cents), `Stepper` | | Masked input | | ✓ | ✓ | ✅ `MaskedField` | | One-time code (OTP) | ✓ | | ✓ | ✅ `InputOtp` | | Calendar | ✓ | ✓ | ✓ | ✅ `Calendar` | | Year view | | ✓ | ✓ | ✅ `YearView`: twelve months with marks per day, clicks and one Tab stop | | Date picker | ✓ | ✓ | ✓ | ✅ `DatePicker` | | Date range picker | ✓ | ✓ | ✓ | ✅ `DateRangePicker`: two months, ready-made periods | | Time picker / DateTime picker | | ✓ | ✓ | ✅ `TimePicker`, `DateTimePicker` | | Theme picker and token editor | | | ✓ | ✅ `ThemePicker`: menu of themes. `ThemeEditor`: colors, corners and text sizes by token name, applied at once, with live contrast and JSON to copy and paste | | Color picker / palette / gradient | | ✓ | ✓ | 🟡 `ColorPicker`: palette, shade square, hue and opacity bars, hexadecimal; gradients planned | | Rating | | ✓ | ✓ | ✅ `Rating`: read-only at the cost of one element, or given with the pointer, arrows, Home, End and digits | | File upload | | ✓ | ✓ | ✅ `FileUpload`, `ImageField`: the system's chooser and drops, on desktop and web; sending is up to the app | | Drop zone | | ✓ | ✓ | ✅ `DropZone`: around any content, with the rules of `FileUpload`; every zone lights up during the drag, since platforms say only that files are over the window | | Signature | | | ✓ | ⬜ | | Rich text editor | | ✓ | ✓ | ⬜ | | Code editor | | ✓ | | ⬜ | | Spreadsheet | | ✓ | ✓ | 🟡 `Spreadsheet` (`ravenrs-spreadsheet`): opens XLSX, XLS, ODS and CSV and saves XLSX, 500+ functions with recalculation, editing in the cell and the formula bar, pointer and keyboard selection, clipboard, undo, bold, italic, underline, alignment, number formats, column widths, sheet tabs. Planned: every command on a ribbon, frozen panes, charts and pictures on the sheet, conditional formats drawn, fill handle, find | ## Data | Component | S | D | T | RavenRS | |---|---|---|---|---| | Table | ✓ | ✓ | ✓ | ✅ `Table` | | Data grid | ✓ | ✓ | ✓ | 🟡 `DataGrid`; see its parity table in ARCHITECTURE.md | | Tree list / Tree grid | | ✓ | ✓ | ⬜ | | Pivot grid | | ✓ | ✓ | ⬜ | | List / List view | | ✓ | ✓ | ✅ `List`, `ListItem` | | Virtual list with rows of any height | | ✓ | ✓ | ✅ `VirtualList`: measured in layout and kept by key, jump to a row, stays at the end, still when rows enter above, keyboard and semantics of a list | | Sortable list | | ✓ | ✓ | ✅ `SortableList`: a drag with a line at the target, Alt and the arrows, announced to screen readers. It keeps every row built, so it is for lists of a screenful | | List box (with reordering) | | ✓ | ✓ | ⬜ | | Card view / Tile view | | ✓ | ✓ | ✅ `CardView`: virtualized, as many columns as fit, keyboard and selection | | Scheduler (resource timeline) | | ✓ | ✓ | 🟡 `Scheduler`; gaps listed under "Scheduler" below | | Calendar with events | | ✓ | ✓ | ✅ `EventCalendar` | | Recurrence editor | | ✓ | ✓ | ✅ `RecurrenceEditor`, `RecurrenceRule` (reads and writes iCalendar's `RRULE`) | | Gantt | | ✓ | ✓ | ⬜ | | Kanban / Task board | | | ✓ | ⬜ | | Timeline | | | ✓ | ⬜ | | File manager | | ✓ | ✓ | 🟡 `FileBrowser`: breadcrumb, sorting, selection, icon, size and date over the app's data, virtualized. No upload, tree of folders or thumbnails view | ## Charts and visualization | Component | S | D | T | RavenRS | |---|---|---|---|---| | Bar / column chart | ✓ | ✓ | ✓ | ✅ `Chart`, `BarChart`: horizontal, grouped, stacked, negative, labels, active, focus that fades the rest, gradient, least length, growing from any side, categories on the line of zero | | Area chart, sparkline | ✓ | ✓ | ✓ | ✅ `Chart`, `AreaChart`: stacked, expanded, gradient, curves; a sparkline with a tooltip and cursor when asked | | Line chart | ✓ | ✓ | ✓ | ✅ `Chart`: natural, monotone, linear and step curves, dots, icons, labels | | Pie / donut | ✓ | ✓ | ✓ | ✅ `Chart`: rings, active slice, a slice that comes forward under the pointer, center text, labels, guides with an elbow, a real gap between slices, a radius per slice | | Radar, radial | | ✓ | ✓ | ✅ `Chart`: polygon and circle grids, filled, tracks, stacked | | Chart tooltip, legend, keyboard | ✓ | ✓ | ✓ | ✅ `ChartTooltip` (share, color by value, free content, either side on both axes), `ChartLegend`, arrow keys; cursor and dots that glide and fade | | Chart entrance | ✓ | ✓ | ✓ | 🟡 `Chart`, `SankeyChart`: the data draws in the first time it is painted, with axes and grid there from the first frame: lines, areas, candles and flows from the left, bars out of zero, pies clockwise, radars out of the center; the tooltip waits; `no_appear`, `appear_key`; `animate_changes` moves from old data to new, with the axis; radial bars only fade in ⬜ | | Chart axes | ✓ | ✓ | ✓ | ✅ `Chart`: dashed grid, columns, steps of the axis, labels measured with the real font, aligned ends, lines of reference, fixed domain | | Scatter, bubble, funnel, financial | | ✓ | ✓ | 🟡 scatter in `Chart`, with a tooltip and cursor when asked; candlestick in `Chart`, with an axis of round numbers, outlines for what rose and the keyboard; bubble and funnel ⬜ | | Sankey | | ✓ | ✓ | ✅ `SankeyChart`: alignment, ribbons in a gradient, the rest fading around the focus, arrow keys, every flow read aloud, a clear message when the flows go in a circle | | Gauges (linear, circular, arc) | | ✓ | ✓ | ✅ `ProgressBar`, `Chart` with `RadialBar` | The whole [shadcn/ui chart catalog](https://ui.shadcn.com/charts) can be built with `Chart` and `ChartCard`: all 70 patterns are in the gallery's Charts page, under their names (`examples/component-gallery/src/charts.rs`). | Maps | | ✓ | ✓ | ⬜ | | Diagram / Org chart | | ✓ | ✓ | ⬜ | | QR code / Barcode | | ✓ | ✓ | ✅ `Barcode`: QR Code, Code 128, Code 39, EAN-13, EAN-8, UPC-A, ITF | ## Overlays and feedback | Component | S | D | T | RavenRS | |---|---|---|---|---| | Dialog / Alert dialog | ✓ | ✓ | ✓ | ✅ `Dialog`, `ConfirmDialog`: Escape, the backdrop and the X can each be turned off | | Sheet / Drawer (side panel) | ✓ | ✓ | ✓ | ✅ `Sheet` | | Splash screen | | ✓ | ✓ | ✅ `SplashScreen` | | Tray icon, closing to it | | ✓ | ✓ | ✅ `AppIdentity::close_to_tray` | | System notifications | | ✓ | ✓ | ✅ `SystemNotifier` | | Popover | ✓ | ✓ | ✓ | ✅ `Popover`: any placement, with or without the panel | | Tooltip | ✓ | ✓ | ✓ | ✅ `Tooltip` with a key cap, and `.tooltip()` on any view | | Hover card | ✓ | | | ✅ `HoverCard` | | Toast / Notification | ✓ | ✓ | ✓ | ✅ `Toaster`: six places, per stack or per toast, and toasts that stay until closed | | Alert / Callout | ✓ | ✓ | ✓ | ✅ `Alert`: with a compact strip | | Code snippet with copy | | ✓ | | ✅ `CodeBlock` | | Release notes | | | | ✅ `ReleaseNotes` | | Training film and player | | | | ✅ `ravenrs::movie`: `Recorder`, `MoviePlayer`, with click and key sounds, a voice and boxes that explain each step | | Question that opens a film at the right moment | | | | ✅ `MovieAssistant`, `MovieLibrary`: words the films hold, no model | | Film editor with timeline | | | | ✅ `MovieEditor`; its preview has no sound | | Bug report with a film | | | | ✅ `FlightRecorder`, `BugReporter` | | Progress bar | ✓ | ✓ | ✓ | ✅ `ProgressBar`: label, percentage, detail, height, color | | Loader / Spinner | | ✓ | ✓ | ✅ `Spinner` | | Lottie animation | | ✓ | ✓ | ✅ `Lottie`; no expressions, text layers, 3D, blend modes or effects other than Fill yet, and `.lottie` archives need unpacking first | | Skeleton | ✓ | ✓ | ✓ | ✅ | | Badge | ✓ | ✓ | ✓ | ✅ | | Chip / Chip list | | ✓ | ✓ | 🟡 `Badge` (tone or own color, outline, height); removable chips via `Button` with an icon | ## Media and content | Component | S | D | T | RavenRS | |---|---|---|---|---| | Avatar | ✓ | ✓ | ✓ | ✅ `Avatar`: initials or picture, with a ring | | Icon set | ✓ | ✓ | ✓ | ✅ 200 Lucide icons | | Image | | ✓ | ✓ | ✅ `Image`: dissolves into the next one if asked, and asks for frames only meanwhile | | Carousel / Scroll view | ✓ | ✓ | ✓ | ✅ `Carousel` | | Kbd | ✓ | | | ✅ takes the color of what it is in, on request | | List of shortcuts | | | | ✅ `ShortcutSheet`, `ShortcutHintBar`, over `Kbd` and `Dialog` | | Empty state | ✓ | | | ✅ `EmptyState`, over `IconTile` | | Status dot, badge on a corner | | | ✓ | ✅ `Dot`, `OverlayBadge` | | Banner, stacked meters, text with a shine | | | | ✅ `HeroBanner`, `MultiProgress`, `ShimmerText` (still with reduced motion) | | Day divider in a list | | | | ✅ `DaySeparator` | | Typography scale | ✓ | ✓ | ✓ | ✅ `Text` styles | | PDF viewer | | ✓ | ✓ | 🟡 `PagedDocumentView`: bar, zoom and virtual pages, with pictures from the app. Reading the PDF and searching it are up to the app | | Filmstrip | | | | ✅ `Filmstrip`: virtual, cells from the app, selection with modifiers, keyboard, arrows on the ends | | Histogram | | | | ✅ `Histogram`: channels from ready-made bins, added up as light, linear or logarithmic, with marks at clipped ends | | Ruler | | | | ✅ `Ruler`: horizontal or vertical, in the units of the caller, with a round step for the zoom, origin and marker; vertical numbers stack, since text does not rotate | | Color wheel | | | | ✅ `ColorWheel`: hue and saturation on a disc that is computed once and shared, with arrow keys and Shift to keep the hue | | Curve editor | | | | ✅ `CurveEditor`: points over a monotone curve, added with a press, removed with a double click or Delete, moved with the arrows; baseline and histogram behind | | Zoom control | | | | ✅ `ZoomControl`: fit, 1:1, percentage, + and −, through a `ZoomView` shared with `ZoomPanView` and `Minimap` | | Zoom and pan | | | | ✅ `ZoomPanView`: pinch, Cmd/Ctrl with the wheel, wheel and drag to move, double click, keys, inertia that respects reduced motion, limits. Content is an image or a closure: the runtime has no transform for a subtree | | Minimap | | | | ✅ `Minimap`: a small copy with the rectangle of what is in view, by the same `ZoomView` as `ZoomPanView`, with drag and arrow keys | | Composition grid | | | | ✅ `RuleOfThirdsGrid`: thirds, equal divisions, squares or the golden ratio, in a box or over a `ZoomPanView`, taking no input | | Rubber band selection | | | | ✅ `RubberBand`: a rectangle drawn with a drag in `Binding`, selected on release, with the keyboard; costs no builds while dragged if nobody else reads it | | Selection outline | | | | ✅ `Marquee`: marching ants around a rectangle, a path or loose segments, which stop and ask for no frames with reduced motion | | Transform box | | | | ✅ `TransformHandles`: eight handles, turn and pivot over a `BoxTransform` in content units, with Shift and Alt as in image editors and the keyboard. It returns geometry and turns nothing, since the renderer has no transforms; `CursorIcon` has no diagonals, so corners show the hand | | Protractor | | | | ✅ `Protractor`: a band of ticks along an arc that sets an angle by a drag from anywhere, a double click back to zero, keys and value | | Crop | | | | ✅ `CropOverlay`: on `TransformHandles` and the lines of `RuleOfThirdsGrid`, with a veil outside, fixed proportion and angle, returning rectangle and angle. The frame turns over a still picture, since the renderer has no transforms | | Eyedropper | | | | ✅ `Eyedropper` and `EyedropperArea`: an armed button that takes the color of the point clicked, which the caller supplies with a closure or `EyedropperState::pick`; Esc disarms. It does not capture the screen, and `CursorIcon` has no dropper, so the cursor is the hand | | Compare images | | | | ✅ `CompareView`: two images side by side or with a divider dragged or moved with the arrows, through `ImageQuad.uv`, with captions and an optional chosen side | | Image lightbox | | | | ✅ `ImageLightbox`: a picture over the whole app with zoom by `ZoomPanView`, arrows, Esc, caption and counter, from `Labels` | | Image editor | | | ✓ | ⬜ | | Text with marks and links | | | | ✅ `RichText`: `*bold*`, `_italic_`, `~struck~`, `` `code` ``, fenced blocks, lists, quotes and web addresses as links with click and keyboard, wrapped word by word. `ravenrs-text` has one style per text and no italic or monospace font, so italics are told apart by a muted color and code by a chip | | Attachments of a message | | | | ✅ `AttachmentStrip`: icon or thumbnail, name that gives way, size, a menu for how to send it when there is a choice and a button to remove it; the app holds the list | | Audio player | | | | ✅ `AudioPlayerButton`: play and stop over `SoundProvider::speak_clip`, progress by the clock, drawn without rebuilding, audio the app can deliver after the press, and a transcript to open or ask for. A new recording sends the old one back to rest. It has no seek, since the service does not report or change where it is | | Bar over a multiple selection | | | | ✅ `SelectionBar`: the count from `Labels`, a way out by button or Esc, and actions over `Toolbar`, which go to a menu when they do not fit and wait for something chosen | | Chat | | ✓ | ✓ | ✅ `Chat`, with marks and links by `RichText` and a photo, a document or a recording in the bubble by `ChatMedia`; `MovieChat` over training films | | Choosing local AI models | | | | ✅ `ModelPicker`; `ravenrs_ai::ModelManager` offers only what fits | ## Reporting | Component | S | D | T | RavenRS | |---|---|---|---|---| | Report engine (bands, groups, summaries, expressions) | | ✓ | ✓ | 🟡 `ravenrs-report`; see [REPORTING-PARITY.md](REPORTING-PARITY.md) | | Document viewer (pages, zoom, search, thumbnails) | | ✓ | ✓ | ✅ `ravenrs-report-viewer` | | End-user report designer | | ✓ | ✓ | 🟡 `examples/report-designer`, [live](https://alexkads.github.io/RavenRS/designer/) | | Export: PDF, XLSX, DOCX, CSV, HTML, images | | ✓ | ✓ | 🟡 `ravenrs-report-pdf`, `ravenrs-report-xlsx` (XLSX and CSV), SVG per page, Excel, PDF and CSV from `DataGrid`; DOCX, HTML and PNG planned | ## Platform capabilities | Capability | S | D | T | RavenRS | |---|---|---|---|---| | Themes (light, dark, custom tokens) | ✓ | ✓ | ✓ | ✅ Raven Dark, Light, Glass | | Keyboard navigation and focus | ✓ | ✓ | ✓ | ✅ | | Screen reader support | ✓ | ✓ | ✓ | ✓ AccessKit on desktop, ARIA mirror on the web | | Right-to-left | ✓ | ✓ | ✓ | ⬜ (Parley supports bidi; layout mirroring planned) | | Localization (formats, strings) | | ✓ | ✓ | 🟡 number formats; `Labels` and `Localized` for component text (Ribbon, Carousel and InputOtp still fixed) | | Animations | ✓ | ✓ | ✓ | ✅ springs, transitions, shared layout, enter and exit; 🟡 no rotation or group opacity yet | | Native desktop and browser from one codebase | | | | ✅ (none of the three does both) | ## Scheduler The reference is [Bryntum Scheduler](https://bryntum.com/products/scheduler/). The behavior is the model and the implementation is our own. | Capability | Status | |---|---| | Rows of resources, time axis with two header rows, bars by time | ✅ | | Hours, days, weeks and months, with zoom keeping the same time | ✅ | | Drag along time and between rows, resize by the ends, draw on an empty stretch | ✅ | | Snap to the scale, refusal through `can_drop`, locked and fixed-length events | ✅ | | Overlapping events on lanes, with the row growing | ✅ | | Rows sharing the height left over (`fill_rows`) | ✅ | | A full look: tinted bars with a stripe, a second line on bars, avatars, today's column (`card`, `event_detail`, `avatars`) | ✅ | | Weekend shading, marked time ranges, a line at now, opening at now | ✅ | | Collapsible groups of resources | ✅ | | Keyboard for events; names and dates for screen readers | ✅ | | Only what is in view is built | ✅ tested with 5,000 rows | | Recurring events | ✅ `RecurrenceRule`; the app expands the days | | Scrolling the wheel over the resources column | ✅ | | Scrolling by itself while dragging near an edge | ✅ | | Tooltip while hovering over or dragging an event | ✅ with `card` | | Scrolling a bar into view when keyboard focus moves to it | ✅ | | Dependencies between events | ⬜ | | Resource tree of any depth, multiple assignment, extra columns | ⬜ | | Working hours, non-continuous axis, vertical mode, nested events | ⬜ | | Summaries and histograms per row or period | ⬜ | | Dragging from a list or grid onto the timeline, or between schedulers | ⬜ | | Menus on events and on empty stretches (`event_menu`, `lane_menu`), zoom with a pinch or Ctrl and the wheel | ✅ | | Undo and redo | 🟡 the app keeps the moves, as in Raven Finanças' agenda; none built in | | Built-in event editor | ⬜ | | Export to PDF, PNG, Excel and ICS, printing | ⬜ | --- # Create reports URL: https://alexkads.github.io/RavenRS/docs/reporting/ Source: https://github.com/alexkads/RavenRS/blob/master/docs/REPORTING.md # RavenRS Reporting — design and roadmap > Status: phase R1 done, 2026-11-01. RavenRS Reporting turns data into paginated documents. It covers what a complete commercial reporting suite covers: a report engine, a document viewer, exports, an end-user report designer and a report server. Everything is in Rust and runs on desktop, in the browser (WebAssembly) and on servers. ## Goals * **One engine everywhere.** The same report definition renders identically in the desktop viewer, the browser viewer, a PDF on a server and a test. * **Reports are data.** A report is a JSON document (`*.rvreport.json`) that can be designed visually, stored in a database, versioned in git and generated by code or AI. * **Pixel-exact output.** Layout is computed once into a `Document` of positioned primitives in points. Every exporter and the viewer draw that same document. * **Commercial depth.** The bands, expressions, summaries, master-detail and controls a business report needs (invoices, statements, labels, ledgers, dashboards), with tests and performance budgets like the rest of RavenRS. ## Architecture ``` report definition (JSON or Rust builder) data (JSON, Rust structs, SQL, CSV/XLSX) │ │ ▼ ▼ ravenrs-report ──┬─ ravenrs-criteria ── data binding ── grouping/sorting/summaries └─ ravenrs-graphics ── vector shapes, chart layout, barcodes │ ▼ pagination (Parley text measurement, keep-together, page breaks, can-grow/shrink) Document (pages of positioned text, rects, lines, paths, images, links; in points) │ ├── ravenrs-report-export One entry point for all of these: format, file name, media type ├── ravenrs-report-pdf PDF 1.7 / PDF/A-2b, embedded font subsets, links, outline (pdf-writer + subsetter) ├── ravenrs-report-xlsx XLSX (rust_xlsxwriter), CSV, HTML, SVG/PNG ├── ravenrs-report-viewer DocumentViewer and ParameterPanel: pages, zoom, thumbnails, search ├── ReportDesigner (on the Ribbon): bands, field list, properties, drag and drop, undo/redo └── ravenrs-report-server HTTP: POST a definition + data, get PDF/XLSX back ``` Today `ravenrs-report`, `ravenrs-criteria`, `ravenrs-graphics`, `ravenrs-report-export`, `ravenrs-report-pdf`, `ravenrs-report-xlsx` (XLSX and CSV) and the designer in `examples/report-designer` exist. `ravenrs-report-server` runs them headless ([docs/REPORT-SERVER.md](REPORT-SERVER.md)). `ravenrs-report-viewer` shows documents and asks for parameters. The other exporters arrive in the phases below. `ravenrs-report` has no GPU or window dependency, so it runs in servers and tests. Text is measured with the same Parley stack as the UI, so the viewer and the PDF agree to the point. ## Report model * **Page.** Size (A4, Letter, Legal, custom), orientation, margins, watermark (text or image). * **Bands.** * Report: Report Header and Report Footer. * Page: Page Header and Page Footer. * Groups: Group Header and Group Footer, nested, each with its group field and sort. * Detail. * Detail Report: master-detail with its own bands, related by key. * Band behavior: * Keep-together, page break before and after. * "Repeat on every page" for headers. * Print on first page / last page. * **Controls.** * Text and data: * Label, with static text or an expression. * Rich Text: simple markup — bold, italic, links, lists. * Check Box. * Page Info: page `n` of `N`, date, user. * Containers: * Table, with rows, cells and spans. * Panel. * Sub-report. * Cross-band line and box. * Table of contents. * Graphics: Picture (PNG, JPEG, SVG), Line, Shape. * Barcodes: Code 128, Code 39, EAN-13, EAN-8, UPC-A, ITF, QR, Data Matrix, PDF417. * Data visualization: * Chart: bar, line, area, pie, doughnut, scatter. * Sparkline. * Gauge. * Cross Tab (pivot). * **Expressions.** The [`ravenrs-criteria`](../crates/ravenrs-criteria) language, reusable by other modules: * Fields and parameters: `[Price] * [Quantity]`, `?Customer`. * Conditionals: `Iif([Total] > 1000, 'VIP', 'Regular')`. * Aggregates: `Sum([Total])`, `Count()`, `Avg`, `Min`, `Max`, `RunningSum`, scoped to group, page or report. * Text and dates: `FormatString('{0:C}', [Total])`, `Today()`, `AddDays`, `Concat`, `Upper` and others. * Expressions drive visibility, formatting rules (conditional styles), sorting, filtering and calculated fields. * **Parameters.** * Kinds: typed values (text, number, date, boolean, list). * Defaults, required flags and multi-value parameters. * The viewer shows a parameter panel. * **Styles.** Named styles, odd/even rows, conditional formatting. Built-in themes follow the RavenRS tokens and print well. * **Localization.** Number, date and currency formats per culture (pt-BR first-class); report strings can be translated. ## Data 1. **JSON and Rust structs** (anything `serde::Serialize`), with nested arrays for master-detail. 2. **SQL:** PostgreSQL, MySQL and SQL Server. Each source has queries with parameters, and the report binds to their results. This lives behind a `sql` feature, so the WebAssembly build stays small. 3. **CSV and Excel files.** 4. **Federation:** join several sources and define master-detail relationships between them. ## Delivery phases Each phase ends with tests, docs, demos and `scripts/check.sh` green, like every RavenRS feature. [REPORTING-PARITY.md](REPORTING-PARITY.md) tracks every feature of a complete reporting suite against these phases. | Phase | Delivers | Status | |---|---|---| | **R1 — Engine and designer preview** | Report model, JSON format with data sources, Rust builder, expressions, parameters, bands, groups, sorting, filtering, summaries, master-detail, pagination, Label/Table/CheckBox/Line/Shape/Panel, styles, pt-BR formats, table wizard, web designer with preview | ✅ 10,000 rows in 80 ms native, about 200 ms in the browser | | **R2 — PDF and viewer** | PDF export with embedded fonts, links and outline; pictures; viewer with thumbnails and search; full parameter panel (lookups, multi-value); the remaining expression functions | In progress: PDF with embedded fonts, links and outline, and pictures, done (305 pages in about 120 ms, 1 MB); XLSX and CSV moved up from R3 (the same report in about 180 ms). Viewer with thumbnails and search, and a parameter panel, done. Parameter lookups and multi-value wait for the next `YY.1`, since they change a public struct | | **R3 — Server, scheduling and more output** | `ravenrs-report-server` (see below); XLSX, CSV, HTML, PNG export; barcodes; charts and sparklines; subreports; table of contents; watermarks; multi-column; the remaining band options | In progress: server with storage, render on demand, cron jobs, email and folder delivery, history, retries and retention done; barcodes done, except Data Matrix and PDF417; charts and line sparklines done | | **R4 — Designer depth** | Expression editor, group and sort panel, snap lines, align and size tools, multi-select, labels wizard, style gallery, drill-down and interactive sorting in preview | Planned | | **R5 — Enterprise data and documents** | SQL, CSV/XLSX and federated sources; Cross Tab; DOCX; PDF/A, PDF/UA, signatures; vertical bands; right-to-left; AI prompt-to-report | Planned | ## Server and scheduled delivery `ravenrs-report-server` runs reports headless with the same engine and layouts: * **Storage.** Layouts (`*.rvreport.json`, data sources included) are stored by the server and versioned; opening and saving them from the designer is planned. * **Data at run time.** * The server resolves `url` data sources when a report runs, only from hosts in its configuration; `sql` sources are planned. * Connections and secrets live in the server's configuration, referred to by name, and never in a layout. * **Render on demand.** An HTTP API: `POST /render` with a layout (or its name), parameters and a format returns PDF or XLSX. * **Scheduled jobs**, following the job model report servers use: * A cron-like schedule. * A report and its parameter values, fixed or by expression (for example, last month). * Recipients, individually or on one shared email. * Delivery as a PDF, XLSX or CSV attachment or a file in a folder; inline HTML and a link are planned. * A skip-if-empty option and document retention. * A run history of who received what, and retries on failure. ## Decisions * **Logic that more than one engine can use lives in a shared crate.** `ravenrs-criteria` (expressions), `ravenrs-graphics` (shapes, charts, barcodes) and `ravenrs-report-export` (files) know nothing about who calls them, so reports, the UI and later modules cannot drift apart. * **Own engine, not a port.** The model uses the concepts report authors already know (bands, criteria-style expressions) so they feel at home, but the format is JSON and the code is ours. * **`pdf-writer` + `subsetter`** (MIT/Apache, used by Typst) instead of a homemade PDF serializer: proven output, small, no C dependencies. * **`rust_xlsxwriter`** (MIT/Apache) for XLSX. * **XLSX cells come from the `Document`, not the data.** A text box keeps the value behind its text, so a spreadsheet gets real numbers and dates on a grid that matches the page. Pages flow into one sheet; the page header prints once and page footers, whose page numbers would mean nothing there, are left out. * **Points (1/72 in) as the document unit;** the viewer scales to the screen. * **Layout separate from rendering:** the paginator produces a `Document` once, and the viewer and every exporter consume it, so they cannot drift apart. ## Showing pages elsewhere `PageFan` (in `ravenrs-report-viewer`) shows real pages of rendered documents spread like a hand of cards, for pages that present reporting: each page is painted once with `paint_page` and reused, opens as the fan scrolls into view, comes forward under the pointer or keyboard focus, and opens its address when clicked. --- # Report server URL: https://alexkads.github.io/RavenRS/docs/report-server/ Source: https://github.com/alexkads/RavenRS/blob/master/docs/REPORT-SERVER.md # Report server `ravenrs-report-server` runs reports without a UI. It stores layouts with versions, renders them over HTTP, and sends scheduled reports by email or to a folder as PDF, XLSX or CSV. It uses the same engine and exporters as the designer, so a report comes out the same everywhere. ## Run it Put the configuration in a file: ```json { "listen": "127.0.0.1:8090", "dataDir": "/var/lib/ravenrs-reports", "outputDir": "/srv/reports", "smtp": { "host": "smtp.example.com", "from": "Reports ", "usernameEnv": "SMTP_USER", "passwordEnv": "SMTP_PASSWORD" }, "dataHosts": [ { "host": "api.example.com", "headersEnv": { "Authorization": "ERP_API_KEY" } } ] } ``` Secrets stay in the environment: the file holds only the names of the variables. ```bash export RAVENRS_SERVER_TOKEN="$(openssl rand -hex 32)" ravenrs-report-server --config server.json ``` Try it with a sample layout: ```bash cargo run -p ravenrs-report-server --example sample_layout > invoice.json ``` ```bash curl -X PUT -H "Authorization: Bearer $RAVENRS_SERVER_TOKEN" --data-binary @invoice.json http://127.0.0.1:8090/layouts/invoice ``` ```bash curl -X POST -H "Authorization: Bearer $RAVENRS_SERVER_TOKEN" -d '{"name":"invoice","format":"pdf"}' -o invoice.pdf http://127.0.0.1:8090/render ``` | Setting | Default | What it does | |---|---|---| | `listen` | `127.0.0.1:8090` | Address and port | | `dataDir` | required | Where layouts, jobs, history and kept files live | | `apiTokenEnv` | `RAVENRS_SERVER_TOKEN` | The variable with the API token, of at least 16 characters; the server does not start without it | | `outputDir` | none | The folder under which `folder` deliveries write | | `smtp` | none | The mail server: `host`, `port`, `security` (`starttls`, `tls` or `none`), `from`, `usernameEnv`, `passwordEnv` | | `dataHosts` | none | The hosts `url` data sources may read, with headers taken from the environment | | `maxConcurrentRenders` | 4 | Reports rendered at the same time | | `retryDelaySeconds` | 60 | Wait before the first retry; it doubles on each one | | `misfireGraceMinutes` | 60 | How late a run may still happen after the server was down | | `historyLimit` | 1000 | Runs kept per job | | `maxBodyMegabytes` | 32 | Largest request body | ## Jobs `PUT /jobs/{id}` with: ```json { "layout": "sales", "schedule": "0 8 * * MON", "timezone": "America/Recife", "parameters": { "Region": "Sul", "From": { "expression": "AddMonths(Today(), -1)" } }, "formats": ["pdf", "xlsx"], "deliveries": [ { "type": "email", "to": ["Ana ", "bruno@example.com"], "separately": true, "subject": "{report} - {date}", "body": "Attached is the report for {date}." }, { "type": "folder", "path": "sales/weekly" } ], "skipIfEmpty": true, "retries": 2, "retentionDays": 30, "enabled": true } ``` * **`schedule`** is five cron fields: minute, hour, day of month, month, day of week. It takes `*`, lists, ranges, steps and names (`MON`, `JAN`). When both day fields are restricted, a day matches if either does. * **`timezone`** applies to the schedule and to `Today()`. On a daylight saving change, a local time that clocks skip runs shifted forward, and one that clocks repeat runs once. * **`parameters`** are fixed values, or expressions in the [criteria language](../crates/ravenrs-criteria) worked out at each run. * **`separately`** sends one message per recipient, so they do not see each other. * **`retries`**: a retry renders nothing again and sends only to who did not receive the files. * **`retentionDays`** keeps each run's files on the server for that long; `0` keeps none. The history stays. A job always renders the latest version of its layout. The history records which version it was. | Run status | Meaning | |---|---| | `success` | Everything was delivered | | `skippedEmpty` | The report had no rows and `skipIfEmpty` is on | | `partial` | Some deliveries worked and others did not, after the retries | | `failed` | Nothing was delivered | | `missed` | Too late to run after the server was down, or the previous run was still going | ## API Every route but `/health` needs `Authorization: Bearer `. | Route | What it does | |---|---| | `GET /health` | Whether the server is up | | `POST /render` | Renders now. Body: `name` (with an optional `version`) or `layout`; `format`; optional `parameters`, `data`, `timezone`. Answers with the file; `x-report-warnings` counts problems in the report | | `GET /layouts` | Layout names | | `PUT /layouts/{name}` | Saves a layout; identical content adds no version. Answers with the version | | `GET /layouts/{name}` | The latest version, or `?version=n` | | `GET /layouts/{name}/versions` | Versions and sizes | | `DELETE /layouts/{name}` | Deletes all versions; refused while a job uses it | | `GET /jobs`, `GET /jobs/{id}` | Jobs with their next run | | `PUT /jobs/{id}` | Saves a job after checking it. Answers with its next run | | `DELETE /jobs/{id}` | Deletes a job with its history and files | | `POST /jobs/{id}/run` | Runs it now, also when disabled | | `GET /jobs/{id}/runs` | History, newest first; `?limit=n`, at most 500 | | `GET /jobs/{id}/runs/{run}` | One run: who received what, errors, report warnings | | `GET /jobs/{id}/runs/{run}/documents/{file}` | A kept file | ## Data * **`json`** sources come from the layout, and `POST /render` can send `data` instead. * **`url`** sources are fetched at each run. `{?Name}` inserts a parameter value, encoded. Only hosts in `dataHosts` can be reached and redirects are not followed, so a layout cannot make the server read its internal network. * **`sql`** sources are not supported yet, and a run that uses one fails saying so. ## Operating it * **One identity.** The API token gives full access: there are no users or per-report permissions yet. Anyone with it can read every layout and history, and send email through your server to any address. * **Keep it private.** It listens on the loopback interface by default and speaks plain HTTP. To reach it from elsewhere, put it behind a proxy with TLS. * **One instance.** Runs are not coordinated across processes, so two servers on one `dataDir` would send everything twice. * **At most once.** The next run is saved before a run starts, so a crash cannot repeat a delivery. The run under way when the server stops is not resumed and leaves no history entry. * **Back up `dataDir`.** It holds everything, as plain files. ## Not built yet SQL data sources, email with the report inline as HTML, delivery of a link instead of the file, opening and saving layouts on the server from the designer, and users with permissions. --- # Quality & accessibility URL: https://alexkads.github.io/RavenRS/docs/quality/ Source: https://github.com/alexkads/RavenRS/blob/master/docs/QUALITY.md # Quality bar RavenRS components are meant to be used in production applications, at the level users expect from commercial component vendors. This page defines what "done" means for a component, which checks enforce it, the performance budgets, and where each component stands today. ## Definition of done for a component A component is ✅ in [PARITY.md](PARITY.md) only when all of these hold: 1. **API.** Typed builder methods with defaults that look right without configuration. Values come in through `Binding`, events go out through closures. Every public item has rustdoc, and the type has a runnable example. Growable enums are `#[non_exhaustive]`. 2. **States.** Rest, hover, pressed, focused (keyboard ring), disabled, selected or checked where it applies, empty, loading and error where it applies, and overflow (long text, many items, a tiny viewport). 3. **Screen readers.** It sets a role and state with `Semantics`, and every control has a name: from its text, a `label`, a `FormField` or a `Tooltip`. The gallery audit (`examples/component-gallery/tests/accessibility.rs`) fails on any unnamed control. 4. **Keyboard.** Reachable with Tab. Enter and Space activate. Arrow keys, Home and End follow the [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/) pattern for that widget. Escape closes overlays. 5. **Themes.** Built only from semantic tokens, so it is correct in Raven Dark, Light and Glass. Text meets WCAG AA contrast, which `ravenrs-style` tests for every theme. 6. **Edge cases.** Empty data, a single item, huge counts, non-ASCII text, out-of-range values (clamped, never panicking) and a zero-size viewport. 7. **Performance.** Meets the [performance contract](#performance-contract) and stays inside the budgets below. Large collections are virtualized. 8. **Tests.** At least one behavior test that drives it through `Ui` (pointer and keyboard). Property tests for any pure logic. A golden image for any component whose drawing is non-trivial. 9. **Docs and demo.** A section in the [component gallery](../examples/component-gallery) that shows its variants and states. ## Enforced checks `scripts/check.sh --full` runs every check below, and nothing ships unless it passes. The project uses no hosted CI, so this script is the gate. Without `--full` it is the quick check for each pull request: rustfmt, and clippy and the tests of the packages the change reaches (`scripts/changed-packages.py`). | Check | What it catches | |---|---| | `cargo fmt --check` | Formatting drift | | `cargo clippy -D warnings` on native and `wasm32` | Lints, including `missing_docs` on every public item | | `cargo test --workspace` | Unit, behavior, accessibility, property ([proptest](https://docs.rs/proptest)) and doc tests | | GPU golden-image tests (`crates/ravenrs/tests/visual.rs`) | Visual regressions in 6 scenes × dark and light; tolerance 24/255 per channel on at most 0.4% of pixels | | GPU pixel tests (`crates/ravenrs-render/tests/offscreen.rs`) | Renderer correctness: shapes, clipping, images | | `cargo doc -D warnings` | Broken intra-doc links | | `cargo deny check` | Vulnerable, yanked or unlicensed dependencies, and unknown sources | | `cargo +1.88 check` | The declared minimum Rust version (MSRV) still builds | | `--no-default-features` | Optional features really are optional | | `cargo bench --no-run`, only with `RAVENRS_GATE=1` | Benchmarks keep compiling. It runs when asked for, with the performance gate | | `cargo run --release -p ravenrs-perf -- gate`, only with `RAVENRS_GATE=1` | Performance regressions: the exact work of every scene against `perf/baseline.json`. It runs when asked for, not on every check | | Web builds | The examples build and bind for WebAssembly | GPU tests skip themselves, with a message, on machines without a GPU adapter. To accept an intended visual change, regenerate the golden images with `RAVENRS_UPDATE_GOLDEN=1 cargo test -p ravenrs --test visual`, then review the PNG diff in the commit. A failing run writes `*.actual.png` next to the golden image. Release checks, run by hand before each publish: - `cargo semver-checks` against the previous release. - `cargo llvm-cov` for coverage, at 75% line coverage as of 0.1. Coverage is a signal, not a target. ## Applications built with RavenRS The rules above that a machine can check also bind applications, through [`ravenrs-lint`](../crates/ravenrs-lint/README.md). It reads an application's syntax tree and denies, like the compiler: literal colors and font sizes, spacing off the 4px grid, button captions without a verb, icon buttons without a name, more than one primary button in a view, `State::new` and blocking work in `body` or `build`, and files over 600 lines. Every rule is an error, and every exception needs a written reason. `cargo ravenrs-lint --init` adds the strictest rustc and Clippy levels, the test that runs the rules on `cargo test`, and the instructions coding agents read; `cargo ravenrs-lint --gate` is the one command that says the work is done. A rule a machine can check goes into `ravenrs-lint`, not only into this page or the skill. ## Performance method Optimizing guarantees nothing; a gate that fails on regressions does. There are two instruments, and [the skill](../.claude/skills/ravenrs-desempenho/SKILL.md) is the runbook for both. **The gate** counts the work of each scene with a real runtime and a real renderer, offscreen: builds, layouts, placements, paints, shaping, allocations in the UI, draw calls and why they split, instances, bytes uploaded, glyphs rasterized, atlas resets. These numbers are exact, so any difference from `perf/baseline.json` fails, improvements included: they have to be accepted with `gate --bless`, which shows up in the diff. Each run repeats itself and fails if the two passes disagree, and it fails without a GPU. On the screens of real applications (`app-*`), allocations are only reported, since they depend on the app's data and the clock. Ceilings in the scene registration keep repeated blessing from eroding the budget. Outside macOS, only ceilings bind. `scripts/perf-plant.py` proves it works by planting regressions. **Frame time** is measured apart, before each release, with `scripts/perf-time.sh`: the best p99 of three processes, because noise on a laptop almost only adds time. On battery the result is invalid. `ravenrs-perf cold` measures what warm numbers hide, the first time a screen opens. Budgets for the reference machine, a 14" MacBook Pro with M2 Pro and a 120 Hz screen, at p99: | Class | CPU | GPU | |---|---|---| | Interaction: hover, scroll, animation | 4 ms | 3 ms | | Rebuild: new data, opening a screen, resizing | 16.67 ms | 3 ms | | Idle | no frames, no wakes | | Time is guaranteed only for the registered scenes on that machine. Counters reflect the same logic everywhere, but a latency on another machine does not follow from them. The system compositor is outside the measurement, and Metal's clock is coarse: short passes often read as zero. ### What was measured On the reference machine at 1280x800 at 2x, with nothing else running. CPU time in milliseconds at p99, per frame: | Scene | Elements | Hover before | Hover now | Scroll before | Scroll now | Instances before | Instances now | Draw calls before | Draw calls now | |---|---|---|---|---|---|---|---|---|---| | `curve-small` | 105 | 0.90 | 0.36 | 0.05 | 0.05 | 684 | 485 | 61 | 44 | | `curve-medium` | 1,005 | 5.48 | 2.71 | 0.44 | 0.08 | 6,804 | 482 | 601 | 43 | | `curve-large` | 10,005 | 59.81 | 30.72 | 5.70 | 0.66 | 68,904 | 482 | 6,001 | 43 | | `data-grid` | 210 | 1.18 | 0.85 | 1.25 | 0.85 | 644 | 526 | 71 | 62 | | `scheduler` scroll | 711 | | | 4.56 | 1.41 | 808 | 696 | 452 | 389 | | `app-finance-dashboard` | 320 | 1.91 | 1.24 | 0.34 | 0.19 | 1,041 | 802 | 116 | 101 | | `app-finance-entries` | 423 | 1.82 | 1.11 | 1.82 | 1.13 | 1,293 | 1,237 | 118 | 116 | The "before" column was measured with the same scenes before the work on the core. A Lottie playing went from a rebuild of the whole screen per frame to none. The finance dashboard opening for the second time went from 25.4 ms to 1.1 ms in its worst frame. The `calculator` scene measures `examples/calculator` as it is, a small screen that stays the same between versions. A click on it, on the same machine: 0.16 ms on the CPU, 0.13 ms recording the GPU's work, 41 instances in 41 draw calls. Where things stand: - **Scrolling and animating no longer depend on the size of the screen.** Only what can be seen is painted and sent to the GPU. - **A rebuild still follows the size of the screen.** About 2.7 µs per element, and layout is now 75% of that. The 4 ms budget holds up to about 1,500 elements. The real screens measured have 200 to 420. Beyond that, the next step would be invalidation by subtree, which changes the API of `View` and `BuildCx`. - **The GPU is not the bottleneck.** Under 2 ms in every scene. - **Batching does little on small screens.** Text and shapes alternate, so the calculator needs one draw call per instance. It costs nothing visible today, and painting order would have to be kept by any fix. - **Resizing a window with charts.** Bars and gridlines are quads now, with no picture to draw or send; a line with more points than columns draws only what can be told apart; and a picture covers only what it draws. The `chart` scenes, dragging the window to a new width each frame, p99 of CPU and of recording the GPU's work, before and after: | Scene | CPU before | CPU now | Recording before | Recording now | |---|---|---|---|---| | `chart`, a line of 5,000 points | 26.0 | 13.9 | 7.2 | 5.7 | | `chart-bars`, three series of 12 bars | 5.5 | 0.1 | 6.3 | 0.1 | | `chart-board`, area, bars, doughnut and line | 7.2 | 5.5 | 4.7 | 3.2 | | `chart-sankey`, ten flows between ten nodes, as quads and curves | | 0.04 | | 0.1 | The finance dashboard went from 19.7 ms to 7.4 ms on the same measure. Lines, areas and slices stay pictures: each scene fits the 16.67 ms of a rebuild, and curves would be drawn again into the atlas on every frame, hover included, at a cost of about 1/26 of a frame per window-sized shape (see below). This comparison was reasoned from those limits, not measured. - **Not measured:** the delay from input to the screen in a real window, the system compositor, and other machines. - **Not done:** the gate does not yet fail a public component missing from every scene's `covers`. ### How much the renderer can draw `ravenrs-perf capacity` grows each kind of work until a frame stops fitting, counting until the GPU finishes, at 2560x1600. On the reference machine: | Work | Fits at 120 Hz | Fits at 60 Hz | Limited by | |---|---|---|---| | Rounded quads | 30,000 | 60,000 | GPU | | Quads with a soft shadow | 4,600 | 9,300 | GPU, the area of the blur | | Text | 180,000 glyphs | 370,000 glyphs | CPU | | A quad and a line of 43 glyphs in turn | 1,700 | 3,600 | Both; draw calls about double the cost | | Quads each with its own clip | 18,000 | 38,000 | Both | | Small shapes of curves | 6,700 | 13,400 | Both | | Window-sized shapes of curves | 26 | 56 | GPU | | Different images | 13,700 | 27,600 | CPU | | Translucent layers over the whole window | 39 | 81 | GPU | The real screens measured use 500 to 1,400 instances, so the renderer has about 20 times the room they need. It is not what limits a screen; a rebuild is. Shapes of curves share a coverage atlas of at most 8192x8192. Past that, the main pass splits to draw another page, so nothing is left out, at the cost of a pass per page. ### Performance contract Each component answers for these, through a scene in `crates/ravenrs-perf/src/scenes/` listing it in `covers`: 1. **A scene** at its worst realistic case, using only the public API. Simple components can share one. 2. **A class:** static, virtualized, animated or picture. 3. **Scale:** going from N to 10N items, a virtualized component's counters grow at most 1.2 times. 4. **No useless work:** an interaction that does not change text shapes nothing, and an animation builds nothing. 5. **Idle:** no wakes without an animation running or when paused. 6. **Cold cost declared** for a picture: its cache key and what it does with large data. 7. **Ceilings** on draw calls, batch breaks and allocations per interaction. ## Performance budgets Measured with `cargo bench` (criterion) on an Apple M-series laptop, release profile. Budgets assume a 60 Hz frame (16.7 ms). | Benchmark | Budget | Measured | |---|---|---| | Full frame: form with 40 cards (build + layout + paint) | < 8 ms | 1.6 ms | | Full frame: DataGrid with 100,000 rows | < 8 ms | 2.0 ms | | Animation frame: the same form, paint phase only | < 2 ms | 0.11 ms | | DataGrid query over 100k rows: no sort | < 16 ms | 1.0 ms | | … sort by a number | < 16 ms | 7.9 ms | | … sort by text, then number | < 16 ms | 12.3 ms | | … filter row range `100..200` | < 16 ms | 0.9 ms | | … filter row expression over three conditions | < 16 ms | 8.6 ms | | … search across columns | < 16 ms | 3.2 ms | | … group with summaries | < 16 ms | 11.3 ms | | Shape a label / shape and wrap a paragraph | < 0.1 ms | 0.003 ms / 0.035 ms | | Report: grouped sales over 10,000 rows, 305 pages | < 300 ms | 80 ms native, about 200 ms in the browser | | Report: invoice | < 5 ms | 0.05 ms | Charts are pictures made on the CPU, so their cost is paid when the data or the size changes, and not on other frames: a rebuild that produces the same shapes keeps the same texture. Measured in WebAssembly on Node 24 with the `-Oz` profile, without SIMD, on the same laptop: | Chart | Median | |---|---| | 12 bars, 760x260 at 2x | 4.3 ms | | Line with 12 points, 760x260 at 2x | 6.1 ms | | Pie with 8 slices, 360x260 at 2x | 5.3 ms | | `AreaChart`, 14 values, 760x200 at 2x | 10.8 ms | | Line with 12 points, 1600x900 at 2x | 22.2 ms | | Line with 1000 points, 760x260 at 2x | 55.8 ms | | QR Code, 160 at 2x | 2.1 ms | Dragging a window's edge redraws every visible chart on each frame, so more than a few at once, or a long series, go over 16.7 ms while it lasts. The size of a WebAssembly build, after `wasm-opt -Oz`: | Build | Bytes | Gzipped | |---|---|---| | `hello-world`, `charts` off | 2,805,367 | 1,202,999 | | `hello-world`, `charts` on, not using it | 2,806,628 | 1,203,232 | | `component-gallery` before charts used it | 3,301,057 | 1,399,745 | | `component-gallery` using every chart and code | 3,582,964 | 1,504,909 | Link-time optimization drops what an app does not call, so the feature costs an app without charts about 0.2 KB gzipped. Turning it off saves compile time, not download size. An app that does use charts pays about 105 KB gzipped, its own demos included. The DataGrid caches the query result and reruns it only when the data or the query changes, so these costs are paid once per user action, not every frame. Version 0.1 broke the budget in three cases (search at 47 ms, filter at 31 ms, text sort at 23 ms). They were fixed by parsing filters once per query, matching text case-insensitively without allocating, computing sort keys only for matching rows, and replacing text comparisons with precomputed ranks. ## Component audit Status against the definition of done. **Behavior** names the test suites that drive the component. **Visual** marks golden-image coverage. Every component sets screen reader semantics, covered by `accessibility` (widgets) and the gallery audit. Right-to-left layout is missing for every component and is tracked once, in the gaps below. | Component | Behavior tests | Visual | Keyboard | Notes | |---|---|---|---|---| | Button, IconButton | ui, keyboard_and_basics | ✅ | ✅ Enter/Space, tested | | | ToggleButton, ToggleGroup | batch_parity, keyboard_and_basics | | ✅ arrows move focus, Space toggles, tested | | | TextField | components, properties, editing_and_clipboard | ✅ | ✅ editing, word and line moves, Shift selection, select all, copy/cut/paste, mouse drag and double-click, tested; IME on web | Passwords paste but never copy | | Select | components, keyboard_and_basics | | ✅ Enter/Space/arrows open on the current option, arrows, Enter, focus returns, tested | | | Combobox | batch_parity | | ✅ type, arrows, Enter, tested | | | Checkbox, Toggle | components, batch_parity, keyboard_and_basics | ✅ | ✅ Space, tested | | | RadioGroup, SegmentedControl | keyboard_and_basics | ✅ | ✅ arrows, Home/End, wrap, tested | Keyboard added in this pass | | Slider | components | ✅ | ✅ arrows, Home/End, tested | | | Stepper | keyboard_and_basics | | ✅ arrows, Home/End, tested | Keyboard added in this pass | | TextArea, NumberField, MaskedField, TimePicker, DateTimePicker | editors | | ✅ typing, arrows, tested | | | LookupField, FormLayout, FilterBuilder | components | | ✅ type, arrows, Enter, tested | | | Toolbar, SplitButton, ConfirmDialog | components | | ✅ buttons and menus, tested | | | TreeView, CardView | app_components | | ✅ one Tab stop; arrows, Home/End, Enter, Space, tested | Virtualized | | ThemePicker, ThemeEditor | components | | ✅ built from `DropdownMenu`, `ColorPicker` and `Stepper`, which bring their keys and names; tested by name | | TagBox, ColorPicker, DateRangePicker | app_components | | ✅ arrows, type, Enter, Escape, tested | | | Wizard, FileUpload, ImageField | app_components | | ✅ buttons, tested | Dropping files has no keyboard way; the chooser is the way | | TileLayout | app_components | | ✅ arrows move, Shift with arrows resizes, tested | | | DataGrid row details and Export menu | app_components | | ✅ Right and Left, menu, tested | | | Disabled and read-only editors | components | | ✅ out of the tab order, or readable, tested | | | InputOtp | batch_parity | | ✅ digits and Backspace, tested | | | FormField, Label | batch_parity, keyboard_and_basics | | n/a | | | Calendar, DatePicker | batch_parity, keyboard_and_basics | ✅ | ✅ one Tab stop; arrows, Home/End, Page Up/Down, Enter, tested | | | EventCalendar | event_calendar | ✅ | ✅ one Tab stop, shared with Calendar; arrows, Home/End, Page Up/Down, Enter, tested | A day's name and description read out its events, tested | | Scheduler | scheduler | ✅ | ✅ events are Tab stops; arrows move, Shift resizes, Enter opens, Delete removes, tested | Grid, rows and events named, with dates, tested. Pointer: drag, resize, draw, tested | | RecurrenceEditor | event_calendar | ✅ | ✅ made of tested controls | Says the rule and its next days in words, tested | | Ribbon | ribbon | ✅ | 🟡 Tab + Enter; its menus are fully keyboard-operable | Key tips planned | | DropdownMenu, ContextMenu, Menubar | keyboard_and_basics, components, batch_parity | | ✅ first item focused on open, arrows, Enter, Escape, focus returns; Menubar Left/Right between titles and open menus, tested | | | Tabs | components, layout_and_content, keyboard_and_basics | | ✅ arrows open tabs, Home/End, tested | | | Sidebar | sidebar, layout_and_content, components | | ✅ Cmd/Ctrl+B, Tab + Enter, Up/Down/Home/End between rows, Left/Right fold submenus and groups, Escape closes the sheet, tested | `navigation` landmark; rail keeps names and shows tooltips on hover and on keyboard focus, tested. Width eases and is instant under reduced motion. Sheet goes by the window width, not its container's | | Breadcrumb, Pagination, Accordion, Disclosure | components, layout_and_content, batch_parity | | 🟡 Tab + Enter | Links and buttons by pattern; untested | | CommandPalette | batch_parity | | ✅ ⌘K, type, arrows, Enter, tested | | | Dialog, Sheet, Popover, HoverCard, Tooltip, Toaster | components, batch_parity, keyboard_and_basics | | ✅ Escape; Dialog and Sheet trap focus and return it, tested | | | SplashScreen | splash | | ✅ no focus stop: takes no input, focus leaves the page and returns on close, tested | Status and progress announced, tested | | DataGrid | datagrid, properties, editing_and_clipboard | ✅ | ✅ row navigation, Page Up/Down, Enter edits, Cmd/Ctrl+C copies rows as tab-separated text, Cmd/Ctrl+A, tested | See the DataGrid parity table in ARCHITECTURE.md | | Table, List, Stat | components | ✅ partial | n/a | | | Chart, BarChart, AreaChart, Barcode | charts, components | ✅ a chart reads its values; a code reads its text or `alt` | n/a | The layout's own tests are in `ravenrs-graphics` | | Layout: stacks, Grid, ScrollView, Splitter, AspectRatio, Card, Section | layout_and_content, components, keyboard_and_basics | ✅ partial | 🟡 Splitter arrows, untested | AspectRatio content now fills its box | | Link, Anchor, CountUp, Ticker | page_motion | ✅ `Link` is a link with its label, focusable, tested by click | ✅ Enter opens it | Anchors glide, and jump under reduced motion; tickers stand still, tested | | Sticky, ScrollScene, ScrollLinked | scroll_scene | ✅ pinned content is placed before hit testing, so the tree and clicks see it where it shows, tested | n/a, takes no input | Scrolling rebuilds nothing, tested. Under reduced motion links only fade, tested | | Footer | footer | | ✅ no focus stop of its own; its links are buttons (Tab + Enter), tested | `contentinfo` landmark, tested. Stacks by window width, not by its container's | | StatusBar | status_bar | | ✅ no focus stop of its own; its actions are buttons (Tab + Enter), tested | `contentinfo` landmark; the center is a `status` | | Timeline, ToolRail | timeline | | 🟡 blocks, markers and tools work with Tab + Enter; moving and trimming need the pointer, so apps offer them in fields too | Blocks and markers are named buttons | | ModelPicker | model_picker | | ✅ every action is a button (Tab + Enter), tested through `ravenrs-ai` | Actions are named buttons; the progress bar reads its value | | Chat, FloatingPanel | chat | | ✅ field, examples, send and sphere work with Tab + Enter, arrow keys move the sphere between corners and Escape closes the panel, tested | Answers are a `status`, announced as they appear; the panel is a named group | | ReleaseNotes, CodeBlock | release_notes | | ✅ versions and buttons work with Tab + Enter, tested | Lists its versions as buttons; the copy button says when it copied | | MoviePlayer, MovieAssistant, MovieEditor, BugReporter | ravenrs-movie | | ✅ controls, chapters, marks and blocks work with Tab + Enter; dragging on the canvas and the timeline has no keyboard way yet: 🟡 | Marks say what they teach; a film is an image with a name | | Lottie | lottie | ✅ decoration unless given `alt`, then an image with that name | n/a | Takes no input; shows its last frame still where motion is reduced, tested. `crates/ravenrs/tests/lottie_corpus.rs` measures real files against lottie-web | | SplitPanes, SplitHandle | structural_extensions | | ✅ Arrows, Shift and Home and End move the bar, tested | | | StepIndicator, StepBadge | structural_extensions | | ✅ Tab and Enter on the steps that are buttons, tested | | | Dot, OverlayBadge, Rating, IconTile, EmptyState, HeroBanner, MultiProgress, ShimmerText, DaySeparator, ShortcutSheet, ShortcutHintBar, PropertyRow, PageHeader, ZStack, ButtonGroup, FavoriteToggle, SwatchGrid, NinePointAnchor, ColorPair | simple_pieces | | ✅ Rating, SwatchGrid and NinePointAnchor take arrows, the rest Tab and Enter where they act, tested | | | Media and feedback: Image, Icon, Carousel, Avatar, Badge, Alert, ProgressBar, Spinner, Skeleton, Kbd, Divider | layout_and_content, components, batch_parity | ✅ partial | 🟡 Carousel arrows, untested | | ✅ means the WAI-ARIA keys for that widget work and a test covers them. 🟡 means it is reachable and activatable but the full pattern is missing or untested. Defects found and fixed during this audit: - RadioGroup, SegmentedControl and Stepper had no keyboard support beyond Tab. Tabs, ToggleGroup, every menu, the Select list and the Calendar grid lacked their arrow-key patterns, and Calendar days could not take focus at all. - Tab could leave an open Dialog or Sheet for the page behind it; focus now stays inside and returns to the opener on close. - Focus moved by code hid the focus ring even when the user was navigating by keyboard. - `AspectRatio` content with modifiers (for example `Spacer::new().background(...)`) collapsed to zero height. - The DataGrid query exceeded the frame budget on 100k rows (see above). ## Known gaps These block a "commercial-grade" label and are prioritized ahead of new components of the same family: 1. **Right-to-left layout.** Parley shapes bidirectional text, but layouts do not mirror. 2. **Remaining keyboard patterns** in the 🟡 rows above: Ribbon key tips. 3. **Visual coverage.** Golden images exist for 7 scenes. Overlays and navigation have none yet. 4. **Screen reader depth.** Text fields do not expose caret position or selection, and the web mirror is rebuilt whole when the tree changes. Tested with macOS VoiceOver's API (System Events) and the browser's DOM; not yet with NVDA, Narrator or Orca. --- # Performance URL: https://alexkads.github.io/RavenRS/docs/performance/ Source: https://github.com/alexkads/RavenRS/blob/master/docs/PERFORMANCE.md # Finding out why a screen is slow This guide is for anyone using RavenRS. Budgets, and how the library keeps to them, are in [QUALITY.md](QUALITY.md). ## Open the panel Press Ctrl+Shift+D, or Cmd+Shift+D on a Mac, in any app. On the desktop, `RAVENRS_DIAG=1` also opens it at start. While it is closed, none of this costs anything. While it is open, it also measures itself, so expect slightly higher numbers. ## Read it in this order **1. Latest frames.** Each bar is a frame, split by where its time went: views, layout, paint, GPU work and waiting. The line across is the 120 Hz budget. Bars under the line are fine. If only a few spikes cross it, look at what you were doing at that moment. **2. Why the screen was rebuilt.** Each rebuild runs every view on the screen, so its cost follows the size of the screen. This list says what caused them: | Cause | What to check | |---|---| | Data changed | A `State` is being set more often than it changes. Use `set_if_changed`. | | Pointer moved | Normal for views that ask `is_hovered`. Do not ask unless the view looks different. | | A view's timer | `wake_at` rebuilds everything. For drawing that moves, use `Paint::Live`, which repeats only the painting. | | Scrolled | Expected for a virtualized list, not for plain content. | **3. Costliest views.** The types of view that took the longest in the latest build, without counting the views inside them. Your own components appear under their own names. If one is at the top, look for work done in `build` that could happen once. **4. Draw calls split by.** Text and shapes alternating split them by kind, which is normal. Many splits by image mean many separate images on screen. **5. GPU resources.** Atlas resets should stop once the screen has been shown. If they keep rising, too many different glyph sizes are in use. ## Take a capture "Copy capture for Perfetto" copies the latest frames. Save them to a `.json` file and open it at to see every phase on a timeline, or attach it to a bug report. ## What makes a screen cheap - **Rebuilds are the expensive thing.** They cost about 3 µs per element on a recent laptop, most of it in layout. Scrolling and animating do not rebuild, unless a view asks to. - **What is off screen costs little.** It is built and laid out but not painted, so a long list scrolls smoothly. Beyond about a thousand elements, use a virtualized view such as `DataGrid`, because each rebuild still runs them all. - **Text is shaped once.** The same text in the same style is reused between rebuilds, so showing a changing number costs little. - **Charts are pictures.** Their cost comes when their data or size changes. One that returns unchanged is not drawn again. ## Measuring in this repository See [the runbook](../.claude/skills/ravenrs-desempenho/SKILL.md). --- # Versions & upgrades URL: https://alexkads.github.io/RavenRS/docs/versioning/ Source: https://github.com/alexkads/RavenRS/blob/master/docs/VERSIONING.md # Versioning and releases > Adopted on 2026-10-31. The first release under this model is 26.2.0, which follows 0.1.0. RavenRS follows the release model of commercial component vendors, adapted so that every version is also valid [Cargo SemVer](https://doc.rust-lang.org/cargo/reference/semver.html). ## The version number: `YY.R.P` | Part | Meaning | Example | |---|---|---| | `YY` | Year of the major release. The only place where breaking changes happen. | `27` | | `R` | Feature release within the year: 1 in the first half, 2 in the second. Additions only. | `27.1` | | `P` | Maintenance release: fixes, performance and documentation. Monthly, or sooner for serious bugs. | `27.1.3` | Commercial component vendors name releases the same way, such as `25.1` and `25.2`, or `2025.1`. The difference is that in RavenRS, `27.1 → 27.2` never breaks your build. `ravenrs = "27"` in `Cargo.toml` follows every feature and maintenance release of that year automatically. Breaking changes wait for the next year's `.1` release, from 28.1.0 on: see below for what holds until then. ## Calendar A breaking change can bring a `YY.1.0` forward rather than slip into a feature release: 27.1.0 shipped in November 2026 for that reason. That was the last time: **`YY` and `R` now follow the calendar only**. Until 27.2.0, in the second half of 2027, every release is the next `27.1.P`. **Until 28.1.0, any release may hold breaking changes**, listed under *Breaking* in the CHANGELOG. The library is still taking shape, and holding its API still costs more than it saves. So until then `ravenrs = "27"` can break your build on `cargo update`: pin an exact version, `ravenrs = "=27.1.8"`, if that matters to you. The table below is what holds from 28.1.0 on. | When | Release | What it may contain | |---|---|---| | First half of the year | `YY.1.0` | New components and features, **and** the year's breaking changes, each listed in a migration guide | | Second half of the year | `YY.2.0` | New components and features only | | Monthly | `YY.R.P` | Fixes only | Each feature release has a public beta (`YY.R.0-beta.N`) about four weeks before the final release. ## Support - The current year's line gets fixes. The previous year's line gets security and critical fixes for 12 months after the next major ships. - **MSRV.** A feature release may raise the minimum Rust version to one at least six months old. A maintenance release never raises it. - Deprecations are announced in a feature release with `#[deprecated]` and a replacement, and are removed no earlier than the next major. ## What counts as breaking Breaking changes are the ones that Cargo SemVer and [`cargo semver-checks`](https://github.com/obi1kenobi/cargo-semver-checks) treat as major. Enums that grow are `#[non_exhaustive]`, and builders have defaults, so new variants, fields and options are additions. Visual changes count too: a change that moves pixels in the golden-image tests ships only in a feature or major release, with a CHANGELOG entry. ## Release checklist 1. `scripts/check.sh --full` passes, including the golden-image tests. 2. `cargo semver-checks` runs against the previous release. Until 28.1.0, what it finds goes under *Breaking* in the CHANGELOG; from then on it has to agree with the version bump. 3. Benchmarks are within the budgets in [QUALITY.md](QUALITY.md). 4. CHANGELOG has an entry, plus a migration guide for a major. 5. All crates are published in dependency order, then the tag `vYY.R.P`, the GitHub release, and the site and API docs are updated. 6. Every crate's documentation built on docs.rs (`https://docs.rs/crate///status.json`); it uses a recent nightly, so it can fail when stable passes. ## Unified publication Framework crates, the cargo-ravenrs CLI, the RavenRS plugin and the npm preparer ship together under the same YY.R.P version. Every release publishes all public crates, the versioned Git tag, the npm package, API documentation and both documentation sites. CLI and npm-only fixes are included in the next complete release; do not report them as a complete RavenRS release. A different publication strategy requires an explicit request from Alex. The CLI inherits the workspace version. npm/create-ravenrs/release.json pins that same version, cliVersion and vYY.R.P Git tag. Run prepare-release after the workspace bump; prepack rejects any mismatch in package versions, pins, toolchain or bundled licenses. Before publication, confirm the chosen version is unpublished on crates.io and npm, then follow the checklist above. Historical npm 27.1.24–27.1.26 and CLI-only 27.1.24 kept framework 27.1.23. Starting with 27.1.27, all release artifacts are aligned again.