Debug UI
SafiEngine's debug overlay uses MicroUI — a tiny (~1100 SLOC), pure-C immediate-mode UI library. The engine ships its own SDL_gpu batched-quad backend (stb_truetype font atlas, vertex/index buffer batching, pipeline, scissor + draw) so the whole stack stays in C.
Lifecycle
The debug UI is opt-in via SafiAppDesc.enable_debug_ui. When enabled:
safi_app_initcallssafi_debug_ui_init— creates the MicroUI context, bakes a ProggyClean font atlas via stb_truetype into anSDL_GPUTexture, builds the UI graphics pipeline.- Each frame, after
safi_renderer_begin_frameand beforesafi_renderer_begin_main_pass:safi_debug_ui_begin_frame(r)— callsmu_begin, opens the widget frame.safi_debug_ui_draw_panels(r, world)— draws the built-in Scene and Inspector panels.- Optionally build additional custom widgets with
mu_begin_window / mu_label / mu_button / …. safi_debug_ui_prepare(r)— callsmu_end, batches all draw commands into vertex/index data, and uploads through a copy pass. Must happen before the main render pass opens; SDL_gpu forbids nested passes.
- After
safi_renderer_begin_main_passand any engine geometry, callsafi_debug_ui_render(r)to record the batched draw commands into the active render pass. safi_app_shutdowncallssafi_debug_ui_shutdown.
The engine's input system already forwards SDL events to MicroUI — you don't need to call anything extra for mouse, keyboard, or text input.
Built-in panels
The engine provides two ready-to-use panels via safi_debug_ui_draw_panels. No MicroUI include or widget code needed in your app.
Scene hierarchy
Positioned on the left side of the window. Displays every entity with a SafiName component as a collapsible tree that respects EcsChildOf hierarchy:
- Root entities (no parent) appear at the top level.
- Child entities appear indented under their parent with an arrow toggle.
- Clicking the arrow (
>/v) expands or collapses a parent's subtree. - Clicking the entity name selects it for the Inspector.
- The selected entity is highlighted with a distinct button color.
Inspector
Positioned on the right side of the window. Displays the selected entity's name and all its stock components in collapsible sections (expanded by default):
Property widgets:
- Number fields — drag to scrub, double-click to type a value directly.
- Vec3 rows — label + three number cells in a single row.
- Checkboxes — bool fields (e.g.
MeshRenderer.Visible). - Dropdowns — enum fields render as a button showing the current value; clicking drops a scrollable list below the button. Only one dropdown can be open at a time; clicking outside dismisses it.
Only components present on the selected entity are shown.
Enabling / disabling
The panels are gated by SafiAppDesc.enable_debug_ui:
At runtime, app.debug_ui_enabled can be toggled to show/hide the panels.
Functions
Framework
Scene & Inspector
The selected entity is stored on SafiEditorState.selected_entity, not inside the debug UI. The Scene panel writes it on click; the Inspector, the toolbar highlight, and the gizmo system all read from the same singleton. Use safi_editor_get_selected(world) / safi_editor_set_selected(world, e) from any system; see Editor State.
Example
Custom widgets
If you need additional MicroUI widgets beyond the built-in panels, include <microui.h> and add your own mu_begin_window / mu_end_window blocks between safi_debug_ui_begin_frame and safi_debug_ui_prepare:
No special macros or feature flags needed — just include the header and call MicroUI functions directly.
Why MicroUI?
An earlier iteration used Nuklear. While functional, it was a large single-header (~18K SLOC) and its vertex-buffer conversion model (nk_convert) added complexity to the rendering backend.
MicroUI is:
- ~1100 lines of C — trivially auditable
- pure C99, no generator, no submodule, no C++
- command-based rendering — emits simple draw commands (rect, text, icon, clip) that the backend batches into GPU-friendly vertex data
- license-friendly (MIT)
Trade-off: MicroUI has a smaller widget catalogue than Nuklear or Dear ImGui. For a debug overlay this is fine — the engine extends it with custom property widgets (vec3 rows, double-click number editing) as needed.
See also
SafiRenderer— frame lifecycle (begin_frame→begin_main_pass→end_main_pass→end_frame)SafiName— entities need aSafiNamecomponent to appear in the Scene panel- Editor State — where the selected entity is actually stored
- Editor Toolbar — Play/Pause/Stop + tool buttons drawn at the top of the viewport
- MicroUI project — upstream source and documentation