Skip to content

Instantly share code, notes, and snippets.

@keenanwoodall
Last active March 29, 2025 18:03
Show Gist options
  • Save keenanwoodall/b6f7ecf6346ba3be4842c7d9fd1f372d to your computer and use it in GitHub Desktop.
Save keenanwoodall/b6f7ecf6346ba3be4842c7d9fd1f372d to your computer and use it in GitHub Desktop.
Easy two-line raylib + microui integration for the Odin programming language

Odin + Raylib + microui

  1. Copy/paste rlmu.odin into an rlmu/ folder in your Odin project
  2. Import rlmu package import "rlmu"
  3. Import microui package import mu "vendor:microui"
  4. Call rlmu lifecycle procs like so:
main :: proc() {
    rl.SetWindowState({ rl.ConfigFlag.WINDOW_RESIZABLE })
    rl.InitWindow(720, 600, "Odin/Raylib/microui Demo")
    defer rl.CloseWindow()

    ctx := rlmu.init_scope() // same as calling, `rlmu.init(); defer rlmu.destroy()`

    for !rl.WindowShouldClose() {
        defer free_all(context.temp_allocator)

        rl.BeginDrawing(); defer rl.EndDrawing()
        rl.ClearBackground(rl.BLACK)
        
        rlmu.begin_scope()  // same as calling, `rlmu.begin(); defer rlmu.end()`
        
        // make micro ui calls here!
        if mu.begin_window(ctx, "Test Window", { 100, 100, 100, 100 }) {
            defer mu.end_window(ctx)
            
            mu.label(ctx, "Hello, world")
        }
    } 
}

image


Refer to demo.odin for a more complete example:

rlmui_demo_LKXzupZVtY

