
Blender's user interface, as a library you can use from Python, Rust, JavaScript/React, C, C++ and Dear ImGui.
People spent decades building and optimizing Blender's GUI. It's battery and memory efficient, and super duper fast. We decoupled it, to give everyone a chance to build their own products with it. It supports multiple languages and frameworks, so you won't miss a beat!
Website and full documentation: bui.preetham.top
This README is a small book. Read it front to back for the whole story, or jump to the chapter you need. Every chapter links to a longer guide in docs/ or on the website when there's more to say.
BUI takes the user interface of Blender (its widget drawing, its interaction rules and its look) and turns it into a standalone UI library with a small C core. Your code builds the widget tree and handles callbacks. Layout, input, drawing and presenting all happen in fast native code, on the CPU or the GPU.
At the heart of BUI is the kit: a small runtime written in C. The kit holds every widget in one tree, lays them out, handles the mouse and keyboard, and draws them the way Blender does. The drawing code is a C port of Blender's own widget code, and the interaction follows Blender's rules. On top of the kit sit bindings: thin layers that let Python, Rust, JavaScript, C++ and Dear ImGui talk to it.
What BUI is not:
interface_widgets.cc, and the Dear ImGui support runs the real Dear ImGui.Blender's interface is famous for how it feels. You press a number field and drag: the cursor vanishes, the value glides under your hand, and you never hit the edge of the screen. You type 2*pi into a field and it just works. You type "ad cu" into a menu and it finds "Add Cube". Millions of artists have these habits in their hands.
That interface is also fast and frugal. It redraws only what changed, sleeps when nothing does, and starts in milliseconds. BUI exists so that any app, in any language, can have both: Blender's feel, and Blender's efficiency.
You can drive BUI many ways, and all of them share the same C runtime, so they all get the same widgets and interactions:
| You write | You get | Status |
|---|---|---|
| tkinter code, even existing programs, unchanged | Blender-styled apps; every tkinter and ttk API | Complete |
Qt-style Python (QWidget, layouts, clicked.connect) |
The common subset of Qt widgets | Partial |
Blender components (Panel, NumberSlider, PieMenu, the node editor ...) |
45 native Blender widgets and editors, from Python; ten of them also from Rust and JavaScript | Complete |
Rust: HTML + CSS + Tailwind, HTML-like markup (rsx! / view!), hooks, or gpui / zopra syntax |
A web-like framework: HTML that looks like a browser's, Blender's look with styled, and Blender components (<NumberSlider>, <Button> ...) |
Working |
| JavaScript / React on Node.js or Bun: a DOM-like API or JSX | The same engine and components in native windows, over napi-rs; React's hooks backed by signals | Working |
| Dear ImGui from C++, C, Python or Rust | The real Dear ImGui on BUI windows in Blender's theme, or a build where ImGui's widgets are Blender's widgets | Working |
C (BUI_kit.h) |
The kit itself: nodes, widgets, windows | Working |
| MyGo | MyGo | Not yet (coming soon) |
Not sure which to pick? If you already have code, use the row that matches it. If you're starting fresh in Python, the Blender components are the friendliest.
2*pi, min(4, 3), round(1.7), x if c else y.QPainter, menus and timers.from blenderui import imgui, pyimgui-style.pip install, no Qt or Tk widgets drawing anything.styled gives any subtree Blender's look, and Blender components (<NumberSlider>, <Button>, <ColorSwatch> ...) are real Blender widgets.calc() and every math function, var(), oklch() and color-mix(), :has(), nesting, @layer, @media ranges, @container, ::before/::after, counters.@keyframes animations and transitions, filters, backdrop-filter, blend modes, clip-path, masks, 2D transforms, border-image.class="md:hover:bg-sky-500/50" or bare attributes (<div p-4 bg-[#4772b3]>), with "did you mean" compile errors.rsx! / view! HTML-like markup, #[component], and hooks in zopra, React or Leptos style (use_state, signal, Memo, Effect, context).div().flex().gap_2(), Render, cx.listener.cargo add blenderui.frame holds one frame; reduced motion holds them all.<img>, CSS background-image, object-fit, Blender Image components, tkinter PhotoImage (now also JPEG, WebP, BMP, TGA, QOI, HDR, SVG), canvas items, data-table image columns, chat-list media and Dear ImGui textures (animated ones too).<NumberSlider>, <Button> ...). BUI draws them, on the CPU or the GPU; there's no browser and no webview.{count}) and a change updates that one node without re-rendering; read it while rendering (count.value) and the component re-renders, as with React. A conformance suite checks each hook against React's own.@blender-ui/native: a DOM-like API (createElement, appendChild, addEventListener) with the same signals, for apps without React.bui build app.jsx produces one executable of about 7 MB: BUI with an embedded QuickJS engine.--bun builds a Bun executable instead, with Node's APIs and a JIT.imgui.h works, including ShowDemoWindow().imgui.h), C (cimgui.h, all 399 functions), Python and Rust (generated from imgui.h).BUI_IMGUI_BLENDER_WIDGETS=ON, Blender's actual widgets and interaction, pixel-identical to BUI's own.Xft.dpi on Linux, Retina on macOS.Every screenshot here is a real BUI window, rendered off-screen. Where an image comes in a dark and a light version, GitHub shows the one that matches your theme.

The node editor, from Python (examples/site/geometry_nodes.py)

A pie menu, and Blender's menu search (type in any open menu)

The color picker, and a tooltip

CSS effects in Rust: filters, clip-path, transforms, blend modes, backdrop-filter (site_effects.rs)

CSS grid with repeat(auto-fill, minmax()), spans and grid-auto-flow: dense (site_grid.rs)

CSS @keyframes at 0.25 s, 0.6 s and 1.0 s (site_animation.rs)
More screenshots

examples/showcase.rs: HTML + CSS in Rust

Dear ImGui's demo with its own widgets, in Blender's theme

tkinter programs: text with tags, canvas, dialogs
Python. You need CPython 3.11 or newer. Wheels are published for Windows, Linux and macOS, so there's nothing to compile. One package gives you all four Python APIs (tkinter syntax, Qt syntax, the Blender components and Dear ImGui):
pip install blenderui python -c "import blenderui; print(blenderui.__version__)"
Import it as blenderui. You'll also see import bui in older examples: it's the same code under its original name, but an unrelated project owns bui on PyPI, so prefer blenderui in anything you publish.
Rust. The crate is blenderui on crates.io:
cargo add blenderui
The first build compiles BUI's C kit for you, so you need Rust 1.88 or newer, CMake and a C compiler (on Windows, the Visual Studio Build Tools for the default MSVC target), and on Linux the X11 headers (libx11-dev or libX11-devel; no GL headers are needed).
JavaScript. You need Node.js 18 or newer, or Bun:
npm install @blender-ui/react react # or: bun add @blender-ui/react react npm install @blender-ui/native # the DOM-like API, without React
The npm packages are prebuilt for Windows x64. On Linux, build the addon from a clone (npm install then npm run build in bindings/, with Rust, CMake and a C compiler); windows are native X11 windows there. On macOS, JavaScript runs headless for now (see Platforms).
C and C++. Build from source with CMake 3.20+ and a C11 / C++20 compiler (MSVC, GCC or MinGW-w64, or Clang); on Linux, also the X11 headers:
git clone https://github.com/ppmpreetham/BUI && cd BUI cmake -S . -B build cmake --build build -j 2 ctest --test-dir build # optional: run the test suites
In your own CMake project, add BUI as a subdirectory and link bui::kit (or bui::imgui for Dear ImGui):
add_subdirectory(BUI) target_link_libraries(myapp PRIVATE bui::kit)
Dear ImGui. Python gets it with pip install blenderui (import blenderui.imgui). C++ links bui::imgui, C links bui_cimgui. Rust uses the blenderui-imgui crate:
cargo add blenderui-imgui
If something goes wrong:
| Symptom | Fix |
|---|---|
ImportError: buipy |
You're running from a source checkout without a build. Build first (cmake --build build), or set BUIPY_BUILD_DIR. The extension must match your Python version. |
| The window never appears | Start the event loop: mainloop(), app.exec() or App.run(). |
| Blurry or tiny UI | Set BUI_UI_SCALE, for example BUI_UI_SCALE=1.25. |
gpu=True does nothing |
Run with BUI_GPU_DEBUG=1 to see why the GPU device didn't start. Without Direct3D 11 or OpenGL 3.3, BUI stays on the CPU renderer, which draws the same pixels. |
More: docs/getting-started.md and Install BUI on the website.
Run any tkinter program on BUI without changing a line:
blenderui-tk my_app.py # same as: python -m bui.tksyntax my_app.py
blenderui-tk makes import tkinter return BUI's implementation for that run. Or opt in from code:
import blenderui.tksyntax as tk from blenderui.tksyntax import ttk, messagebox
root = tk.Tk() name = tk.StringVar(value="Cube") ttk.Entry(root, textvariable=name).pack(fill="x", padx=8, pady=8) ttk.Button(root, text="Greet", command=lambda: messagebox.showinfo("Hi", "Hello " + name.get())).pack() root.mainloop()

