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 <canvas>. 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 (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 | 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<RefCell> sprawl. |
| Xilem / Masonry | 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 | 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 | 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 | 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 | 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 | MIT | — | — | Flexbox, Grid, Block | Measure callback | Yes | Wire text into layout through the measure function from day one. |
| wgpu | MIT OR Apache-2.0 | Vulkan, Metal, DX12, GL, WebGPU | — | — | — | WebGPU and WebGL2 | Pin the major version; breaking releases arrive roughly quarterly. |
| 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-designerravenrs-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, §6.4 and 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 PathFills) |
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, 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::buildcreates layout boxes and paint instructions throughBuildCx. The whole build runs inside a reactiveObserver, so everyStateread 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 theScene. 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).
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<T>owns a value.get/withrecord a read in the innermost trackingObserver;set/updatenotify only the observers that read it during their latest run.Binding<T>is a read/write view of a value owned elsewhere (SwiftUI's@Binding).Binding::mapprojects onto a field.Derived<T>is a lazily recomputed, cached value over other state (a memo). It notifies its own readers when its inputs change.Observerruns 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<observer>, 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:
- Functions returning
impl Viewfor stateless composition. Component: a struct that holds its state and implementsbody() -> impl View. EveryComponentis aViewthrough a blanket implementation.Viewimplemented directly, with full access toBuildCx:cx.child(&view),cx.children(&seq),cx.keyed_child(key, &view)to build children;cx.container(LayoutStyle, &children, Paint)andcx.text(...)to create layout boxes;Paint::Quad(QuadPaint)orPaint::Custom(DrawFn)to draw anything into theScene, orPaint::Rich(RichFn)when the drawing also shapes text or needs the device scale;cx.on_click,cx.focusable,cx.cursorfor 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<V> 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 aState<T>owned by that widget instance, keyed by itsWidgetIdand 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_horizontallyadds the other axis, whichDataGriduses): offsets are kept perWidgetId, 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 orcx.request_focusmoves 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 nodetoppixels 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), andcx.scroll_linked(node, link)paints a subtree with theEffectaScrollLinkcomputes 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);#nameinstead glides the scroll container to the node marked withcx.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 anEffectof 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, andcx.is_hover_within()for hover over a subtree. - Clipboard.
cx.clipboard()reads and writes the system clipboard:arboardon desktop,navigator.clipboardon the web (where reading is asynchronous, so a paste arrives through the browser'spasteevent as typed text).Ui::set_clipboardswaps the provider; tests use an in-memory one. - Sound.
cx.sound()plays short clips and speaks. On the desktop,cpalplays 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 andspeechSynthesis.Ui::set_soundswaps the provider; the default is silence. - Theme as state. The theme is a
State<Theme>;ThemeSwitcheror any component can change it throughcx.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.pyregenerates the subset and theIconNameenum.
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.
Criteriais 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.
Scopeinterprets the tree in memory over anyRecord, such as JSON, a map or your own rows.Environmentholds parameters and custom functions. - Function catalog. One table in
functionsdrives parsing, arity checks and expression editors. - Validation.
validatechecks an expression against aSchemabefore anything runs. - SQL.
to_sqltranslates to parameterized SQL perDialect. - Formatting.
format_valueandformat_compositeuse .NET-style format strings perCulture.
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 previewExpressions 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). ravenrs-report-viewer shows documents with thumbnails and search, and asks for parameters; docs/REPORTING.md has the design and phases, and 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
erfapproximation 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
*Srgbtargets 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.rschecks pixels of real scenes andcrates/ravenrs/tests/visual.rscompares 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 <canvas>:
- async adapter and device creation (
wasm-bindgen-futures), never blocking the main thread; - a friendly message next to the canvas when
navigator.gpuis missing (no WebGL fallback in this phase); - canvas backing store sized from CSS size ×
devicePixelRatio, re-checked every frame and onResizeObservercallbacks; - pointer events with pointer capture, keyboard events with Tab/Space/Enter kept inside the app;
requestAnimationFramescheduled only when the UI is invalidated (at most one pending);- GPU device-loss recovery: the device is re-requested asynchronously;
WebAppremoves 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
Semanticsor 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_actionmaps 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<V>, 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), the diagnostics panel with GPU timestamps (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 |
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<Mutex> |
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. |