Docs·Charting Library·Guides

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:

Code
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.

TOML
[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"] }
rust
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:

GestureCall
Window resizeengine.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:

rust
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