Unchanged tkinter and ttk programs (examples/site/tk_export.py, ttk_queue.py)
Number fields (Spinbox, Scale) scrub like Blender's, Ctrl+Z / Ctrl+Shift+Z undo edits, and filedialog opens Blender's File Browser. Guide: docs/tkinter.md.
Build with Blender's own components:
from blenderui.components import App, Panel, NumberSlider, VectorField, Checkbox, ColorSwatch, Button
App("Object", (420, 520), Panel("Transform", VectorField("Location", (0.0, 1.25, 0.0)), NumberSlider("Scale", 1.0, min=0, max=10, on_change=print)), Panel("Shading", Checkbox("Shadows", True), ColorSwatch("Color", (0.8, 0.3, 0.1, 1.0))), Button("Apply", on_click=lambda: print("apply")), ).run()

A Render Properties editor from the components (examples/site/render_props.py)
There are 45 components: layout, values, buttons, views, menus and pie menus, the color picker, the color ramp, curve and node editors, areas and templates. Reference: docs/components.md.
Write Qt-style Python. The import is the only line that differs from PySide or PyQt, and neither is needed:
import sys from blenderui.qtsyntax import (QApplication, QWidget, QVBoxLayout, QFormLayout, QLineEdit, QComboBox, QSlider, QProgressBar, QPushButton, QLabel, Qt)
app = QApplication(sys.argv) window = QWidget() window.setWindowTitle("Export") layout = QVBoxLayout(window)
form = QFormLayout() kind = QComboBox() kind.addItems(["glTF", "FBX", "OBJ"]) quality = QSlider(Qt.Horizontal) quality.setRange(0, 100) form.addRow("Format:", kind) form.addRow("Quality:", quality) layout.addLayout(form)
bar = QProgressBar() quality.valueChanged.connect(bar.setValue) layout.addWidget(bar)
status = QLabel("Ready.") go = QPushButton("Export") go.clicked.connect(lambda: status.setText("Exporting as " + kind.currentText())) layout.addWidget(status) layout.addWidget(go)
window.show() sys.exit(app.exec())