package demo
// port of micro ui c demo to odin, using rlmu as renderer
import "rlmu"
import "core:fmt"
import "core:strings"
import rl "vendor:raylib"
import mu "vendor:microui"
log_sb := strings.builder_make()
log_updated := false
log_input_text := make_slice([]u8, 128)
log_input_text_len : int
bg : [3]u8 = { 90, 95, 100 }
main :: proc() {
rl.SetWindowState({ .WINDOW_RESIZABLE })
rl.InitWindow(720, 600, "Odin/Raylib/microui Demo")
defer rl.CloseWindow()
ctx := rlmu.init_scope() // same as calling, `rlmu.init(); defer rlmu.destroy()`
for !rl.WindowShouldClose() {
defer free_all(context.temp_allocator)
rl.BeginDrawing(); defer rl.EndDrawing()
rl.ClearBackground({ bg.r, bg.g, bg.b, 255 })
rlmu.begin_scope() // same as calling, `rlmu.begin(); defer rlmu.end()`
style_window(ctx)
test_window(ctx)
log_window(ctx)
}
}
style_window :: proc(ctx : ^mu.Context) {
Style_Color :: struct {
label: string,
type: mu.Color_Type
}
@(static)
colors := [?]Style_Color {
{ "text:", .TEXT },
{ "border:", .BORDER },
{ "windowbg:", .WINDOW_BG },
{ "titlebg:", .TITLE_BG },
{ "titletext:", .TITLE_TEXT },
{ "panelbg:", .PANEL_BG },
{ "button:", .BUTTON },
{ "buttonhover:", .BUTTON_HOVER },
{ "buttonfocus:", .BUTTON_FOCUS },
{ "base:", .BASE },
{ "basehover:", .BASE_HOVER },
{ "basefocus:", .BASE_FOCUS },
{ "scrollbase:", .SCROLL_BASE },
{ "scrollthumb:", .SCROLL_THUMB },
}
if mu.begin_window(ctx, "Style Editor", mu.Rect { 350, 250, 300, 240 }) {
defer mu.end_window(ctx)
slider_width := i32(f32(mu.get_current_container(ctx).body.w) * 0.14)
mu.layout_row(ctx, { 80, slider_width, slider_width, slider_width, slider_width, -1 }, 0)
for i in 0..<len(colors) {
color_type := colors[i].type
color := &ctx.style.colors[color_type]
mu.label(ctx, colors[i].label)
u8_slider(ctx, &color.r, 0, 255)
u8_slider(ctx, &color.g, 0, 255)
u8_slider(ctx, &color.b, 0, 255)
u8_slider(ctx, &color.a, 0, 255)
mu.draw_rect(ctx, mu.layout_next(ctx), color^)
}
}
}
test_window :: proc(ctx: ^mu.Context) {
if mu.begin_window(ctx, "Demo Window", mu.Rect { 40, 40, 300, 450 }) {
defer mu.end_window(ctx)
win := mu.get_current_container(ctx)
win.rect.w = max(win.rect.w, 240)
win.rect.h = max(win.rect.h, 300)
/* window info */
if .ACTIVE in mu.header(ctx, "Window Info") {
win = mu.get_current_container(ctx)
mu.layout_row(ctx, { 54, -1 }, 0)
mu.label(ctx,"Position:")
mu.label(ctx, fmt.tprintf("%d, %d", win.rect.x, win.rect.y))
mu.label(ctx, "Size:")
mu.label(ctx, fmt.tprintf("%d, %d", win.rect.w, win.rect.h))
}
/* labels + buttons */
if .ACTIVE in mu.header(ctx, "Test Buttons", { .EXPANDED }) {
mu.layout_row(ctx, { 86, -110, -1 }, 0)
mu.label(ctx, "Test buttons 1:")
if .SUBMIT in mu.button(ctx, "Button 1") do write_log("Pressed button 1")
if .SUBMIT in mu.button(ctx, "Button 2") do write_log("Pressed button 2")
mu.label(ctx, "Test buttons 2:")
if .SUBMIT in mu.button(ctx, "Button 3") do write_log("Pressed button 3")
if .SUBMIT in mu.button(ctx, "Popup") do mu.open_popup(ctx, "Test Popup")
if mu.begin_popup(ctx, "Test Popup") {
defer mu.end_popup(ctx)
if .SUBMIT in mu.button(ctx, "Hello") do write_log("Hello")
if .SUBMIT in mu.button(ctx, "World") do write_log("World")
}
}
/* tree */
if .ACTIVE in mu.header(ctx, "Tree and Text", { .EXPANDED }) {
mu.layout_row(ctx, { 140, -1 }, 0)
{
mu.layout_begin_column(ctx)
defer mu.layout_end_column(ctx)
if .ACTIVE in mu.begin_treenode(ctx, "Test 1") {
defer mu.end_treenode(ctx)
if .ACTIVE in mu.begin_treenode(ctx, "Test 1a") {
defer mu.end_treenode(ctx)
mu.label(ctx, "Hello")
mu.label(ctx, "world")
}
if .ACTIVE in mu.begin_treenode(ctx, "Test 1b") {
defer mu.end_treenode(ctx)
if .SUBMIT in mu.button(ctx, "Button 1") do write_log("Pressed button 1")
if .SUBMIT in mu.button(ctx, "Button 2") do write_log("Pressed button 2")
}
}
if .ACTIVE in mu.begin_treenode(ctx, "Test 2") {
mu.layout_row(ctx, { -1 }, 0)
defer mu.end_treenode(ctx)
if .SUBMIT in mu.button(ctx, "Button 3") do write_log("Pressed button 3")
if .SUBMIT in mu.button(ctx, "Button 4") do write_log("Pressed button 4")
if .SUBMIT in mu.button(ctx, "Button 5") do write_log("Pressed button 5")
if .SUBMIT in mu.button(ctx, "Button 6") do write_log("Pressed button 6")
}
if .ACTIVE in mu.begin_treenode(ctx, "Test 3") {
defer mu.end_treenode(ctx)
@static checks : [3]bool = { true, false, true }
mu.checkbox(ctx, "Checkbox 1", &checks[0])
mu.checkbox(ctx, "Checkbox 2", &checks[1])
mu.checkbox(ctx, "Checkbox 3", &checks[2])
}
}
{
mu.layout_begin_column(ctx)
defer mu.layout_end_column(ctx)
mu.layout_row(ctx, { -1 }, 0)
mu.text(ctx, "Lorem ipsum dolor sit amet, consectetur adipiscing " +
"elit. Maecenas lacinia, sem eu lacinia molestie, mi risus faucibus " +
"ipsum, eu varius magna felis a nulla."
)
}
}
/* background color sliders */
if .ACTIVE in mu.header(ctx, "Background Color", { .EXPANDED }) {
mu.layout_row(ctx, { -78, -1 }, 74)
/* sliders */
mu.layout_begin_column(ctx)
mu.layout_row(ctx, { 46, -1 }, 0)
mu.label(ctx, "Red:")
u8_slider(ctx, &bg[0], 0, 255)
mu.label(ctx, "Green:")
u8_slider(ctx, &bg[1], 0, 255)
mu.label(ctx, "Blue:")
u8_slider(ctx, &bg[2], 0, 255)
mu.layout_end_column(ctx)
/* color preview */
r := mu.layout_next(ctx)
mu.draw_rect(ctx, r, { bg[0], bg[1], bg[2], 255 })
mu.draw_control_text(ctx, fmt.tprintf("#%02X%02X%02X", bg[0], bg[1], bg[2]), r, .TEXT, { .ALIGN_CENTER })
}
}
}
log_window :: proc (ctx : ^mu.Context) {
if mu.begin_window(ctx, "Log Window", mu.Rect{ 350, 40, 300, 200 }) {
defer mu.end_window(ctx)
/* output text panel */
mu.layout_row(ctx, { -1 }, -25)
mu.begin_panel(ctx, "Log Output")
panel := mu.get_current_container(ctx)
mu.layout_row(ctx, { -1 }, -1)
mu.text(ctx, strings.to_string(log_sb))
mu.end_panel(ctx)
if log_updated {
panel.scroll.y = panel.content_size.y
log_updated = false
}
/* input textbox + submit button */
submitted := false
mu.layout_row(ctx, { -70, -1 }, 0)
if .SUBMIT in mu.textbox(ctx, log_input_text, &log_input_text_len) {
mu.set_focus(ctx, ctx.last_id)
submitted = true
}
if .SUBMIT in mu.button(ctx, "Submit") {
submitted = true
}
if submitted == true {
write_log(string(log_input_text[:log_input_text_len]))
log_input_text_len = 0
}
}
}
write_log :: proc(text: string) {
if strings.builder_len(log_sb) != 0 {
// Append newline if log isn't empty
fmt.sbprint(&log_sb, "\n")
}
fmt.sbprint(&log_sb, text)
log_updated = true
}
u8_slider :: proc(ctx: ^mu.Context, value: ^u8, low, high: int) -> mu.Result_Set {
mu.push_id_uintptr(ctx, transmute(uintptr)value)
defer mu.pop_id(ctx)
@(static) tmp: f32
tmp = f32(value^)
res := mu.slider(ctx, &tmp, f32(low), f32(high), 0, "%.f", { .ALIGN_CENTER })
value ^= u8(tmp)
return res
}
// paste me into your raylib project!
package rlmu
import "core:fmt"
import "core:unicode/utf8"
import "core:strings"
import mu "vendor:microui"
import rl "vendor:raylib"
@(private) Color32 :: [4]u8
RLMU_State :: struct {
atlas : rl.Texture2D,
ctx : mu.Context,
}
global_state := new(RLMU_State)
init :: proc(state := global_state) {
// Allocate an array of 32 bit pixel colors for the atlas used by mui
// An atlas is a big image packed with multiple smaller images. Like a sprite sheet!
pixels := make([]Color32, mu.DEFAULT_ATLAS_WIDTH * mu.DEFAULT_ATLAS_HEIGHT)
// mui has its default atlas already "baked" into a pixel array like ours
// However it only stores the alpha of each pixel since there are not colors in the atlas
// We can just copy the alpha values into the alpha channel of our pixels array, leaving the rgb as white
for alpha, i in mu.default_atlas_alpha do pixels[i] = {255, 255, 255, alpha}
// Create a raylib Image whose data is the pixels we just allocated
image := rl.Image {
data = raw_data(pixels),
width = mu.DEFAULT_ATLAS_WIDTH,
height = mu.DEFAULT_ATLAS_HEIGHT,
format = .UNCOMPRESSED_R8G8B8A8,
mipmaps = 1,
}
// The image we just created lives on the CPU side of things
// If we want to actually draw stuff on the GPU we need to create a *Texture*
// This will actually send the pixel data to the GPU so it can be rendered
state.atlas = rl.LoadTextureFromImage(image)
// Now that the pixel data is stored in video memory, we can delete the pixel array in system memory
delete(pixels)
image = {}
// Initialize mui with our mui context
mu.init(ctx = &state.ctx,)
// and point the text_width/height callback functions of our context to the default ones since we're using the default atlas
state.ctx.text_width = mu.default_atlas_text_width
state.ctx.text_height = mu.default_atlas_text_height
// These variables are actually pointers to `(font: Font, str: string) -> i32` functions,
// which mui uses to calculate the pixel width/height of a string when rendered with a certain font
// Let's also assign the clipboard callbacks for copy/paste support. We have our own functions for handling these events.
state.ctx.textbox_state.set_clipboard = set_clipboard
state.ctx.textbox_state.get_clipboard = get_clipboard
}
destroy :: proc(state := global_state) {
// Free atlas texture
rl.UnloadTexture(state.atlas)
}
begin :: proc(state : ^RLMU_State = global_state) -> ^mu.Context {
// Forward all input from raylib to mui
// We need to tell mui what keys are pressed, where the mouse is etc so the ui can react accordingly
forward_text_input(state)
forward_mouse_input(state)
forward_keyboard_input(state)
// Now we can tell mui that we're ready to tell it what UI we want to draw
mu.begin(&state.ctx)
return &state.ctx
}
end :: proc(state := global_state) {
// Tell mui that we're done drawing ui
mu.end(&state.ctx)
// We've "declared" the ui, but it's just a list of commands
// Now we need to render the UI
// mui transforms our high-level UI calls into a list of primitive render commands: text, rectangle, icon, clip (masking)
current_command: ^mu.Command
for cmd_variant in mu.next_command_iterator(&state.ctx, &current_command) {
#partial switch cmd in cmd_variant {
// Draw a (single-line) string
case ^mu.Command_Text:
// This is the top-left position of the first character in the string we need to draw
// We will move it to the right as we draw each character
draw_position := [2]i32{cmd.pos.x, cmd.pos.y}
// Loop over each character in the text. This "do if" condition ensures that we only process single-byte ASCII characters.
for char in cmd.str do if !is_utf8_continuation_byte(char) {
// We need to convert the UTF8 character to an plain ASCII integer so that we can use it to index
// into the mui default atlas.
ascii := char_to_ascii(char)
// "mu.default_atlas" is an array of rects for every character and icon in the default atlas texture
// "mu.DEFAULT_ATLAS_FONT" stores the index of the atlas rect for the first ASCII character texture
// By adding our ascii int to the base index we can get the rect for our char's texture
rect := mu.default_atlas[mu.DEFAULT_ATLAS_FONT + ascii]
// Now that we have the atlas rect for the current char we can draw it to the screen
draw_mu_atlas_texture(state.atlas, rect, draw_position, cmd.color)
// Finally, we need to offset our draw position before drawing the next char
draw_position.x += rect.w
}
// Draw a rectangle
case ^mu.Command_Rect:
rl.DrawRectangle(cmd.rect.x, cmd.rect.y, cmd.rect.w, cmd.rect.h, transmute(rl.Color)cmd.color)
// Draw an icon
case ^mu.Command_Icon:
// cmd.id stores the index into the default_atlas array of rects which we can use to get the icons atlas rect
rect := mu.default_atlas[cmd.id]
x := cmd.rect.x + (cmd.rect.w - rect.w) / 2
y := cmd.rect.y + (cmd.rect.h - rect.h) / 2
draw_mu_atlas_texture(state.atlas, rect, {x, y}, cmd.color)
// Start masking what we draw
case ^mu.Command_Clip:
// End any previous masking
rl.EndScissorMode()
// Begin a mask using the current commands rect
rl.BeginScissorMode(cmd.rect.x, cmd.rect.y, cmd.rect.w, cmd.rect.h)
}
}
// Make sure we end any lingering scissor mode.
// Without this precaution it's possible for clipping to persist into the next frame
// and obstruct stuff until the next clip command from microui is handled!
rl.EndScissorMode()
}
@(deferred_in=destroy)
init_scope :: proc(state := global_state) -> ^mu.Context { init(state); return &state.ctx }
@(deferred_in=end)
begin_scope :: proc(state := global_state) -> ^mu.Context { begin(state); return &state.ctx }
// Sends the current text (typing) input from raylib to mui
@(private)
forward_text_input :: proc(state : ^RLMU_State) {
// Create a buffer to hold UTF8 text input
// UTF8 is a "variable width encoding" so some characters may be 1 byte whereas other may be up to 4
text_input : [512]byte = ---
// This will track the index into the text_input buffer where characters bytes will be copied to
text_input_offset := 0
// This loops reads the characters currently pressed until we've read all the pressed characters or we hit the
// limit of our text input buffer. We'd only hit the limit if someone was holding a LOT of characters
for text_input_offset < len(text_input) {
// Get the pressed UTF8 character rune
// Called multiple times since multiple keys may be pressed
pressed_rune := rl.GetCharPressed()
// If the pressed rune (Unicode character) is 0 there are no more keys pressed
if pressed_rune == 0 do break
// UTF8 characters are stored using "variable width encoding"
// This means a character could be represented by 1-4 bytes
// Encoding the rune into UTF8 always returns 4 bytes, but the count indicates how many runes the character actually is
bytes, count := utf8.encode_rune(pressed_rune)
// We'll copy from the start of the bytes array, up to the number of bytes used to represent the character,
// into our text input buffer at the current text input offset
copy(text_input[text_input_offset:], bytes[:count])
// Finally we can offset the text_input_offset by the number of bytes we copied into the buffer
text_input_offset += count
}
// Now we can send our text_input buffer to mui so it knows the currently pressed characters
// We'll just send a slice of the full text_input buffer, up to the latest input offset since it probably wasn't filled
mu.input_text(&state.ctx, string(text_input[:text_input_offset]))
}
// Sends the current mouse input from raylib to mui
@(private)
forward_mouse_input :: proc(state : ^RLMU_State) {
// Get the current mouse position/scroll from raylib
mouse_position := [2]i32{rl.GetMouseX(), rl.GetMouseY()}
mouse_scroll := rl.GetMouseWheelMove() * -30
// Send the mouse position/scroll to mui
mu.input_mouse_move(&state.ctx, mouse_position.x, mouse_position.y)
mu.input_scroll(&state.ctx, 0, i32(mouse_scroll))
// This struct stores a mapping from a raylib mouse button enum to the equivalent mui enum
MouseButtonMapping :: struct {
rl : rl.MouseButton,
mu : mu.Mouse
}
// We'll create an array of mappings using the struct above
@static
MouseButtonMappings := [?]MouseButtonMapping {
{.LEFT, .LEFT},
{.RIGHT, .RIGHT},
{.MIDDLE, .MIDDLE}
}
// Check each if each mouse button is down or released with Raylib and forward the event to mui if so
for button in MouseButtonMappings {
if rl.IsMouseButtonPressed(button.rl) {
mu.input_mouse_down(&state.ctx, mouse_position.x, mouse_position.y, button.mu)
}
else if rl.IsMouseButtonReleased(button.rl) {
mu.input_mouse_up(&state.ctx, mouse_position.x, mouse_position.y, button.mu)
}
}
}
// Sends the current keyboard input from raylib to mui
// Not quite the same as forward_text_input_to_mui() which sends any pressed *characters*
// whereas this sends specific "modifier" keys like Shift, Enter and Backspace
@(private)
forward_keyboard_input :: proc(state : ^RLMU_State) {
// This struct stores a mapping from a raylib key enum to the equivalent mui enum
KeyMapping :: struct {
rl : rl.KeyboardKey,
mu : mu.Key
}
// We'll create an array of mappings using the struct above
// We don't need to map every raylib key - just the ones mui needs for ui stuff
@static
KeyMappings := [?]KeyMapping {
{.LEFT_SHIFT, .SHIFT},
{.RIGHT_SHIFT, .SHIFT},
{.LEFT_CONTROL, .CTRL},
{.RIGHT_CONTROL, .CTRL},
{.LEFT_ALT, .ALT},
{.RIGHT_ALT, .ALT},
{.ENTER, .RETURN},
{.KP_ENTER, .RETURN},
{.BACKSPACE, .BACKSPACE},
{.DELETE, .DELETE},
{.END, .END},
{.HOME, .HOME},
{.LEFT, .LEFT},
{.RIGHT, .RIGHT},
{.A, .A},
{.C, .C},
{.V, .V},
{.X, .X},
}
for key in KeyMappings {
if rl.IsKeyPressed(key.rl) {
mu.input_key_down(&state.ctx, key.mu)
}
else if rl.IsKeyReleased(key.rl) {
mu.input_key_up(&state.ctx, key.mu)
}
}
}
// Draws a section of the mui atlas to the screen with a tint
// The target_position it draws to will be the top left of the drawn texture
@(private)
draw_mu_atlas_texture :: proc(atlas : rl.Texture2D, atlas_source : mu.Rect, target_position : [2]i32, color : mu.Color) {
// Create a raylib version of the source rect and target position.
// We don't have to do this for the color since its memory layout is identical
rl_target_position := rl.Vector2 { f32(target_position.x), f32(target_position.y) }
rl_atlas_source := rl.Rectangle {
f32(atlas_source.x),
f32(atlas_source.y),
f32(atlas_source.w),
f32(atlas_source.h),
}
// Draw the source rect part of the atlas (sprite) to the screen
// We can transmute the mu.Color to a rl.Color since the memory layout is the same. This is like casting without type-checking
rl.DrawTextureRec(atlas, rl_atlas_source, rl_target_position, transmute(rl.Color)color)
}
@(private)
set_clipboard :: proc(user_data: rawptr, text: string) -> (ok: bool) {
// Try and convert the text string handed to us from mui to a cstring, which raylib expects
ctext, err := strings.clone_to_cstring(text, context.temp_allocator)
if err == .None {
rl.SetClipboardText(ctext)
return true
}
return false
}
@(private)
get_clipboard :: proc(user_data: rawptr) -> (text: string, ok: bool) {
// Try and convert the cstring handed to us from raylib to a string, which mui expects
ctext := rl.GetClipboardText()
if ctext == nil || len(ctext) == 0 {
ok = false
return
}
clipboard_text, err := strings.clone_from_cstring(ctext, context.temp_allocator)
text = clipboard_text
ok = err == .None
return
}
// Checks if the character is a continuation byte in UTF-8 encoding.
// In UTF-8, any byte where the top two bits are 10 (binary 0x80 in hexadecimal) is a continuation byte.
@(private)
is_utf8_continuation_byte :: proc(char : rune) -> bool {
return char & 0xc0 == 0x80
}
@(private)
char_to_ascii :: proc(char : rune) -> int {
return min(int(char), 127)
}
@bartimusprimed
Copy link

Newest odin seems to error on the return &mu_ctx need to change to return &state.mu_ctx

@go-dockly
Copy link

Assertion failed: (window != NULL), function glfwSetWindowAttrib, file window.c, line 931.
[1] 59461 abort odin run .

@keenanwoodall
Copy link
Author

Updated to use &state.mu_ctx

@keenanwoodall
Copy link
Author

Demo updated to be exact port of the main micro ui demo code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment