Native & Desktop (GPUI / Rust)
Embed the same chart engine natively in a Rust desktop app. One engine emits a renderer-agnostic command buffer that dispatches to Canvas 2D on the web and to GPUI / wgpu / skia natively — candlesticks, orderbook heatmap, footprint, GEX, and 18+ indicators, identical on both targets.
kline-orderbook-chart started as a web chart engine and grew a native
path. The same engine core that renders in the browser also builds as a
plain Rust crate — no browser, no <canvas>, no JavaScript runtime — so
you can embed a full trading chart inside a native desktop app (for example a
GPUI window) and get lower latency,
no tab throttling, and direct GPU-backed painting.
You build once and ship to both. Same candles, same orderbook heatmap, same footprint, same GEX gamma heatmap, same numbers.
Why native
The web is a great distribution channel and a hostile runtime for a chart a trader stares at for eight hours: background tabs throttle to ~1 fps, garbage-collection pauses land mid-pan, and Canvas is a single-threaded raster target with no direct GPU control. A native build removes those: predictable frame pacing, real threads, GPU-backed painting through GPUI / wgpu / skia, and no browser sandbox between you and the metal.
Historically, going native meant rewriting your chart. Here it does not.
One engine, one command buffer
The idea that makes web-and-native work with no rewrite: the engine never touches a canvas or a GPU directly. Each frame it computes a compact, renderer-agnostic command buffer — a list of typed draw commands:
FillRect { x, y, w, h, color }
Line { x1, y1, x2, y2, color, line_width }
Text { x, y, text, size, color, align }
HeatmapWorld { … } // depth / GEX raster in world coordinates
Circle { … } · Polyline { … } · GradientRect { … } · ClipRect { … } · …
On the web, a thin layer reads that buffer and calls ctx.fillRect,
ctx.stroke, ctx.fillText. Natively, you read the same buffer and call
your painter. The engine does not know or care which one it is — the wire
format is identical, so a bar drawn on the web is the same bar drawn on the
desktop.
How it's distributed
The native SDK is sold under the same commercial licence as the web
package — you licence it, you do not get the engine source. It ships as
a precompiled engine (a native library) plus a thin, source-visible
decoder + adapter (the Paint / command-buffer reader and the reference
GPUI mapping). Your app links the binary and reads its command buffer; the
engine internals stay closed, exactly like the WASM build on the web.
Delivered through your licence (a private registry or a vendored path), not crates.io. Contact [email protected] for a native key + SDK access.
The native Rust API
No JavaScript, no WASM, no bridge — you use the licensed crate directly.
[dependencies]
# Licensed native SDK: precompiled engine + open decoder/adapter.
# Delivered via your licence (private registry / vendored path), not crates.io.
chart-engine = { version = "0.1", features = ["native"] }
use chart_engine::{ChartEngine, render::Paint};
// 1. Create the engine at a logical (CSS-pixel) size.
let mut engine = ChartEngine::new(960.0, 600.0);
// 2. Activate your licence (native tokens skip the web domain check — see below).
engine.set_license(include_str!("../license.mrd").trim());
// 3. Feed columnar OHLCV.
engine.set_klines(&ts, &open, &high, &low, &close, &volume);
// 4. Render one frame, then walk its command buffer.
for cmd in engine.commands() {
match cmd {
Paint::FillRect { x, y, w, h, color } => painter.fill_rect(x, y, w, h, color),
Paint::Line { x1, y1, x2, y2, color, line_width } => painter.line(x1, y1, x2, y2, color, line_width),
Paint::Text { x, y, text, size, color, .. } => painter.text(x, y, text, size, color),
Paint::HeatmapWorld { .. } => painter.heatmap(cmd), // depth / GEX raster
_ => {}
}
}
engine.render_frame() renders and returns the raw bytes;
engine.commands() gives you a streaming iterator of typed Paint values over
the last frame. That match block is your renderer adapter — a couple
hundred lines mapping ~30 command variants to your backend.
Input wiring
All input methods are ordinary &mut self calls (CSS-pixel coords, origin
top-left). Wire them to your window events, then re-render:
| Gesture | Call |
|---|---|
| Window resize | engine.resize(w_css, h_css) |
| Pan (drag) | engine.pan(dx, dy) |
| Zoom (wheel, toward cursor) | engine.zoom(sx, sy, factor) — factor > 1 = zoom in |
| Crosshair (mouse move) | engine.set_crosshair(sx, sy) |
| Crosshair off (mouse leave) | engine.hide_crosshair() |
The engine is dirty-tracked — it only rebuilds the base layer when something
changed, so calling render_frame() every animation tick is cheap.
Wiring it into GPUI
GPUI is the Rust UI framework behind the
Zed editor: retained-mode elements, a
GPU-backed painter, and a tight frame loop. The chart becomes a single custom
element whose paint pass drains the command buffer:
impl Element for ChartElement {
fn paint(&mut self, bounds: Bounds<Pixels>, _: &mut (), cx: &mut WindowContext) {
let mut engine = self.engine.borrow_mut();
engine.set_size(bounds.size.width.into(), bounds.size.height.into());
engine.render();
for cmd in engine.commands() {
match cmd {
Paint::FillRect { x, y, w, h, color } => {
cx.paint_quad(fill(rect(x, y, w, h, bounds), rgba(color)));
}
Paint::Line { x1, y1, x2, y2, color, line_width } => {
cx.paint_path(line_path(x1, y1, x2, y2, bounds), rgba(color), line_width);
}
Paint::Text { x, y, text, size, color, .. } => {
cx.paint_text(point(x, y, bounds), text, size, rgba(color));
}
Paint::HeatmapWorld { .. } => paint_heatmap(cx, &cmd, bounds),
_ => {}
}
}
}
}
Feed pan / zoom / crosshair from GPUI's input events into the engine's viewport setters, request a repaint, and you have a native, GPU-painted trading chart.
A buildable reference GPUI adapter ships in the repo — start there instead
of writing the match from scratch. GPUI's paint API tracks its own version;
if a paint_* call doesn't resolve against your pinned gpui rev, the fix is
local to that one adapter file.
Licensing native builds
Web licences are locked to a domain and mobile licences to a bundle ID. A
native build has neither, so it uses an Ed25519 token minted with
pl:"any" — the pl (platform) claim tells the engine to skip the domain /
bundle check while keeping the plan and expiry checks intact. Pass the token
string to engine.set_license(...); when it expires the engine stops drawing
the chart body, exactly like the web build.
The engine ships as a precompiled library — you licence the binary, the same commercial model as the web package; the engine source is not sold. Contact [email protected] for a native licence key + SDK access.
What you get in the native crate
The native crate is the full engine, not a candle-only stub:
- Candlesticks with O(1) amortised appends for live bars.
- Orderbook depth heatmap — resting liquidity behind the candles.
- GEX gamma heatmap — dealer gamma by strike over time.
- Footprint cells — bid×ask executed volume, POC, delta, stacked imbalances.
- Liquidation heatmap — force-close clusters as an overlay.
- 18+ indicators — VRVP, CVD, TPO, RSI, MACD, Open Interest, Funding, more.
- Custom indicators — a sandboxed scripting language, no fork required.
Every one of those is a command-buffer producer, so every one renders identically on the web and in your native window.
Where to go next
- Getting Started — the web side, install + first chart in five minutes.
- Chart Instance — the engine object, its lifecycle, and the render loop.
- Building a Native GPUI Chart in Rust — A GEX Gamma Heatmap — the full walkthrough with screenshots.
- Kline Chart With Orderbook Heatmap: A Library Guide — the web integration guide.
- Code: GitHub — kline-orderbook-chart · npm · GPUI