Qt syntax (examples/site/qt_material.py)
Qt syntax is partial (about 24% of Qt's method names on the main widget classes); anything missing raises AttributeError. Guide: docs/qt.md.
BUI's Rust crate has two first-class syntaxes, and they draw the same window:
rsx! is HTML-like markup inside a macro, with #[component] functions and hooks for state.div().flex().gap_3().child(...). State lives in your view struct, and cx.listener updates it. gpui code compiles against BUI too.In rsx! (this is bindings/rust/blenderui/examples/hello.rs; run it with cargo run --release --example hello in bindings/rust):
use blenderui::prelude::*;
#[component] fn counter(label: &str) { let (count, set_count) = use_state(0); rsx! { <div flex items-center gap-3 p-4 rounded-xl bg-zinc-800> <span text-zinc-300>{label.to_string()} ": " {count().to_string()} <div px-3 py-1 rounded-md bg-sky-700 hover:bg-sky-500 onClick={move || set_count(count() + 1)}> "Add one"
struct Hello;
impl Render for Hello { fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement { rsx! {
fn main() { Application::new().run(|cx| { cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| Hello)).unwrap(); }); }
In gpui builders, the same window:
use blenderui::prelude::*;
struct Hello { count: i32 }
impl Render for Hello { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let counter = div().flex().items_center().gap_3().p_4().rounded_xl().bg(rgb(0x27272a)) .child(span().text_color(rgb(0xd4d4d8)).child(format!("Clicks: {}", self.count))) .child(div().px_3().py_1().rounded_md().bg(rgb(0x0369a1)) .hover(|s| s.bg(rgb(0x0ea5e9))).cursor_pointer() .child("Add one") .on_click(cx.listener(|this, _, _, cx| { this.count += 1; cx.notify(); }))); body().m_0().p_6().css("min-height", "100vh").flex().flex_col().gap_4() .bg(rgb(0x18181b)).text_color(white()) .child(h1().text_2xl().font_bold().child("Hello, BUI")) .child(counter) .child(NumberSlider().min(0.0).max(10.0).value("5")) // a real Blender slider } }
fn main() { Application::new().run(|cx| { cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| Hello { count: 0 })).unwrap(); }); }
Two small things that trip people up in rsx!:
&'static str, String, SharedString) or elements. Numbers and borrowed &str props aren't, so write {count().to_string()} and {label.to_string()}.rsx!.Here's a bigger example, a render-farm dashboard, written once in each syntax. The two programs draw identical pixels:

site_dashboard.rs (rsx!) and site_dashboard_builder.rs (gpui builders)
Guide: docs/rust.md, and the Rust tutorial on the website.
/** @jsxImportSource @blender-ui/react */ import { render, useState, NumberSlider } from "@blender-ui/react"
function Counter() { const [count, setCount] = useState(0) // React's hook, holding a signal return ( <main styled className="p-6 flex flex-col gap-3"> {" "} {/* `styled`: Blender's look */} <span>Clicks: {count}</span> <button onClick={() => setCount((c) => c + 1)}>Add one</button> <NumberSlider min={0} max={10} /> {/* a Blender component */} </main> ) }
render(
bun app.jsx # run it (Bun reads JSX directly) npx bui build app.jsx # or ship it: app.exe, one 7 MB file
Without React, @blender-ui/native gives you a DOM-like API:
const { App, signal } = require("@blender-ui/native")
const app = new App({ gpu: true }) const win = app.window({ title: "Hello", width: 360, height: 200 }) win.body.className = "m-0 p-6 flex flex-col gap-3 bg-[#2b2b2b] text-[#e6e6e6]"
const clicks = signal(0) const label = win.createElement("span") label.textContent = clicks // follows the signal const button = win.createElement("button") button.textContent = "Add one" button.addEventListener("click", () => clicks.value++) win.body.appendChild(label) win.body.appendChild(button)

A task list in React (examples/site/js/todo_react.js); the DOM-like version draws the same
Node.js 18+ or Bun. Prebuilt for Windows x64; on Linux, build the addon from source (npm run build in bindings/node) and windows are native. Guide: docs/javascript.md.
The real Dear ImGui 1.93 runs on BUI: BUI's windows, input, CPU/GPU renderer, Blender's theme and the Inter font. Existing ImGui code compiles unchanged.
C++:
#include "imgui.h" #include "imgui_impl_bui.h" // see examples/imgui_demo.cpp
static void frame(void*) { ImGui::ShowDemoWindow(); ImGui::Begin("Hello"); static float v = 0.5f; ImGui::SliderFloat("Value", &v, 0.0f, 1.0f); ImGui::End(); }
int main() { ImGui::CreateContext(); ImGui_ImplBUI_App* app = ImGui_ImplBUI_CreateApp("Demo", 1280, 800, frame, nullptr); ImGui_ImplBUI_RunApp(app); ImGui_ImplBUI_DestroyApp(app); ImGui::DestroyContext(); }
Python:
import blenderui.imgui as imgui # C: cimgui.h · Rust: the blenderui-imgui crate
state = {"value": 0.5}
def frame(): imgui.show_demo_window() imgui.begin("Hello") _, state["value"] = imgui.slider_float("Value", state["value"], 0.0, 1.0) imgui.end()
imgui.run(frame, title="Hello", size=(1280, 800))
C (cimgui.h, cimgui conventions: ImGui_Button, ImGui_ButtonEx, and the backend in cimgui_impl_bui.h) and Rust (blenderui-imgui) work the same way:
use blenderui_imgui as imgui;
fn main() { let mut value = 0.5f32; imgui::run("Hello", [1280, 800], move || { imgui::show_demo_window(); imgui::begin("Hello"); imgui::slider_float("Value", &mut value, 0.0, 1.0); imgui::end(); }); }
Build with -DBUI_IMGUI_BLENDER_WIDGETS=ON (or the Rust feature blender-widgets) and ImGui's widgets become Blender's own: DragFloat is Blender's drag-to-scrub number field (Shift for precision, Ctrl to snap), SliderFloat Blender's number slider, checkboxes drag-toggle, menus and combos get Blender's menus. The rest of the ImGui API is unchanged. (The PyPI wheel is built without it, so from Python you get ImGui's own widgets in Blender's theme unless you build from source.)

Dear ImGui's own demo with BUI_IMGUI_BLENDER_WIDGETS=ON: number fields, sliders, color rows and panel headers are Blender's

The same ImGui code: ImGui's widgets (left) and the Blender-widgets build (right) (examples/site/imgui_inspector.cpp)
Guide, coverage and the stock look: docs/imgui.md.
The kit's API is one header, kit/include/BUI_kit.h, and it doubles as the reference: every function has a one-line comment. This is a shortened examples/kit_hello.c: a slider and a button in a native window.
#include <stdio.h> #include "BUI_kit.h"
static void on_event(void *data, BKNode *node, int kind, void *ref, double a, double b) { if (kind == BK_EV_CLICK) printf("clicked %s\n", BK_get_str(node, "text")); else if (kind == BK_EV_CHANGE) printf("%s = %g\n", BK_get_str(node, "label"), BK_get_num(node, "value")); }
int main(void) { BKNode *root = BK_node_new("frame"); /* everything is a node */ BK_set_num(root, "padding", 8); /* configured by property name */
BKNode *size = BK_node_new("slider"); BK_set_str(size, "label", "Size"); BK_set_num(size, "max", 10); BK_set_num(size, "value", 2.5); BK_node_add(root, size, -1); BK_geom_set_manager(size, BK_GEOM_PACK); /* Tk's packer, in C */ BK_geom_set(size, "fill", 1);
BKNode *apply = BK_node_new("button"); BK_set_str(apply, "text", "Apply"); BK_node_add(root, apply, -1); BK_geom_set_manager(apply, BK_GEOM_PACK);
BKContext *ctx = BK_context_new(root); /* input, focus, popups, undo */ BKHost host = {0}; host.emit = on_event; /* the window fills in the rest */ BK_context_set_host(ctx, &host);
if (BK_window_supported()) { /* Win32, X11 or Cocoa */ BKWindow *win = BK_window_new(ctx, "BUI from C", 320, 120); BK_window_show(win); BK_window_run(); BK_window_free(win); } BK_context_free(ctx); BK_node_free(root); return 0; }
cmake -S . -B build -DBUI_BUILD_EXAMPLES=ON cmake --build build --target bui_kit_hello

A bigger C program: a modifier stack (examples/site/kit_modifiers.c)
Without a window system, render into a raster instead (BK_render(ctx, raster, w, h, full)), which is also how tests and screenshots work. Guide: docs/c-api.md.
your code: tkinter Qt components Dear ImGui (C++/C/Python/Rust) Rust: rsx! / HTML / CSS / Tailwind / hooks JS / React
│ │ │ │ │ │
└──── Python: options, signals │ ImGui core + BUI backend └── style cascade + taffy layout ◄────┘
│ │ │
C kit (kit/): node tree · geometry · input & interaction · Blender widget drawing · undo · animation · themes · Blender search/expr
│
renderer: damage-driven software raster (parallel) or GPU (Direct3D 11 / OpenGL 3.3, instanced, opt-in)
│
window: native Win32 / X11 / Cocoa window · stdlib Tk window · headless (tests, screenshots)
What's under the hood:
interface_widgets.cc (round_box_edges, widgetbase_draw, the shape tables), and interaction follows interface_handlers.cc. Color math, rects, text cursors, string search and expression evaluation are Blender's blenlib, copied in. A few interface files (string search, text-edit undo, float precision) are copied into kit/intern/blender/ and edited for the kit. The X11 and Cocoa hosts follow Blender's GHOST; the Win32 host is BUI's own.More: ARCHITECTURE.md and How it works.
These rules live in the C kit, so they work the same everywhere Blender widgets are drawn: the Python components, tkinter and Qt syntax, the Rust and JavaScript Blender components and form controls inside styled, ImGui's Blender-widgets build, and every C kit widget.
expr_pylike_eval.cc) turns 2*pi, 90/4, min(4, 3) or x if c else y into numbers. This works in Blender number fields and sliders; plain text fields keep what you type as text.- negates, Ctrl+Wheel steps, Ctrl+C / Ctrl+V copy and paste, right-click opens Copy, Paste and Reset.Coming soon: typing values with units ("2 m"), Blender's search-as-you-type popups, and widget drag and drop. More: Blender's widgets.
In Rust and JavaScript you pick the look per element:
| You write | You get |
|---|---|
Plain HTML: <button>, <input type="range">, <select> ... |
What a browser draws: black text on white and Chrome's form controls, in the Inter UI font |
styled on any element: <div styled>, div().styled() |
Blender's look for it and everything inside: Blender's theme (dark or light), UI font, Blender widgets |
Blender components (capitalized): <NumberSlider>, <Button> ... |
Blender's widgets and interaction everywhere (drag to scrub, Ctrl+Z, Blender's color picker) |
The components are Button, Checkbox, Radio, NumberField, NumberSlider, TextField, TextArea, ColorSwatch, Dropdown and ProgressBar. Each one is the HTML element it stands for, so every attribute, style and event works on it:
rsx! {
<main class="p-4 flex gap-2"> // browser look
<input type="range" /> // Chrome's slider
import { render, NumberSlider, Button } from "@blender-ui/react"
//

cargo run --example forms: plain HTML, inside styled, and the Blender components

The same markup, plain (left) and inside styled (right). Plain HTML looks the same in both themes.

Blender components from Rust (site_components.rs)
Python's Blender components (Panel, PieMenu, the node editor ...) are a separate, larger set.
BUI has two renderers, and they draw the same pixels.
gpu/intern/gpu_raster.c) is the default. It draws anti-aliased shapes with signed-distance coverage, like Blender's widget shaders, into one frame buffer, compares it with the last frame in 64×32 tiles, and reports only the tiles that changed. Big canvases draw in parallel bands on Windows (BUI_RASTER_THREADS). It needs nothing from your graphics card and uses the least memory.gpu/intern/gpu_d3d11.c) and OpenGL 3.3 core on Linux (gpu/intern/gpu_gl.c). It's modeled on Blender's instanced widget drawing: each primitive is one 128-byte instance of a single uber-shader, so a frame is a few instanced draw calls. Text and images are cached in atlases. Frames render into a persistent target, so only damaged regions redraw; on Windows, a flip-model swap chain presents dirty rectangles. On Linux, libGL, libEGL and libX11 are loaded at run time (GLX for windows, EGL for offscreen), so no GL headers are needed, and BUI falls back to the CPU where OpenGL is missing.The shaders evaluate the software raster's formulas: the parity test finds 10 of 172,800 pixels that differ by more than 3/255, on both backends.
Turn the GPU on:
| Where | How |
|---|---|
| Python | bui.set_gpu(True) before creating windows; tkinter syntax also tk.Tk(gpu=True) |
| Rust | Application::new().gpu(true), or WindowOptions { gpu: Some(true), .. } |
| JavaScript | new App({ gpu: true }), or React's render(..., { gpu: true }) |
| C | BUI_gpu_set_default(1), or BK_window_set_gpu(win, 1) |
| Any app, no code change | BUI_GPU=1 |
BUI_GPU=1 overrides the app-wide setting in code; BUI_GPU=0 turns the app-wide defaults off, but a window that asks for the GPU itself still gets it. Where no device can start, BUI quietly stays on the CPU (BUI_GPU_DEBUG=1 says why).
Which one? A tool window, form or settings panel: the CPU (lightest on memory, idles at zero). A large or HiDPI window, lots of animation, thousands of items: the GPU (about half the CPU per frame, or less, for about 10 MB more RAM plus GPU memory). Servers and tests: the CPU. macOS: the CPU, for now (a GPU renderer there is coming soon).
More: Rendering.
Leave a BUI window open and walk away: it uses no CPU at all. Hover a button, and BUI redraws that button, not the window.
XPutImage, Cocoa invalidates them, the GPU presents dirty rectangles).On the full component demo, that's about 0.7 ms for a complete frame and about 0.05 ms for a hover or number-scrub frame. Animations run only while something moves, paced to the display. More: Damage rendering.
A window host puts a kit context in a top-level window and feeds it input.
| Host | Where | Notes |
|---|---|---|
Win32 (kit_window_win32.c) |
Windows | BUI's own host. Per-monitor DPI, vsync-paced animation, clipboard, cursors, window-manager features. |
X11 (kit_window_x11.c) |
Linux; Wayland through XWayland | Input mapping after Blender's GHOST. Xft.dpi scaling. libX11 is loaded when the first window opens. |
Cocoa (kit_window_cocoa.m) |
macOS | Experimental. Ported from Blender's GHOST Cocoa code, with Retina scaling. It's checked by CI and has been tried by hand on one Apple Silicon Mac so far; testing notes in docs/macos-testing.md. |
| Tk | Python, anywhere | A stdlib tkinter window that shows the kit's frame buffer. BUI_HOST=tk forces it; it's also the fallback where no native host works. |
| Headless | Everywhere | No window: render into a raster for tests and screenshots, with a virtual clock that steps animations exactly. |
BUI_UI_SCALE=1.5 fixes the UI scale instead of following the display (1 is Blender's default, 20 px widgets). More: Window hosts.
Your app's text looks the same on Windows, Linux and macOS, and it looks like Blender's:
BUI_TEXT_FREETYPE=OFF, stb_truetype rasterizes instead.BUI_SLUG=0, bui.set_slug(False), .slug(false), BUI_gpu_set_slug(0)).BUI_FONT=path.ttf replaces Inter, BUI_FONT_DIR adds a folder to search first, blenderui::add_font (Rust) and BUI_text_add_font (C) register faces from memory, and CSS @font-face loads local files.-DBUI_TEXT_GDI=ON, Rust feature gdi-text): ClearType-style hinting and system fonts, without Slug.Limits worth knowing: no per-glyph font fallback yet (missing characters show as boxes), no IME, right-to-left text is reordered but not shaped, no OpenType features. Font fallback and IME are coming soon. More: Text and fonts.
BUI looks like Blender because it uses Blender's theme. It ships two: Blender's Dark theme (the default) and its Light preset. Both are generated by tools/gen_theme.py from Blender's own Blender_Dark.xml and Blender_Light.xml presets, over Blender 5.x's built-in defaults. Switch at run time, and every open window relayouts and redraws:
| Where | Light theme |
|---|---|
| C | BK_set_theme("light"); BK_theme_name(), BK_theme_color("region_bg", rgba) |
| Python | blenderui.set_theme("light") (also switches the Dear ImGui extension); theme_name(), theme_color(key) |
| Rust | blenderui::set_theme("light"), or Application::new().theme("light"); theme_name(), theme_color(key) |
| JavaScript | new App({ theme: "light" }), app.setTheme("light"), or React's render(..., { theme: "light" }) |
| Dear ImGui | Its Blender style follows the kit's theme (pick it before the backend starts) |
| Any app, no code change | BUI_THEME=light (or dark) |
Widgets, panels, the region background, the styled HTML sheet and ImGui's Blender style all follow the theme. Plain HTML keeps a browser's look either way. Inside each theme you can still color things yourself: bg / fg in tkinter, setStyleSheet in Qt syntax, color= on components, full CSS on HTML in Rust and JavaScript, ImGui's own style colors.
BUI also includes Blender's icons: all 781 of Blender's SVG icons, rasterized with Blender's NanoSVG code so they're sharp at any scale. Use them by Blender's name: Button("Add", icon="ADD"), EnumRow(..., icons=[...]).
Coming soon: loading any Blender theme file, beyond Dark and Light. More: Theming.
pip install blenderui gives you four APIs in one package:
blenderui.tksyntax): every public tkinter and ttk name (7,846 of 7,846). Run existing programs with blenderui-tk app.py, or call blenderui.tksyntax.install() first. messagebox and simpledialog are Blender-styled, colorchooser is Blender's color picker, and filedialog is Blender's File Browser (BUI_FILEDIALOG=native for the OS dialogs). Look-only Tk options (relief, ttk.Style ...) are accepted, but the look comes from Blender's theme. Guide: docs/tkinter.md.blenderui.qtsyntax): widgets, layouts (box, grid, form, stacked), signals, QPainter, menus, timers and animations, QFileDialog as Blender's File Browser. Partial: model/view classes, docks, toolbars, splitters and Qt stylesheets are missing. Guide: docs/qt.md.blenderui.components): 45 components, from NumberSlider to NodeEditor. Reference: docs/components.md.blenderui.imgui), pyimgui-style. Guide: docs/imgui.md.You can mix them in one process: windows, timers and the event loop are shared (bui.host). Every interaction runs in C, so tests can drive widgets headless. On the website: tkinter, Qt syntax, components, headless testing.
The blenderui crate is a web-like framework drawn by BUI: every HTML element, CSS, Tailwind, HTML-like markup (rsx! / view!) or gpui-style builders, components and hooks. A taste:
rsx! { // Tailwind as attributes, any variant, arbitrary values; typos fail to compile <button px-3 py-1 rounded-md bg-zinc-800 hover:bg-sky-600 md:dark:hover:bg-sky-400 active:style="transform: scale(0.97)" onClick={move || save()}> "Save"
// control flow right in the markup
<ul>{for item in items { <li class="py-1"\>{item}</li> }}</ul>
{if dirty { <p text-amber-400\>"Unsaved changes"</p> }}
// real CSS, evaluated by the engine
<div style="width: calc(100% - 2 \* var(--gap)); background: color-mix(in oklch, var(--brand) 60%, white)" />
}
error: rsx!: `bg-sky-50O` is not a Tailwind utility; did you mean `bg-sky-500` or `bg-sky-50` or `bg-sky-100`?
What's in the box:
grid-template-areas and named lines), tables, relative / absolute / fixed positioning (fixed boxes are placed against the window), scrolling, and bidirectional text.@layer / @media / @supports / @scope / @container / @font-face / @property, var() and env(), every math function, CSS Color 4 and 5.@keyframes animations and transitions (every longhand, cubic-bezier(), steps(), linear(), interruptible transitions), filter and backdrop-filter, mix-blend-mode, clip-path, masks, 2D transforms, border-image. Frames render only while something moves, and headless windows step a virtual clock (App::advance_clock).rsx! and tw!.styled, and the theme.Application::headless(), screenshots, simulated input.Not yet: async resources (use_resource), generic components, 3D transforms, OpenType features, multi-column layout. Guide: docs/rust.md, crate README: bindings/rust/blenderui, and the website's Rust section (including effects and animations).
@blender-ui/react: a React 19.3 renderer. HTML host components, CSS and Tailwind, events, Suspense, transitions, actions, and the Blender components. Hooks keep React's names but hold signals: pass one into JSX and a change updates that node without re-rendering. README: bindings/react.@blender-ui/native: a DOM-like API (createElement, appendChild, addEventListener, className with Tailwind, style) with signals, memo, effect and batch. README: bindings/node.npx bui build app.jsx makes a ~7 MB executable with an embedded QuickJS engine; --bun makes a Bun executable instead; npx bui run app.jsx runs on QuickJS without building.new App({ headless: true }) renders off-screen, and win.click(x, y) simulates input.Limits: npm packages are prebuilt for Windows x64 only; events reach JavaScript after BUI handled them (no native preventDefault), and event.target is the deepest listening element. Guide: docs/javascript.md; website: React, native API, signals, standalone executables.
| Language | What you use | Built by |
|---|---|---|
| C++ | imgui.h + imgui_impl_bui.h |
CMake target bui_imgui (bui::imgui) |
| C | cimgui.h + cimgui_impl_bui.h |
CMake target bui_cimgui |
| Python | import blenderui.imgui as imgui |
The _buiimgui extension, in the wheel |
| Rust | the blenderui-imgui crate (on crates.io) |
Cargo |
The C, Python and Rust bindings are generated from imgui.h. ImGui is idle-free on BUI: frames run only on input and while something animates. The Blender-widgets build (BUI_IMGUI_BLENDER_WIDGETS=ON, Rust feature blender-widgets) swaps in Blender's widgets, and can be switched at run time (ImGui_ImplBUI_SetBlenderWidgets, imgui.set_blender_widgets). In that build, window title bars, tooltips, plain popups, tables and the color picker keep ImGui's drawing.
Guides: docs/imgui.md, bindings/imgui/README.md (the backend and the Blender style), bindings/imgui/blender/README.md (the widget mapping), and the website's Dear ImGui section.
The kit's concepts: nodes made by type name (frame, slider, button, panel, tree, canvas, filebrowser ...), properties set by name, Tk's geometry managers (pack, grid, place) in C, a context that owns input, popups, focus and undo, a host that receives events, and windows. A custom node draws and handles input itself; Dear ImGui runs on BUI that way. Guide: docs/c-api.md.
To embed BUI in your own renderer, render the kit into a BuiRaster and blit it (as bui.qt.BuiCanvas does inside Qt). A separate, lower-level layer, render sinks (api/bui_c_api.h), hands the triangles, lines and text of the immediate-mode GPU seam to your renderer; the Qt/QPainter backend and the SVG recorder use it. Guide: INTEGRATION.md.
| Guide | What's inside |
|---|---|
| Website | Everything below, as tutorials, concepts and references, with screenshots in dark and light |
| Getting started | Building, environment variables, window hosts, GPU rendering, troubleshooting |
| tkinter | Running tkinter apps on BUI, behavior notes, limitations |
| Qt syntax | Classes, signals, layouts, painting, coverage |
| Components | The bui.components reference |
| Rust | rsx!, hooks, HTML, CSS and Tailwind coverage, effects, animations, events, headless tests |
| JavaScript and React | @blender-ui/native, @blender-ui/react, how changes cross to the UI thread, performance |
| Dear ImGui | Dear ImGui on BUI from C++, C, Python and Rust; the Blender-widgets build; coverage |
| C API | The C kit (BUI_kit.h) |
| Integration | Embedding BUI drawing in C, C++, Qt or PySide hosts |
| Architecture | Modules and what was taken from Blender |
| Syntax coverage | The Qt and tkinter surface, exactly |
| macOS testing | How to try the experimental Cocoa host on a Mac |
| Publishing | Releasing to PyPI and crates.io |
| Changelog | What changed |
These numbers were measured on a Ryzen 7 5800U laptop (Windows 11, on AC power) before the latest features landed: Dear ImGui, the Linux GPU renderer, the light theme, and the Blender search and expression code. They show the shape of things, not a promise for your machine. Rerun them with the tools in Run the benchmarks yourself.
The same small settings window (a label, text field, checkbox, slider and button) written each of the five ways, from bench/apps, on BUI's CPU renderer (the default) and its GPU renderer (BUI_GPU=1). The last rows are one gpui-syntax source file, settings.rs, compiled unchanged against blenderui, Zed's gpui and gpui-fast. gpui has no form widgets, so there the controls are styled divs.
Median of 10 launches, from process start until the first frame is on screen, with Python 3.14 and Node 22:
| Renderer | Start time | CPU used to start | Memory (idle) | Idle CPU per minute | |
|---|---|---|---|---|---|
| Rust (HTML + CSS) | CPU | 38 ms | 27 ms | 13 MB | 0 ms |
| GPU | 42 ms | 61 ms | 31 MB | 1.8 ms | |
| JavaScript (React, Node.js) | CPU | 94 ms | 100 ms | 48 MB | 68 ms |
| GPU | 99 ms | 131 ms | 60 MB | 71 ms | |
| Blender components (Python) | CPU | 112 ms | 86 ms | 23 MB | 0 ms |
| GPU | 116 ms | 125 ms | 32 MB | 1.7 ms | |
| tkinter syntax (Python) | CPU | 139 ms | 112 ms | 25 MB | 0.2 ms |
| GPU | 159 ms | 157 ms | 43 MB | 2.4 ms | |
| Qt syntax (Python) | CPU | 156 ms | 123 ms | 27 MB | 0 ms |
| GPU | 161 ms | 160 ms | 36 MB | 1.2 ms | |
| Same gpui code: blenderui | CPU | 40 ms | 27 ms | 15 MB | 0 ms |
| blenderui | GPU | 43 ms | 60 ms | 31 MB | 2.1 ms |
| gpui | GPU | 231 ms | 226 ms | 29 MB | 743 ms |
| gpui-fast | GPU | 226 ms | 217 ms | 31 MB | 2.5 ms |
On the same code, blenderui shows its first frame about 5× sooner than gpui and gpui-fast, spends about 8× less CPU getting there, and on the CPU renderer uses half the memory. Idle, gpui keeps about 1.2 % of a core busy; blenderui uses none.
Laptops can't report energy per app, so battery cost is measured by the CPU time an app causes: the CPU it spends starting up, and the CPU it uses over 20 seconds while the window sits idle (what drains a battery while an app is open). BUI redraws only when something changes and otherwise sleeps, so idle use is near zero on either renderer. The GPU renderer starts a little slower and uses more memory (the GPU device), and pays off on large, animated scenes (below). Node's event loop accounts for the JavaScript idle figure, Python's times include starting the interpreter, and memory doesn't count GPU memory.
Python and C, on the full component demo (520×900):
| Frame | Time |
|---|---|
| Complete frame (everything redrawn) | ~0.7 ms |
| Hover or number-scrub frame (damage only) | ~0.05 ms |
| Native window: input event → on screen | ~0.06 ms |
Dear ImGui's demo window with every section open (27k triangles, 1280×800) is a full software redraw of about 5–8 ms.
Rust, against Zed's gpui and the faster fork gpui-fast. The scene is 3000 animated cells, and all apps ran in the same session (GPU rows on Direct3D 11):
| blenderui (GPU) | blenderui (CPU) | gpui-fast | gpui | |
|---|---|---|---|---|
| FPS | 59.9 | 57.6 | 31.5 | 19.9 |
| CPU per frame | 11.6 ms | 22.0 ms | 31.9 ms + GPU | 50.5 ms + GPU |
| RAM | 40 MB | 30 MB | 65 MB | 77 MB |
| First frame | ~5 ms | ~5 ms | ~200 ms | ~220 ms |
| Idle CPU | 0 % | 0–0.9 % | 0–0.4 % | 0.9–1.4 % |
Full results, the GPU renderer, and the method
| blenderui (GPU) | blenderui (CPU) | gpui-fast | gpui | |
|---|---|---|---|---|
| FPS, 1000 animated cells | 60 | 60 | 49.2 | 36.4 |
| CPU per frame, 1000 / 100 cells | 4.7 / 1.2 ms | 10.2 / 4.6 ms | 20.5 / 2.7 ms + GPU | 27.7 / 4.7 ms + GPU |
| Uncapped FPS, 100 / 1000 / 3000 cells | 1491 / 309 / 94 | 649 / 202 / 76 | - | - |
| RAM, 1000 / 100 cells | 35 / 28 MB | 24 / 21 MB | 45 / 47 MB | 47 / 40 MB |
| GPU memory | 19–22 MB | none | 19–24 MB | 16–21 MB |
The GPU renderer is off by default. Turn it on with Application::new().gpu(true), tk.Tk(gpu=True), bui.set_gpu(True) or BUI_GPU=1:
Its shaders evaluate the software raster's formulas: the parity test finds 10 of 172,800 pixels that differ by more than 3/255, on both backends.
gpui-fast's heavy-scene frame rate varies between sessions on this laptop (30–60 fps), so compare within one session. Method, raw data and how to rerun: bindings/rust/bench/README.md.
Startup. These launch a small React app (the counter above) that quits once its first frame is on screen. Each figure is the median of 12 launches after a warm-up launch (node bench/startup.js).
| First frame | Launch to exit | RAM at first frame | What you ship | |
|---|---|---|---|---|
QuickJS executable (bui build) |
68 ms | 104 ms | 19 MB | 7.3 MB |
Bun executable (bui build --bun) |
79 ms | 147 ms | 35 MB | 114 MB |
Bun (bun app.js) |
80 ms | 152 ms | 35 MB | Bun + your app |
Node.js (node app.js) |
108 ms | 187 ms | 45 MB | Node + your app |
| Rust (for reference: the benchmark app) | 5 ms | ~92 ms | 3.5 MB |
First frame is measured from the runtime's start. Launch to exit is wall time measured from outside, including process creation and teardown.
Animation. The same scene as the Rust table above, in one mode: one cell changes per frame, GPU renderer, medians of 3 runs. CPU is the whole process, counted with the cycle counter as for Rust.
@blender-ui/native.| 3,000 cells | FPS | CPU per frame | RAM |
|---|---|---|---|
| Rust | 59.6 | 12.0 ms | 43 MB |
| QuickJS: plain JS / React with signals / React with state | 59.1 / 58.7 / 19.7 | 16.3 / 14.9 / 51.0 ms | 56 / 61 / 67 MB |
| Bun 1.4: plain JS / React with signals / React with state | 58.8 / 58.4 / 55.8 | 13.1 / 13.3 / 20.8 ms | 80 / 85 / 118 MB |
| Node 22: plain JS / React with signals / React with state | 55.8 / 56.7 / 54.8 | 14.4 / 14.0 / 18.9 ms | 88 / 93 / 121 MB |
| 1,000 cells | FPS | CPU per frame | RAM |
|---|---|---|---|
| Rust | 60 | 4.8 ms | 32 MB |
| QuickJS: plain JS / React with signals / React with state | 60 / 60 / 56.8 | 6.0 / 5.6 / 17.8 ms | 38 / 40 / 43 MB |
| Bun: plain JS / React with signals / React with state | 60 / 60 / 60 | 5.2 / 5.2 / 7.5 ms | 58 / 63 / 81 MB |
| Node: plain JS / React with signals / React with state | 60 / 60 / 60 | 5.6 / 5.6 / 6.9 ms | 68 / 71 / 97 MB |
What this means:
--bun.Method, the memory breakdown and how to rerun: docs/javascript.md.
Every number above comes from a tool in this repo. Build in release mode, close other heavy apps, and plug the laptop in. Run one benchmark (and one build) at a time.
Start time and battery: the five ways and gpui (bench/, Windows). The same settings window written each way, on the CPU and the GPU renderer, plus one gpui-syntax file compiled for blenderui, gpui and gpui-fast. It reports start time, CPU used to start, memory and idle CPU, and writes bench/results.json.
cmake -S . -B build && cmake --build build --config Release --target buipy # Python extension cd bindings/node && npm run build && cd ../.. # Node addon cd bindings/rust && cargo build --release --example bench_app && cd ../.. # Rust app cd bindings/rust/bench && cargo build --release --bins && cd ../../.. # gpui comparison py -3 bench/run.py 10 20 # launches per app, seconds of idle measurement
Frame cost: 100, 1,000 and 3,000 animated cells, against gpui and gpui-fast (bindings/rust/bench/, Windows). A 1000×800 window filled with N rounded, numbered cells, in three modes:
Each app measures its own CPU cycles, frames and memory, and run.py adds GPU utilisation and GPU memory from Windows' performance counters.
cd bindings/rust/bench set GPUI_FXC_PATH=C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\fxc.exe # gpui needs fxc.exe cargo build --release py run.py --reps 3 # all apps, 100 / 1000 / 3000 cells, about 12 minutes py run.py --cells 3000 --apps blenderui,blenderui-gpu --reps 1 target\release\bui-bench --cells 3000 --mode anim # one run by hand (--mode one / idle, --uncapped) set BUI_GPU=1 && target\release\bui-bench --cells 3000 --mode anim # on the GPU renderer
Method and full results: bindings/rust/bench/README.md.
JavaScript: the same cells, and startup (bindings/node/bench/):
cd bindings/node node bench/cells.js 3000 # DOM-like API, one cell changing per frame node bench/cells.js 3000 --react # React re-rendering the grid node bench/cells.js 3000 --react --signals # React with signals: no re-render node bench/cells.js 3000 --gpu # on the GPU renderer bun bench/cells.js 3000 # on Bun node cli.js run bench/cells.js -- 3000 # on the 5 MB QuickJS runtime node bench/startup.js # launch to first frame: Node, Bun and both executables
tkinter: canvas frame cost (headless, any platform):
py bindings/python/bench/tk_canvas_bench.py 3000 all # move 3,000 canvas items every frame py bindings/python/bench/tk_canvas_bench.py 3000 one # move one
Inside any app: run it with BUI_PROFILE=1 to print per-phase frame times (layout, paint, raster, present) every 60 frames; any value turns it on. Use BUI_GPU=1 / BUI_GPU=0 to pick the renderer. More: Profiling.
| Windows | Linux | macOS | |
|---|---|---|---|
| Window | Native (Win32) | Native (X11; Wayland through XWayland) | Native (Cocoa), experimental |
Python (pip install) |
Works: wheels | Works: wheels (native X11 host) | Works: wheels |
Rust (cargo add blenderui) |
Works | Works | Experimental |
| JavaScript / React | Works: prebuilt on npm | Works: build the addon from source (npm run build); native X11 windows |
Headless (see below) |
| C, C++, Dear ImGui | Works | Works | Experimental |
| Renderer | CPU raster, or GPU (Direct3D 11) | CPU raster, or GPU (OpenGL 3.3) | CPU raster |
| HiDPI | Per-monitor DPI | Xft.dpi |
Retina (backing scale) |
Works means it works and is tested. Experimental means it's written but has had little hands-on testing on real hardware (macOS: one Apple Silicon Mac so far, plus CI).
macOS. The macOS window host is new: it's a port of Blender's Cocoa code (GHOST). So far it has been checked by CI and tried by hand on one Apple Silicon Mac by Krish Kalaria, who reported issue #3 and fixed it in pull request #4. If you have another one, please try it and tell us what happens: docs/macos-testing.md. Python can still use the stdlib Tk window there (BUI_HOST=tk; this needs Python's tkinter, for example from python.org or brew install python-tk). Node runs BUI on its own UI thread and macOS only allows windows on the main thread, so JavaScript stays headless on macOS for now.
Linux. libX11 and libGL are loaded when the first window opens, so BUI also imports and renders headless on servers and in containers without them, and falls back to the CPU raster where OpenGL is missing. Building from source needs the X11 headers (libx11-dev / libX11-devel); no GL headers are needed. The C, Python, window and GPU parity tests pass on Linux, on Mesa's software OpenGL and on an NVIDIA GPU.
Packages:
| Package | Platforms | Notes |
|---|---|---|
blenderui on PyPI |
Windows x86_64, Linux x86_64 (manylinux), macOS arm64 and x86_64 | CPython 3.11 to 3.14. No musllinux or 32-bit wheels. |
blenderui on crates.io |
Any platform with CMake and a C compiler | Rust 1.88+. blenderui-imgui (Dear ImGui) is on crates.io too. |
@blender-ui/native, @blender-ui/react on npm |
Windows x64 | Node.js 18+ or Bun. Elsewhere, build the addon from source. |
| C and C++ (CMake) | Windows, Linux, macOS | CMake 3.20+, a C11 and C++20 compiler. |
Most are read by the C library, so they work the same from every language. On/off variables look at the first character: 1, y, Y, t or T is on. Where a variable and an app-wide API call set the same thing, the variable wins.
| Variable | What it does |
|---|---|
BUI_GPU |
1 draws native windows on the GPU (Direct3D 11 / OpenGL 3.3). Overrides bui.set_gpu(), Application::gpu(), BUI_gpu_set_default(). 0 turns those defaults off; a window that asks for the GPU itself still gets it. |
BUI_SLUG |
GPU text: 1 from glyph outlines (Slug, the default), 0 from a per-glyph atlas. |
BUI_GPU_DEBUG |
1 prints why the GPU renderer couldn't start. |
BUI_GPU_WARP |
1 (Windows) creates the Direct3D 11 device on WARP, the software rasterizer. Handy in VMs and CI. |
BUI_GPU_MAX_TEXTURE |
Testing: the largest texture side the GPU renderers give one image (at least 64), so images over it are tiled as on devices with a small limit. |
BUI_GPU_EGL |
default or surfaceless (Linux): the EGL platform for offscreen GPU rendering. |
BUI_RASTER_THREADS |
1 to 8 (Windows): the most threads the software raster uses for big canvases. |
BUI_PROFILE |
Prints frame times every 60 frames. Any value turns it on, even 0. |
BUI_THEME |
dark (default) or light: Blender's theme, when the app doesn't set one. |
BUI_UI_SCALE |
A fixed UI scale (1.25, 1.5 ...) instead of the display's. Clamped to 0.5–4. |
BUI_HOST |
tk (Python): use the stdlib Tk window even where a native host exists. |
BUI_MAC_TK |
0 (Python on macOS): skip starting a hidden Tk root before the first native window. |
BUI_FONT, BUI_FONT_DIR |
A font file to replace Inter, and an extra font folder to search first. |
BUI_FILEDIALOG |
native (or system, tk), Python: file dialogs use the OS's instead of Blender's File Browser. |
BUI_BOOKMARKS |
The File Browser's bookmarks file. |
BUIPY_BUILD_DIR |
Running Python from a source checkout: the folder holding the built buipy extension. |
BUI_NODE_ADDON |
@blender-ui/native: load this .node addon instead of the bundled one. |
BUI_HEADLESS |
1: bui build apps and bui run render off-screen (same as --headless). |
BUI_SOURCE_DIR, BUI_TEXT_GDI |
Build time, Rust: build the C library from this checkout; use GDI text. |
Every variable, with defaults and where it's read: Environment variables and docs/getting-started.md.
You need CMake 3.20+, a C11 / C++20 compiler (MSVC, GCC/MinGW or Clang) and Python 3.11+ with headers. On Linux, also the X11 headers.
git clone https://github.com/ppmpreetham/BUI && cd BUI cmake -S . -B build && cmake --build build -j 2 ctest --test-dir build # C, C++, ImGui and Python test suites
export PYTHONPATH=$PWD/bindings/python # Windows: set PYTHONPATH=%CD%\bindings\python python examples/components_demo.py
cd bindings/rust && cargo test # Rust: CSS, Tailwind, rsx!, hooks, effects, headless windows
| Option | Default | What it does |
|---|---|---|
BUI_BUILD_TESTS |
ON |
C test programs and the ctest suites |
BUI_BUILD_EXAMPLES |
OFF |
The C examples (bui_kit_hello, bui_svg_demo, the site screenshot programs) |
BUI_BUILD_PYTHON_BINDINGS |
ON |
The buipy extension (and the ImGui extension); skipped without Python's dev files |
BUI_BUILD_IMGUI |
ON with a C++ compiler |
Dear ImGui on BUI, its C API, demo and test |
BUI_IMGUI_BLENDER_WIDGETS |
OFF |
Dear ImGui with Blender's widgets |
BUI_BUILD_QT_BACKEND |
OFF |
The C++ Qt/QPainter render-sink backend (needs Qt 5 or 6) |
BUI_BUILD_UI |
OFF |
Also compile Blender's original ui/ sources (reference only) |
BUI_TEXT_FREETYPE |
ON |
Render glyphs with FreeType like Blender's BLF; OFF uses stb_truetype |
BUI_TEXT_GDI |
OFF |
Windows' GDI text and system fonts |
BUI_SLUG |
ON |
Slug as the GPU's default text method (switchable at run time) |
BUI_IMAGE_SVG |
ON |
SVG images through the kit's nanosvg (no extra code: Blender's icons already use it) |
BUI_WARNINGS_AS_ERRORS |
OFF |
GCC and Clang only: when OFF the build adds -Wno-error; it never adds -Werror or /WX itself |
Rebuild the docs screenshots with python tools/gen_doc_images.py (and the website's dark and light shots with --site). More: CMake options.
| API | Coverage | Status |
|---|---|---|
| tkinter and ttk | 7,846 of 7,846 public names; nine real programs behave identically | Complete |
| Qt syntax | About 220 of 920 public method names on 25 core widget classes (~24%) | Partial |
| Blender components (Python) | 45 components (10 of them also in Rust and JavaScript) | Complete |
| HTML elements (Rust, JavaScript) | 113 of 113 | Complete |
| CSS properties (Rust, JavaScript) | 261 of 745 W3C properties render, 129 more parse, 355 not recognized (mostly drafts, speech, paged media and SVG) | Partial |
| CSS 2.1 screen properties | 90 of 90 | Complete |
| Flexbox / Grid | 12 of 12 / 23 of 23 | Complete |
| Dear ImGui, C | 399 of 399 functions | Complete |
| Dear ImGui, Python and safe Rust | 366 of 399 functions (Rust FFI: 385) | Partial |
| Image formats (every language) | PNG, JPEG, WebP, GIF, BMP, TGA, QOI, HDR, PNM, SVG; not yet AVIF, EXR, TIFF, JPEG XL, HEIC | Partial |
How each was counted: Coverage, SYNTAX_COVERAGE.md, docs/rust.md, docs/imgui.md.
| Path | Contents |
|---|---|
kit/ |
The C runtime: nodes, input, widget drawing (the interface_widgets.cc port), menus, geometry managers, canvas, the File Browser, themes (kit_theme_presets.c), the Win32, X11 and Cocoa window hosts; kit/intern/image/ (images: decoders, cache, worker pool; third-party decoders in kit/extern/); kit/intern/blender/ holds Blender files copied in (string search, text-edit undo, float precision). API: kit/include/BUI_kit.h |
gpu/ |
Software raster, GPU renderer on Direct3D 11 (gpu_d3d11.c, shaders/) and OpenGL 3.3 (gpu_gl.c), textured meshes, Slug text, text engine (FreeType or stb_truetype + embedded Inter, or GDI) |
core/, types/ |
Blender's blenlib subset (including string_search.cc and expr_pylike_eval.cc) and data types |
ui/, compat/ |
Blender's original interface sources, kept for reference (not compiled by default; kit/ re-ports them in C), and the seams replacing Blender's application APIs |
api/ |
Stable C ABI for render-sink hosts |
bindings/python/ |
The buipy extension and the bui / blenderui packages (tkinter, Qt, components, ImGui) |
bindings/rust/ |
blenderui, blenderui-sys, blenderui-macros, blenderui-tailwind, blenderui-imgui, blenderui-jsview, blenderui-node, blenderui-qjs, and the benchmark |
bindings/node/, bindings/react/ |
@blender-ui/native and @blender-ui/react |
bindings/imgui/ |
Dear ImGui (vendored), the BUI backend, Blender style, the C API and its generator, the Blender-widgets build |
backends/qt/ |
C++ Qt/QPainter render-sink backend |
bench/ |
The start time and battery benchmark; bench/images/ (decoders, thumbnail grid) |
examples/, tools/ |
Demos, the website's screenshot programs (examples/site/), the icon and theme generators, the docs screenshot generator |
website/ |
The documentation website, bui.preetham.top |
| File | Shows |
|---|---|
examples/tk_unchanged_app.py |
A plain tkinter app: menus, notebook, form, table, text, canvas |
examples/components_demo.py |
Every Blender component in a properties-editor layout |
examples/qt_syntax_demo.py |
Qt syntax: layouts, form widgets, signals |
examples/tk_syntax_demo.py |
tkinter syntax laid out like a Blender properties editor |
examples/kit_hello.c |
The C kit: a slider and a button in a native window |
examples/imgui_demo.cpp |
Dear ImGui's demo window on BUI, from C++ |
examples/imgui_demo_c.c |
The same from C (cimgui.h) |
examples/imgui_blender_compare.cpp |
ImGui widgets next to BUI's native Blender widgets |
examples/site/ |
The website's screenshot programs: a Render Properties editor and a node editor (Python components), tkinter and ttk dialogs, a Qt material editor, an ImGui inspector (C++ and Python), a C modifier stack, and a JavaScript task list (DOM and React) |
hello.rs |
Rust: a component, hooks, Tailwind attributes and a Blender slider |
showcase.rs |
Rust: HTML, a stylesheet, flex/grid, tables, Blender form widgets |
forms.rs |
Rust: form controls as plain HTML, inside styled, and as Blender components |
site_dashboard.rs, site_dashboard_builder.rs |
Rust: the same dashboard in rsx! and in gpui builders |
site_effects.rs, site_grid.rs, site_animation.rs |
Rust: CSS effects, grid and @keyframes |
site_plain_html.rs, site_styled.rs, site_components.rs |
Rust: the three looks |
blenderui-imgui demo |
Dear ImGui from Rust (cargo run -p blenderui-imgui --example demo) |
bindings/node/examples/counter.js |
JavaScript: the DOM-like API |
bindings/react/examples/counter.jsx |
React in JSX (bun examples/counter.jsx) |
Done
| Area | What works |
|---|---|
C runtime (kit/) |
Nodes, Tk-style geometry managers, input, Blender widget drawing, damage rendering, undo, animation, text area, canvas, scroll views, Blender's string search, expression evaluator, text-edit undo and float precision, Blender's Dark and Light themes |
| Native windows | Windows (Win32) and Linux (X11; Wayland through XWayland), in C: DPI-aware, frame-paced animation, clipboard, cursors, window-manager features |
| tkinter syntax | Complete: every public tkinter and ttk name (7,846 of 7,846) |
| Components | 45 Blender components, including the color ramp, curve and node editors |
| Rust | HTML (113/113 elements), Flexbox 12/12, Grid 23/23, CSS 2.1 90/90, math functions, var(), CSS Color 4/5, Selectors 4, @layer / @media / @supports / @scope / @container / @font-face / @property, @keyframes animations and transitions, filters / blend modes / masks / clip-path / 2D transforms / border-image, bidi text, background images, object-fit, Tailwind v4, rsx!, #[component] with optional props and key, hooks, Blender components, browser-look form controls, headless testing |
| Dear ImGui | The real Dear ImGui 1.93 on BUI (CPU, Direct3D 11, OpenGL), Blender theme and Inter; C++ / C (399/399 functions) / Python and Rust (366–385/399); the Blender-widgets build |
| Rendering | Software raster (parallel, damage-tracked), GPU renderer (Direct3D 11 on Windows, OpenGL 3.3 on Linux), textured meshes, portable text engine (FreeType, as in Blender) |
| JavaScript / React | @blender-ui/native: a DOM-like API over napi-rs, with its own UI thread, only changes crossing the boundary, vsync-paced requestAnimationFrame and signals. @blender-ui/react: a React 19.3 renderer whose hooks (useState, useMemo, useEffect ...) hold signals that update nodes without re-renders. Both on Node.js and Bun, with headless testing. bui build makes standalone executables: about 7 MB with the embedded QuickJS runtime, or Bun's with --bun (guide) |
| Packaging | blenderui on PyPI, with a CI workflow for wheels on all platforms (Linux wheels with the native X11 host); blenderui on crates.io; @blender-ui/react and @blender-ui/native on npm |
| Docs | The website, bui.preetham.top |
Partial or in progress
| Area | What's missing |
|---|---|
| Qt syntax (~24%) | Model/view classes, docks, toolbars, splitters, Qt stylesheets (details) |
| CSS (261 of 745 W3C properties render, 129 more parse) | 3D transforms, OpenType font features, multi-column layout; hit testing ignores 2D transforms. Most of the remaining W3C list is drafts, speech, print and SVG (breakdown) |
| Text | No text wrapping around floats; right-to-left text is reordered (bidi) but not shaped (Arabic joining); no OpenType features; no per-glyph font fallback yet (characters missing from the font show as boxes); no IME |
| Rust components | No async resources (use_resource); no generic components |
| Dear ImGui | Python and safe Rust skip the va_list, void* and C-callback functions (reachable from C and the Rust FFI); with Blender widgets, window title bars, tooltips, plain popups, tables and the color picker keep ImGui's drawing |
| JavaScript / React packages | npm packages are prebuilt for Windows x64 only (on Linux, build the addon from source); events reach JavaScript after BUI handled them (no native preventDefault), and event.target is the deepest listening element |
| macOS | Experimental. The native Cocoa window host is written, checked by CI and tried by hand on one Apple Silicon Mac (how to help); JavaScript is headless there (Node's UI thread isn't the main thread); no GPU renderer yet |
| Themes | Dark and Light work; loading other Blender theme files doesn't yet |
Planned (coming soon)
| Target | Plan |
|---|---|
| WebAssembly | The kit in the browser: the raster blits to <canvas>, DOM events feed the kit; @blender-ui/react and the DOM-like API on top, unchanged |
| egui | BUI panels inside egui, plus a Blender-styled theme |
| More from Blender's source | Per-glyph font fallback (BLF), IME, search-as-you-type popups, units in number fields, widget drag and drop |
| macOS GPU and JavaScript windows | The GPU renderer on Metal (or the OpenGL backend), and Node windows on the main thread |
| More GPU backends | Vulkan / WebGPU drawing the same instance stream as Direct3D 11 and OpenGL |
| Qt parity | Measure against PySide6, run the same programs on Qt and BUI, fill in the missing classes, then install() so import PySide6 programs run unchanged |
| More languages | MyGo, C#, Go, Zig, Swift, Kotlin and Lua over the same C header; Godot, Unity and Bevy; Flutter |
Our roadmap has no dates on it. Like a good render, it's done when the noise goes away.
Do I need Blender installed? No. BUI contains the parts of Blender's UI code it needs and builds on its own. Is it really my unchanged tkinter app? Yes. blenderui-tk app.py makes import tkinter return BUI's implementation, which covers every public tkinter and ttk name. Nine real programs are tested to behave identically on the standard library and on BUI. See the limitations for the few differences. Is it a web browser in Rust? No. There's no browser engine and no network. BUI takes HTML elements and CSS as a UI language: they are laid out with taffy and drawn by BUI's engine, with Blender's widgets where you ask for them, in a native window. JavaScript is optional: the Rust crate needs none, and the JavaScript packages drive the same engine. Is the Dear ImGui support a re-implementation? No. It's the real Dear ImGui (vendored, MIT), running on a BUI backend, so every function behaves as upstream. The optional Blender-widgets build swaps only how ImGui's widgets are drawn and how they react to the mouse, by compiling an edited copy of imgui_widgets.cpp. How is it different from Electron, Electrobun or GPUIX?
BUI also renders natively, with Blender's widgets, a much larger part of CSS, Tailwind, and hooks backed by signals. bui build makes a 5 MB executable that reaches its first frame in about 70 ms. Do the macros cost performance? rsx!, #[component], css! and tw! expand at compile time into the same builder calls you would write by hand. Tailwind classes are compiled to CSS once, on first use. Hooks are a few nanoseconds per read or write. Can I change the colors? Yes. Switch between Blender's Dark and Light themes at run time (Themes), color individual widgets in every API, and use full CSS on plain HTML elements in Rust and JavaScript. Loading any other Blender theme file is coming soon. Does it run on my Mac? Probably, but nobody has checked yet: the Cocoa host is written and experimental. Python can always fall back to the Tk window (BUI_HOST=tk). If you try it, the notes in docs/macos-testing.md say what to report. Is it ready for production? Much of it is complete and tested (the C kit, tkinter, the components, Rust, Dear ImGui, JavaScript), and some parts are partial or experimental (Qt syntax, parts of CSS and text, macOS). Status and roadmap shows which is which. Pin your version, and test on the platforms you ship. Who wrote it? The repository is built entirely using LLMs, with no human-written code. The drawing and interaction code is ported from Blender's sources, and the behavior is checked by tests: C, Python, Rust and JavaScript suites, the tkinter side-by-side programs and the GPU parity test. Why GPL? BUI is derived from Blender's source code, which is GPL-2.0-or-later, so BUI is too. Can I use BUI in a commercial app? The GPL allows commercial use. What it asks is that when you distribute a program that includes BUI, you distribute it under the GPL too, with its source code available. If that doesn't fit your project, talk to a lawyer before you ship. (This is a summary, not legal advice.)
Contributions are welcome: bug reports, missing tkinter/Qt behavior, CSS features, platform hosts and docs. If you have a Mac, running the Cocoa host and reporting what happens is one of the most useful things you can do right now. Read CONTRIBUTING.md first, and please follow the Code of Conduct. Report security issues privately, as SECURITY.md describes.
Releases go to PyPI (blenderui, wheels built by CI on a v* tag), crates.io (the eight Rust crates, in dependency order) and npm (@blender-ui/native, @blender-ui/react). The steps are in docs/publishing.md.
License. BUI is derived from Blender sources and is licensed GPL-2.0-or-later. The SPDX header is kept in every file. Third-party parts keep their own licenses:
| Part | What it does in BUI | License |
|---|---|---|
| Blender sources | Widget drawing, interaction rules, theme, icons, blenlib, GHOST window code, the File Browser and node editor |
GPL-2.0-or-later |
| Dear ImGui by Omar Cornut | The immediate-mode GUI that runs on BUI | MIT |
| Inter by Rasmus Andersson | The embedded UI font, as in Blender (gpu/extern/Inter-LICENSE.txt) |
SIL Open Font License 1.1 |
| FreeType 2.13.3 | Glyph loading, hinting and rendering, like Blender's BLF | FreeType License or GPLv2 (dual-licensed) |
| stb_truetype by Sean Barrett | Font metrics and character maps, and the fallback rasterizer | Public domain |
| NanoSVG by Mikko Mononen, as edited by Blender | Rasterizing Blender's SVG icons | Zlib |
| Slug by Eric Lengyel | The GPU glyph coverage algorithm (shader code "Copyright 2017 by Eric Lengyel") | MIT |
| Tailwind CSS | The v4 default theme values behind the Tailwind support | MIT |
The Rust and JavaScript bindings also depend on crates and packages such as taffy, napi-rs, React and QuickJS, each under its own license.
Acknowledgments. Thank you to:
BUI is Blender's UI, minus the default cube. You're free to add your own.