Blender's UI and Runtime: Much Faster Than GPUI Blender's user interface, as a library you can use from Python, Rust, JavaScript (React, Svelte), 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 · Chat: Discord 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. Contents Part I: Welcome What BUI is Why BUI exists Who it's for The many ways in Features at a glance A gallery Part II: Getting started Install First app: tkinter First app: Blender components First app: Qt syntax First app: Rust, in rsx and gpui builders First app: JavaScript and React First app: Dear ImGui First app: C Part III: Concepts How it works Blender's widgets and interactions Three looks: HTML, styled and Blender components Rendering on the CPU and the GPU Damage rendering Window hosts Text and fonts Themes: dark and light Part IV: Guides by language Python Rust JavaScript and React Dear ImGui C and embedding Where the docs live Part V: Performance Before you read the numbers Start time and battery Frame times JavaScript and React: startup and animation Run the benchmarks yourself Part VI: Reference Platforms Environment variables Building from source and CMake options Coverage Repository layout Examples Part VII: The project Status and roadmap FAQ Contributing and publishing License and credits Part I: Welcome 1. What BUI is 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: Not Blender. It contains the parts of Blender's UI code it needs and builds on its own. You don't need Blender installed, and you don't need to know Blender to use it. Not a browser. The Rust and JavaScript sides use HTML and CSS as a UI language, but there's no browser engine, no webview and no network. Not a re-imagining. The widget drawing is ported from Blender's interface_widgets.cc, and the Dear ImGui support runs the real Dear ImGui. 2. Why BUI exists 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. 3. Who it's for You have a tkinter app and wish it looked less like 1998. Run it on BUI, unchanged. You're building a tool for artists who already know Blender. Give them the panels, sliders and shortcuts their hands expect. You write Rust or React and want a native desktop UI without shipping a browser. You use Dear ImGui and want it to look and feel like Blender, with one build flag. You're curious how a UI toolkit works. The source is readable, and the concepts are explained in plain words. 4. The many ways in 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 (, {/* a Blender component */} ) } render(, { title: "Counter", width: 360, height: 200, gpu: true }) 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. 13. First app: Dear ImGui 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. 14. First app: C 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 #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. Part III: Concepts 15. How it works 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) Your code, in any language above, creates widgets and says what happens when they change. The kit keeps them in one tree, lays them out and handles input, following Blender's rules. The renderer draws them, on the CPU by default or on the GPU if you ask. Damage rendering keeps it cheap: a change redraws only the rectangle it touched, and an idle window uses no CPU at all. What's under the hood: Blender's real UI code. Widget drawing is a C port of Blender's 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. One node tree. Every widget is one kit node. The C kit owns layout, hit testing, hover, focus, drags, menus, tooltips, undo and animation. Damage rendering. A change marks only its widget's rectangle, and only changed tiles reach the screen. Thin bindings. Python, Rust and JavaScript keep only what their syntax layer needs. More: ARCHITECTURE.md and How it works. 16. Blender's widgets and interactions 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. Drag to scrub. Press a number field and drag sideways. The cursor hides and wraps, so you never hit the screen edge, and comes back where the drag started. A float moves one click step per pixel; Shift moves at a tenth of the speed; Esc cancels and restores the value. Ctrl snaps with Blender's rules: unbounded floats to whole units, angles to 10°, sliders to 0.1, 1 or 10 depending on their range, integers to multiples of 10. Click to type, and type math: Blender's expression evaluator (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. Shortcuts on any value: Backspace resets to the default, - negates, Ctrl+Wheel steps, Ctrl+C / Ctrl+V copy and paste, right-click opens Copy, Paste and Reset. Several fields at once. Drag down over a column of number fields to collect them, then sideways to change them together. Drag across checkboxes (or panel headers) to set them all in one stroke. Panels collapse with Blender's animation; Ctrl+click opens one and closes the others; drag the grip to reorder. Menus work press-drag-release or click; hover switches menu-bar menus; pie menus open around the pointer. Menu search is Blender's string search: start typing in any open menu. It matches word initials ("ad cu" finds "Add Cube"), forgives typos, and ranks your recent picks first. Undo everywhere. Ctrl+Z and Ctrl+Shift+Z work on every UI edit, and inside a text field while you type. A burst of typing or a multi-field drag is one step. Float precision follows Blender: small values show the decimals they need. Tooltips appear after Blender's half-second delay. Coming soon: typing values with units ("2 m"), Blender's search-as-you-type popups, and widget drag and drop. More: Blender's widgets. 17. Three looks: HTML, styled and Blender components In Rust and JavaScript you pick the look per element: You write You get Plain HTML: // Blender's look inside } div().child(Checkbox().checked(true)).child(Button().child("Apply")) // gpui syntax import { render, NumberSlider, Button } from "@blender-ui/react" // , // DOM API: win.createElement("NumberSlider") 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. 18. Rendering on the CPU and the GPU BUI has two renderers, and they draw the same pixels. The software raster (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. The GPU renderer is opt-in: Direct3D 11 on Windows (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. 19. Damage 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. A change marks only its widget's rectangle as dirty (popups get a margin for their shadow). Dirty rectangles merge (up to 24 per window). If nothing is dirty, the loop doesn't render; it sleeps until the next input or timer. Only the dirty regions are redrawn, clipped to their rectangles. The raster compares the new pixels with the last frame in 64×32 tiles; a redraw that changed nothing reports nothing. Only damaged tiles reach the screen (Win32 copies them, X11 sends each rectangle with 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. 20. Window hosts 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. 21. Text and fonts Your app's text looks the same on Windows, Linux and macOS, and it looks like Blender's: Inter, Blender's UI font, is embedded (the variable version, with optical-size and weight axes). UI text is 11 pt at scale 1. Rendered like Blender's BLF: BUI vendors FreeType 2.13.3 and renders with light hinting, Blender's gamma-1.43 coverage table, Inter's optical size, a real bold instance, no kerning (as in Blender), and Blender's text shadows. With BUI_TEXT_FREETYPE=OFF, stb_truetype rasterizes instead. On the GPU, glyphs are drawn from their outlines with Slug by default, or from a per-glyph atlas like Blender's (BUI_SLUG=0, bui.set_slug(False), .slug(false), BUI_gpu_set_slug(0)). Other fonts: 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. Windows' GDI text is opt-in (-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. 22. Themes: dark and light 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. Part IV: Guides by language 23. Python pip install blenderui gives you four APIs in one package: tkinter syntax (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. Qt syntax (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. Blender components (blenderui.components): 45 components, from NumberSlider to NodeEditor. Reference: docs/components.md. Dear ImGui (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. 24. Rust 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 // control flow right in the markup
    {for item in items {
  • {item}
  • }}
{if dirty {

"Unsaved changes"

}} // real CSS, evaluated by the engine
} 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: Layout by taffy: block flow and wrapping text, flexbox and grid (every property, including grid-template-areas and named lines), tables, relative / absolute / fixed positioning (fixed boxes are placed against the window), scrolling, and bidirectional text. CSS: selectors level 4, nesting, @layer / @media / @supports / @scope / @container / @font-face / @property, var() and env(), every math function, CSS Color 4 and 5. Effects and animations: @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). Tailwind CSS v4, compiled on first use, with compile-time checks in rsx! and tw!. Components and hooks in zopra, React or Leptos style, with optional props and keys. Blender components, styled, and the theme. Headless testing: 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). 25. JavaScript and React @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/svelte: Svelte 5 components (runes, snippets, bindings, keyed blocks, context, scoped