densa is a Rust toolkit for applications that show a lot of live data: trading terminals, monitoring consoles, operational dashboards, developer tools. It keeps live data in order as it changes, and draws it on the GPU at the display's refresh rate, from the data stream to the pixel.
Status: 0.x, experimental. On crates.io; APIs will change between releases (what changed). Builds and tests on Linux, macOS and Windows.
Architecture
- Data. Arrow record batches from files, streams or query engines go into live tables; views sort and filter them, groups aggregate them, and both follow every change incrementally.
- Interface. A tree of widgets, laid out with flexbox, takes pointer, keyboard and input
method events; widgets (buttons, text inputs, scroll views, a data grid, charts) draw into a
display list that is independent of any GPU API, and
densa-windowpresents a whole frame in one draw call with wgpu. - Runtime. Hand data between threads without locks, place threads on the right cores, format numbers without allocating, and prove that frames never allocate.
Each crate is usable on its own, and only densa-render and densa-window depend on a GPU
or windowing crate.
Highlights
- Live tables. Upsert Arrow batches by key; sorted, filtered views and grouped aggregates are kept up to date as rows change instead of being recomputed.
- Charts over live data. Lines, candlesticks and volume in stacked panes that follow the newest data, zoom around the pointer and pan; a line is drawn one pixel column at a time, so a million points cost about as much as the chart is wide and every high and low shows.
- Widgets with flexbox layout. A retained widget tree laid out with flexbox, with pointer capture, hover, focus and Tab order, and widgets for toolbars, forms, lists, split panes and docking workspaces; it runs headless, so interfaces are tested without a window.
- Accessible. Widgets describe themselves to screen readers (through AccessKit on Windows, macOS and Linux) and do what they are asked: press, type, scroll, choose a tab. Docking tabs are reachable and usable from the keyboard.
- Arrow from end to end. Batches are shared, never copied, from the source to the screen; a grid over millions of rows costs only its visible rows.
- No allocation per frame. Drawing, scrolling, hit-testing, delivering events and moving a caret allocate nothing once fonts and glyphs are loaded; tests prove it with a counting allocator.
- No locks between threads. A wait-free triple buffer hands the newest data to the UI thread; its memory orderings are model-checked with loom.
- Real text. Shaping, wrapping, bidirectional text, selection, editing and input methods (IME), built on parley. Characters the bundled font lacks, such as Korean, Japanese and Chinese, come from the system's fonts.
- One draw call per frame, with a glyph atlas whose memory is bounded.
Quick start
You need Rust 1.90 or later and a GPU with Vulkan, Metal, DirectX 12 or OpenGL.
To use densa, depend on the densa crate, which gathers the libraries as modules
(densa::data, densa::widgets, densa::window, ...):
[]
= "0.2"
Or take only the crates you use, each on its own (densa-data needs no GPU or window):
[]
= "0.2"
= "0.2"
= "0.2"
Crates
| Crate | Use it for |
|---|---|
densa |
All of the crates below under one name, as modules; the window and data features (on by default) leave out the GPU and Arrow when off |
densa-data |
Live Arrow tables: keyed upserts and deletes, sorted and filtered views and grouped aggregates kept up to date incrementally, time windows that keep a table's recent rows, snapshots for SQL engines and other Arrow readers |
densa-arrow |
Arrow tables and live views in the data grid, the grid widget and charts, with typed, allocation-free cell formatting |
densa-ui |
The widget tree: flexbox layout, pointer, keyboard and input method events, focus, themes, an accessibility tree; no window or GPU needed |
densa-widgets |
Labels, buttons, text inputs with undo and the clipboard, scroll views, splitters, panels, docking with saved layouts, menus and context menus, a data grid widget that selects and copies rows, a chart widget |
densa-window |
Running an interface in a native window: winit input in, a wgpu frame out, screen readers through AccessKit |
densa-grid |
A data grid over your own data (GridModel, GridSource): columns, alignment, cell styles |
densa-chart |
Charts over your own data (Points, Candles, ChartModel): lines, candlesticks, bars, number and time axes, a crosshair |
densa-paragraph |
Paragraphs: shaping, wrapping, bidirectional text, selection, keyboard navigation, editing with input methods, undo and redo |
densa-text |
Fonts, glyph rasterization and a glyph atlas that fits the GPU's limits |
densa-draw |
A frame's display list: a Canvas that batches rectangles and glyphs, independent of any GPU API |
densa-render |
Drawing a display list with wgpu in one draw call |
densa-sync |
A wait-free triple buffer: the latest data from a producer thread to the UI thread |
densa-platform |
CPU topology, thread placement and priority (Windows today) |
densa-fmt |
Number formatting into stack buffers, without allocating |
densa-alloc |
A counting global allocator, to prove a code path allocates nothing |
densa-core |
Shared primitives: Color, Point, Rect, Size |
Connectors that bring data in, with the large dependency trees their formats and protocols
need, live in densa-connectors: today
densa-files, which reads CSV and Parquet files as Arrow batches on a background thread,
densa-ws, which reads WebSocket streams of JSON rows or Arrow IPC as Arrow batches and
reconnects on its own, densa-flight, which reads Arrow Flight and Flight SQL results, and
densa-datafusion, which runs SQL with Apache DataFusion over files, batches and snapshots
of live tables (LiveTable::snapshot).
Usage
A live table, a sorted view, aggregates and a time window
use Duration;
use ;
let mut table = new?; // keyed by the `symbol` column
let movers = table.add_view?;
table.upsert?; // a RecordBatch: new symbols insert, known ones update
table.delete?; // an array of keys
let view = table.view.unwrap; // in order, after every change
let top = view.row; // where the row's values are in table.batches()
// Grouped aggregates follow every change the same way.
let by_sector = table.add_groups?;
let summary = table.groups.unwrap.to_batch?; // one row per sector
// Keep the last five minutes by the `time` column: older rows go as newer ones come.
table.set_window?;
A window ends at the newest time in the table, not at the clock, so replayed data is
windowed by its own times. Window::span does the same for a column of plain numbers, in
their units.
To show it in the grid widget, densa_arrow::LiveModel wraps the table: a click on a column
title sorts the view, the app sets a filter, and the selection follows its row as values
change (every row has a RecordId that lasts as long as its key). The
live_table example does this with 100,000
instruments, a thousand of them updated every frame; densa_arrow::LiveGrid paints a view
without widgets.
Widgets in a window
use ;
use ;
use ;
run?;
A Ui also runs without a window: build it, feed it events and read its actions in tests.
A grid over your own data
use write_fixed;
use ;
use Write as _;
/// A monitoring table: one row per sensor.
GridPainter::paint draws a GridSource onto a Canvas; GpuRenderer draws the result.
A chart over your own data
use ;
// A day of readings, one a second: (seconds since the epoch, value).
let readings: =
.map
.collect;
// The last hour, scaled to what it holds.
let x = time;
let = y_extent.unwrap;
let plot = new;
// One span per pixel column, however many readings fall in it.
paint_line;
ChartPainter adds gridlines, tick labels and a crosshair; paint_candles and paint_bars
draw candlesticks and volume. In a window, implement ChartModel (stacked panes on one time
axis, each named above it if you give it a title) and give it to the Chart widget, which follows new data, zooms with the wheel and
pans by dragging: see the chart example.
For live data, densa_arrow::LiveSeries reads two numeric columns of a live table's view
(numbers, timestamps, dates) as a series, so a chart keeps up with the same table a grid
shows: see the linked example.
Text
use ;
let mut text = new?; // fonts and layout scratch space, once per application
// A paragraph: shaped once, re-wrapped to any width without shaping again.
let style = new.with_align.with_max_width;
let mut paragraph = text.paragraph?;
paragraph.reflow?;
// Editable text: typing, deletion by character or word, input-method composition, undo.
let mut editor = new?;
editor.insert?;
editor.delete?; // ctrl+Backspace
editor.undo?; // ctrl+Z: "world" is back
TextSelection turns clicks into caret positions and moves by character, word and line, in
screen order for arrow keys and in text order for deletion, so right-to-left text behaves.
See the paragraphs and
editor examples.
Handing data to the UI thread
use triple_buffer;
let = triple_buffer;
// Producer thread: fill the back slot, then publish it. Never blocks.
spawn;
// UI thread, once per frame: switch to the newest snapshot, if any.
if reader.update
Examples
| Example | Shows |
|---|---|
densa-widgets monitor |
1,000 simulated hosts in a live grid, slowest first, and a chart of the selected host's latency and traffic from a live samples table |
densa-widgets gallery |
A toolbar, a filterable list in a scroll view beside a details pane in a splitter, Tab between controls; --light for the light theme |
densa-widgets dock |
Panels in tab groups (drag tabs to join or split groups, the gaps to resize) around a live watchlist grid: click a title to sort, a row to select; --demo starts mid-drag |
densa-widgets live_table |
100,000 instruments in a live table, updated every frame: click a title to sort, type to filter, select a row and it follows its instrument |
densa-widgets chart |
Live one-minute candles with a moving average over volume: scroll to zoom, drag to pan, double-click to follow the newest candle |
densa-widgets linked |
A grid of 200 live instruments and a chart of the selected one's trades: a view of a live trades table, filtered by symbol and sorted by time |
densa-arrow live |
A live table updated continuously, its "top movers" view kept sorted |
densa-arrow table |
A million-row Arrow table; --live appends batches while it is shown |
densa-paragraph paragraphs |
Wrapping, alignment, selection with mouse and keyboard |
densa-paragraph editor |
Editable text with input methods |
trading-demo |
A streaming market grid: a pinned worker thread, a triple buffer and the grid |
Guarantees and how they are checked
| Guarantee | Checked by |
|---|---|
| Live views and aggregates equal a full recomputation | Model-based tests check every view and every group after each of thousands of random upserts, deletes and compactions |
| Drawing, hit-testing, event delivery and selection allocate nothing | A counting global allocator in each crate's tests/zero_alloc.rs |
| The triple buffer is free of data races | loom model checking; a mutation script proves every weaker memory ordering fails |
| Correct pixels | GPU tests render offscreen and read the pixels back (on machines with a GPU) |
Sound unsafe code |
A // SAFETY: comment on every block (enforced by Clippy); Miri on the concurrency, allocator and platform crates |
| Tests that catch bugs | Deliberate bugs (mutants) must make the tests fail |
Platform support
| Linux | macOS | Windows | |
|---|---|---|---|
| Rendering (wgpu) | Vulkan, OpenGL | Metal | Vulkan, DirectX 12, OpenGL |
| Text, paragraphs, editing | yes | yes | yes |
| Live tables, grid, triple buffer | yes | yes | yes |
| Widgets, layout, windows | yes | yes | yes |
| Thread placement and priority | not yet | not yet | yes |
A window draws with the best API the system offers unless the application picks one
(WindowOptions::with_backend); the WGPU_BACKEND environment variable (vulkan, dx12,
metal, gl) overrides both. Graphics drivers differ in what each API costs, memory most of
all, so measure on the machines you target.
Limitations
- Widgets: labels, buttons, text inputs (with undo, redo and the clipboard), scroll views, splitters, panels, docking, menus and a data grid, with flexbox layout, events, focus and accessibility. Not there yet: keyboard shortcuts handled for menus (a menu shows them; the application handles the keys), and text inputs' caret and selection for screen readers. The grid selects a row or a range of rows (kept on their records when the model gives rows keys) and copies them as tab-separated text; it does not yet select cells or scattered rows, paste or edit. On Linux the clipboard goes through X11 (XWayland under Wayland). Docking saves and restores layouts by panel key, but does not yet float panels in their own windows.
- Charts: lines, candlesticks and bars on number and time axes (UTC: no time zones yet), in stacked panes, fed from your data or a live table's view. Not there yet: logarithmic axes, area fills, annotations and drawing tools, several y axes in one pane, and candles built from a live table's trades (the app aggregates them). A line is one pixel column wide where it is steep, however thick it is set.
- Live data: views sort and filter, groups count, sum, average and find extremes; time windows and joins are not there yet. A table is used from one thread at a time.
- Text: grid cells are single-line, unshaped text in the bundled font. Word breaks in
Chinese and Japanese and line breaks in Thai, Lao, Khmer and Burmese need densa-paragraph's
complex-scriptsfeature (several MB of dictionaries). Not supported yet: choosing a language per paragraph (it picks the regional forms of Han characters), inline boxes, variable-font axes, synthetic bold and italic, color emoji. - Threads: placement and priority are implemented on Windows only.
The roadmap lists what comes next.
Development
RUSTFLAGS="--cfg loom"
Set DENSA_SKIP_GPU_TESTS=1 on machines without a GPU or desktop session. Miri runs against
the Linux target: with a custom global allocator, Miri reports a false positive in the Windows
System allocator.
Bug reports and pull requests are welcome. CONTRIBUTING.md explains how the workspace is laid out, what the code must guarantee and every check a pull request has to pass.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
The bundled font crates/densa-text/fonts/JetBrainsMono-Regular.ttf (JetBrains Mono 2.304)
is licensed separately under the SIL Open Font License 1.1; see
crates/densa-text/fonts/OFL.txt.