Skip to content

Instantly share code, notes, and snippets.

@gingerBill
Last active February 22, 2025 10:35
Show Gist options
  • Save gingerBill/5bbcca224bf8d9dcd09dde38b2567d10 to your computer and use it in GitHub Desktop.
Save gingerBill/5bbcca224bf8d9dcd09dde38b2567d10 to your computer and use it in GitHub Desktop.
microui + SDL Demo in Odin
package microui_sdl
import "core:fmt"
import "core:c/libc"
import SDL "vendor:sdl2"
import mu "vendor:microui"
state := struct {
mu_ctx: mu.Context,
log_buf: [1<<16]byte,
log_buf_len: int,
log_buf_updated: bool,
bg: mu.Color,
atlas_texture: ^SDL.Texture,
}{
bg = {90, 95, 100, 255},
}
main :: proc() {
if err := SDL.Init({.VIDEO}); err != 0 {
fmt.eprintln(err)
return
}
defer SDL.Quit()
window := SDL.CreateWindow("microui-odin", SDL.WINDOWPOS_UNDEFINED, SDL.WINDOWPOS_UNDEFINED, 960, 540, {.SHOWN, .RESIZABLE})
if window == nil {
fmt.eprintln(SDL.GetError())
return
}
defer SDL.DestroyWindow(window)
backend_idx: i32 = -1
if n := SDL.GetNumRenderDrivers(); n <= 0 {
fmt.eprintln("No render drivers available")
return
} else {
for i in 0..<n {
info: SDL.RendererInfo
if err := SDL.GetRenderDriverInfo(i, &info); err == 0 {
// NOTE(bill): "direct3d" seems to not work correctly
if info.name == "opengl" {
backend_idx = i
break
}
}
}
}
renderer := SDL.CreateRenderer(window, backend_idx, {.ACCELERATED, .PRESENTVSYNC})
if renderer == nil {
fmt.eprintln("SDL.CreateRenderer:", SDL.GetError())
return
}
defer SDL.DestroyRenderer(renderer)
state.atlas_texture = SDL.CreateTexture(renderer, u32(SDL.PixelFormatEnum.RGBA32), .TARGET, mu.DEFAULT_ATLAS_WIDTH, mu.DEFAULT_ATLAS_HEIGHT)
assert(state.atlas_texture != nil)
if err := SDL.SetTextureBlendMode(state.atlas_texture, .BLEND); err != 0 {
fmt.eprintln("SDL.SetTextureBlendMode:", err)
return
}
pixels := make([][4]u8, mu.DEFAULT_ATLAS_WIDTH*mu.DEFAULT_ATLAS_HEIGHT)
for alpha, i in mu.default_atlas_alpha {
pixels[i].rgb = 0xff
pixels[i].a = alpha
}
if err := SDL.UpdateTexture(state.atlas_texture, nil, raw_data(pixels), 4*mu.DEFAULT_ATLAS_WIDTH); err != 0 {
fmt.eprintln("SDL.UpdateTexture:", err)
return
}
ctx := &state.mu_ctx
mu.init(ctx)
ctx.text_width = mu.default_atlas_text_width
ctx.text_height = mu.default_atlas_text_height
main_loop: for {
for e: SDL.Event; SDL.PollEvent(&e) != 0; /**/ {
#partial switch e.type {
case .QUIT:
break main_loop
case .MOUSEMOTION:
mu.input_mouse_move(ctx, e.motion.x, e.motion.y)
case .MOUSEWHEEL:
mu.input_scroll(ctx, e.wheel.x * 30, e.wheel.y * -30)
case .TEXTINPUT:
mu.input_text(ctx, string(cstring(&e.text.text[0])))
case .MOUSEBUTTONDOWN, .MOUSEBUTTONUP:
fn := mu.input_mouse_down if e.type == .MOUSEBUTTONDOWN else mu.input_mouse_up
switch e.button.button {
case SDL.BUTTON_LEFT: fn(ctx, e.button.x, e.button.y, .LEFT)
case SDL.BUTTON_MIDDLE: fn(ctx, e.button.x, e.button.y, .MIDDLE)
case SDL.BUTTON_RIGHT: fn(ctx, e.button.x, e.button.y, .RIGHT)
}
case .KEYDOWN, .KEYUP:
if e.type == .KEYUP && e.key.keysym.sym == .ESCAPE {
SDL.PushEvent(&SDL.Event{type = .QUIT})
}
fn := mu.input_key_down if e.type == .KEYDOWN else mu.input_key_up
#partial switch e.key.keysym.sym {
case .LSHIFT: fn(ctx, .SHIFT)
case .RSHIFT: fn(ctx, .SHIFT)
case .LCTRL: fn(ctx, .CTRL)
case .RCTRL: fn(ctx, .CTRL)
case .LALT: fn(ctx, .ALT)
case .RALT: fn(ctx, .ALT)
case .RETURN: fn(ctx, .RETURN)
case .KP_ENTER: fn(ctx, .RETURN)
case .BACKSPACE: fn(ctx, .BACKSPACE)
}
}
}
mu.begin(ctx)
all_windows(ctx)
mu.end(ctx)
render(ctx, renderer)
}
}
render :: proc(ctx: ^mu.Context, renderer: ^SDL.Renderer) {
render_texture :: proc(renderer: ^SDL.Renderer, dst: ^SDL.Rect, src: mu.Rect, color: mu.Color) {
dst.w = src.w
dst.h = src.h
SDL.SetTextureAlphaMod(state.atlas_texture, color.a)
SDL.SetTextureColorMod(state.atlas_texture, color.r, color.g, color.b)
SDL.RenderCopy(renderer, state.atlas_texture, &SDL.Rect{src.x, src.y, src.w, src.h}, dst)
}
viewport_rect := &SDL.Rect{}
SDL.GetRendererOutputSize(renderer, &viewport_rect.w, &viewport_rect.h)
SDL.RenderSetViewport(renderer, viewport_rect)
SDL.RenderSetClipRect(renderer, viewport_rect)
SDL.SetRenderDrawColor(renderer, state.bg.r, state.bg.g, state.bg.b, state.bg.a)
SDL.RenderClear(renderer)
command_backing: ^mu.Command
for variant in mu.next_command_iterator(ctx, &command_backing) {
switch cmd in variant {
case ^mu.Command_Text:
dst := SDL.Rect{cmd.pos.x, cmd.pos.y, 0, 0}
for ch in cmd.str do if ch&0xc0 != 0x80 {
r := min(int(ch), 127)
src := mu.default_atlas[mu.DEFAULT_ATLAS_FONT + r]
render_texture(renderer, &dst, src, cmd.color)
dst.x += dst.w
}
case ^mu.Command_Rect:
SDL.SetRenderDrawColor(renderer, cmd.color.r, cmd.color.g, cmd.color.b, cmd.color.a)
SDL.RenderFillRect(renderer, &SDL.Rect{cmd.rect.x, cmd.rect.y, cmd.rect.w, cmd.rect.h})
case ^mu.Command_Icon:
src := mu.default_atlas[cmd.id]
x := cmd.rect.x + (cmd.rect.w - src.w)/2
y := cmd.rect.y + (cmd.rect.h - src.h)/2
render_texture(renderer, &SDL.Rect{x, y, 0, 0}, src, cmd.color)
case ^mu.Command_Clip:
SDL.RenderSetClipRect(renderer, &SDL.Rect{cmd.rect.x, cmd.rect.y, cmd.rect.w, cmd.rect.h})
case ^mu.Command_Jump:
unreachable()
}
}
SDL.RenderPresent(renderer)
}
u8_slider :: proc(ctx: ^mu.Context, val: ^u8, lo, hi: u8) -> (res: mu.Result_Set) {
mu.push_id(ctx, uintptr(val))
@static tmp: mu.Real
tmp = mu.Real(val^)
res = mu.slider(ctx, &tmp, mu.Real(lo), mu.Real(hi), 0, "%.0f", {.ALIGN_CENTER})
val^ = u8(tmp)
mu.pop_id(ctx)
return
}
write_log :: proc(str: string) {
state.log_buf_len += copy(state.log_buf[state.log_buf_len:], str)
state.log_buf_len += copy(state.log_buf[state.log_buf_len:], "\n")
state.log_buf_updated = true
}
read_log :: proc() -> string {
return string(state.log_buf[:state.log_buf_len])
}
reset_log :: proc() {
state.log_buf_updated = true
state.log_buf_len = 0
}
all_windows :: proc(ctx: ^mu.Context) {
@static opts := mu.Options{.NO_CLOSE}
if mu.window(ctx, "Demo Window", {40, 40, 300, 450}, opts) {
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))
}
if .ACTIVE in mu.header(ctx, "Window Options") {
mu.layout_row(ctx, {120, 120, 120}, 0)
for opt in mu.Opt {
state := opt in opts
if .CHANGE in mu.checkbox(ctx, fmt.tprintf("%v", opt), &state) {
if state {
opts += {opt}
} else {
opts -= {opt}
}
}
}
}
if .ACTIVE in mu.header(ctx, "Test Buttons", {.EXPANDED}) {
mu.layout_row(ctx, {86, -110, -1})
mu.label(ctx, "Test buttons 1:")
if .SUBMIT in mu.button(ctx, "Button 1") { write_log("Pressed button 1") }
if .SUBMIT in mu.button(ctx, "Button 2") { write_log("Pressed button 2") }
mu.label(ctx, "Test buttons 2:")
if .SUBMIT in mu.button(ctx, "Button 3") { write_log("Pressed button 3") }
if .SUBMIT in mu.button(ctx, "Button 4") { write_log("Pressed button 4") }
}
if .ACTIVE in mu.header(ctx, "Tree and Text", {.EXPANDED}) {
mu.layout_row(ctx, {140, -1})
mu.layout_begin_column(ctx)
if .ACTIVE in mu.treenode(ctx, "Test 1") {
if .ACTIVE in mu.treenode(ctx, "Test 1a") {
mu.label(ctx, "Hello")
mu.label(ctx, "world")
}
if .ACTIVE in mu.treenode(ctx, "Test 1b") {
if .SUBMIT in mu.button(ctx, "Button 1") { write_log("Pressed button 1") }
if .SUBMIT in mu.button(ctx, "Button 2") { write_log("Pressed button 2") }
}
}
if .ACTIVE in mu.treenode(ctx, "Test 2") {
mu.layout_row(ctx, {53, 53})
if .SUBMIT in mu.button(ctx, "Button 3") { write_log("Pressed button 3") }
if .SUBMIT in mu.button(ctx, "Button 4") { write_log("Pressed button 4") }
if .SUBMIT in mu.button(ctx, "Button 5") { write_log("Pressed button 5") }
if .SUBMIT in mu.button(ctx, "Button 6") { write_log("Pressed button 6") }
}
if .ACTIVE in mu.treenode(ctx, "Test 3") {
@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_end_column(ctx)
mu.layout_begin_column(ctx)
mu.layout_row(ctx, {-1})
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.",
)
mu.layout_end_column(ctx)
}
if .ACTIVE in mu.header(ctx, "Background Colour", {.EXPANDED}) {
mu.layout_row(ctx, {-78, -1}, 68)
mu.layout_begin_column(ctx)
{
mu.layout_row(ctx, {46, -1}, 0)
mu.label(ctx, "Red:"); u8_slider(ctx, &state.bg.r, 0, 255)
mu.label(ctx, "Green:"); u8_slider(ctx, &state.bg.g, 0, 255)
mu.label(ctx, "Blue:"); u8_slider(ctx, &state.bg.b, 0, 255)
}
mu.layout_end_column(ctx)
r := mu.layout_next(ctx)
mu.draw_rect(ctx, r, state.bg)
mu.draw_box(ctx, mu.expand_rect(r, 1), ctx.style.colors[.BORDER])
mu.draw_control_text(ctx, fmt.tprintf("#%02x%02x%02x", state.bg.r, state.bg.g, state.bg.b), r, .TEXT, {.ALIGN_CENTER})
}
}
if mu.window(ctx, "Log Window", {350, 40, 300, 200}, opts) {
mu.layout_row(ctx, {-1}, -28)
mu.begin_panel(ctx, "Log")
mu.layout_row(ctx, {-1}, -1)
mu.text(ctx, read_log())
if state.log_buf_updated {
panel := mu.get_current_container(ctx)
panel.scroll.y = panel.content_size.y
state.log_buf_updated = false
}
mu.end_panel(ctx)
@static buf: [128]byte
@static buf_len: int
submitted := false
mu.layout_row(ctx, {-70, -1})
if .SUBMIT in mu.textbox(ctx, buf[:], &buf_len) {
mu.set_focus(ctx, ctx.last_id)
submitted = true
}
if .SUBMIT in mu.button(ctx, "Submit") {
submitted = true
}
if submitted {
write_log(string(buf[:buf_len]))
buf_len = 0
}
}
if mu.window(ctx, "Style Window", {350, 250, 300, 240}) {
@static colors := [mu.Color_Type]string{
.TEXT = "text",
.BORDER = "border",
.WINDOW_BG = "window bg",
.TITLE_BG = "title bg",
.TITLE_TEXT = "title text",
.PANEL_BG = "panel bg",
.BUTTON = "button",
.BUTTON_HOVER = "button hover",
.BUTTON_FOCUS = "button focus",
.BASE = "base",
.BASE_HOVER = "base hover",
.BASE_FOCUS = "base focus",
.SCROLL_BASE = "scroll base",
.SCROLL_THUMB = "scroll thumb",
}
sw := i32(f32(mu.get_current_container(ctx).body.w) * 0.14)
mu.layout_row(ctx, {80, sw, sw, sw, sw, -1})
for label, col in colors {
mu.label(ctx, label)
u8_slider(ctx, &ctx.style.colors[col].r, 0, 255)
u8_slider(ctx, &ctx.style.colors[col].g, 0, 255)
u8_slider(ctx, &ctx.style.colors[col].b, 0, 255)
u8_slider(ctx, &ctx.style.colors[col].a, 0, 255)
mu.draw_rect(ctx, mu.layout_next(ctx), ctx.style.colors[col])
}
}
}
@Bios-Marcel
Copy link

Hey, I was wondering how to add any custom commands?

The original microui says you can easily add custom controls, but here i cant seem to add any custom commands as far as i can see, since the Command_Vatiant union is a fixed set of commands, so iterator cant return my new command. For example I was trying to draw other shapes besides revta, so I wanted to add draw circle and draw line.

Is there any documentation anywhere?

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