Last active
September 27, 2024 03:00
-
-
Save keenanwoodall/b6f7ecf6346ba3be4842c7d9fd1f372d to your computer and use it in GitHub Desktop.
Easy two-line raylib + microui integration for the Odin programming language
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package rlmui | |
import "core:fmt" | |
import "core:unicode/utf8" | |
import mu "vendor:microui" | |
import rl "vendor:raylib" | |
@(private) Color32 :: [4]u8 | |
State :: struct { | |
pixels : []Color32, | |
image : rl.Image, | |
atlas : rl.Texture2D, | |
mu_ctx : mu.Context, | |
} | |
global_state : ^State = new(State) | |
InitUI :: proc(using state : ^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 just lives on the CPU and the pixels are stored in RAM | |
// 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 | |
atlas = rl.LoadTextureFromImage(image) | |
// Initialize mui with our mui context | |
mu.init(&mu_ctx) | |
// and point the text_width/height callback functions of our context to the default ones since we're using the default atlas | |
mu_ctx.text_width = mu.default_atlas_text_width | |
mu_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 | |
} | |
DisposeUI :: proc(using state : ^State = global_state) { | |
// Free the system/video pixel memory | |
delete(pixels) | |
rl.UnloadTexture(atlas) | |
} | |
BeginUI :: proc(using state : ^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_to_mui(state) | |
forward_mouse_input_to_mui(state) | |
forward_keyboard_input_to_mui(state) | |
// Now we can tell mui that we're ready to tell it what UI we want to draw | |
mu.begin(&mu_ctx) | |
return &mu_ctx | |
} | |
EndUI :: proc(using state : ^State = global_state) { | |
// Tell mui that we're done drawing ui | |
mu.end(&mu_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(&mu_ctx, ¤t_command) { | |
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_mui_atlas_texture(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_mui_atlas_texture(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 | |
// The y position needs to be flipped for raylib | |
rl.BeginScissorMode(cmd.rect.x, rl.GetScreenHeight() - (cmd.rect.y + cmd.rect.h), cmd.rect.w, cmd.rect.h) | |
case ^mu.Command_Jump: | |
unreachable() | |
} | |
} | |
} | |
@(deferred_in=DisposeUI) | |
InitUIScope :: proc(using state : ^State = global_state) -> ^mu.Context { InitUI(state); return &mu_ctx } | |
@(deferred_in=EndUI) | |
BeginUIScope :: proc(using state : ^State = global_state) -> ^mu.Context { BeginUI(state); return &mu_ctx } | |
// Sends the current text (typing) input from raylib to mui | |
@(private) | |
forward_text_input_to_mui :: proc(using state : ^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 | |
read_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(&mu_ctx, string(text_input[:text_input_offset])) | |
} | |
// Sends the current mouse input from raylib to mui | |
@(private) | |
forward_mouse_input_to_mui :: proc(using state : ^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(&mu_ctx, mouse_position.x, mouse_position.y) | |
mu.input_scroll(&mu_ctx, 0, i32(mouse_scroll) * -30) | |
// 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(&mu_ctx, mouse_position.x, mouse_position.y, button.mu) | |
} | |
else if rl.IsMouseButtonReleased(button.rl) { | |
mu.input_mouse_up(&mu_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_to_mui :: proc(using state : ^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}, | |
} | |
for key in KeyMappings { | |
if rl.IsKeyPressed(key.rl) { | |
mu.input_key_down(&mu_ctx, key.mu) | |
} | |
else if rl.IsKeyReleased(key.rl) { | |
mu.input_key_up(&mu_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_mui_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) | |
} | |
// This condition checks if the character is not 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) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package rlmuitest | |
import "rlmui" | |
import "core:fmt" | |
import rl "vendor:raylib" | |
import mu "vendor:microui" | |
main :: proc() { | |
rl.SetWindowState({ rl.ConfigFlag.WINDOW_RESIZABLE }) | |
rl.InitWindow(720, 480, "Example"); defer rl.CloseWindow() | |
ctx := rlmui.InitUIScope() // same as calling, `rlmui.InitUI(); defer rlmui.DisposeUI()` | |
for !rl.WindowShouldClose() { | |
rl.BeginDrawing(); defer rl.EndDrawing() | |
rl.ClearBackground(rl.BLACK) | |
rlmui.BeginUIScope() // same as calling, `rlmui.BeginUI(); defer rlmui.EndUI()` | |
if mu.window(ctx, "Test Window", { 400/4, 400/4, 400/2, 400/2 }){ | |
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)) | |
} | |
} | |
free_all(context.temp_allocator) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Newest odin seems to error on the
return &mu_ctx
need to change toreturn &state.mu_ctx