1package microui_sdl
2
3import "core:fmt"
4import "core:strings"
5import SDL "vendor:sdl3"
6import mu "vendor:microui"
7
8State :: struct {
9 mu_ctx: mu.Context,
10 log_buf: [1<<16]byte,
11 log_buf_len: int,
12 log_buf_updated: bool,
13 bg: mu.Color,
14
15 atlas_texture: ^SDL.Texture,
16 window: ^SDL.Window,
17 renderer: ^SDL.Renderer,
18}
19state := State {
20 bg = {90, 95, 100, 255},
21}
22
23main :: proc() {
24 num_renderers := SDL.GetNumRenderDrivers()
25 fmt.println("Renderers:")
26 for i in i32(0)..<num_renderers {
27 fmt.printfln("\t%s", SDL.GetRenderDriver(i))
28 }
29
30 if !SDL.Init(SDL.INIT_VIDEO) {
31 fmt.eprintfln("Couldn't initialize SDL: %s", SDL.GetError())
32 return
33 }
34 defer SDL.Quit()
35
36 if !SDL.CreateWindowAndRenderer("microui-odin", 960, 540, SDL.WINDOW_RESIZABLE, &state.window, &state.renderer) {
37 fmt.eprintfln("Couldn't create window/renderer: %s", SDL.GetError())
38 return
39 }
40 defer {
41 SDL.DestroyRenderer(state.renderer)
42 SDL.DestroyWindow(state.window)
43 }
44
45 fmt.printfln("Selected renderer: %s", SDL.GetRendererName(state.renderer))
46
47 if !SDL.SetRenderVSync(state.renderer, 1) {
48 fmt.eprintfln("Couldn't set render vsync: %s", SDL.GetError())
49 return
50 }
51
52 state.atlas_texture = SDL.CreateTexture(state.renderer, .RGBA32, .TARGET, mu.DEFAULT_ATLAS_WIDTH, mu.DEFAULT_ATLAS_HEIGHT)
53 assert(state.atlas_texture != nil)
54 if !SDL.SetTextureBlendMode(state.atlas_texture, {.BLEND}) {
55 fmt.eprintln("SDL.SetTextureBlendMode: %s", SDL.GetError())
56 return
57 }
58
59 pixels := make([][4]u8, mu.DEFAULT_ATLAS_WIDTH*mu.DEFAULT_ATLAS_HEIGHT)
60 for alpha, i in mu.default_atlas_alpha {
61 pixels[i].rgb = 0xff
62 pixels[i].a = alpha
63 }
64
65 if !SDL.UpdateTexture(state.atlas_texture, nil, raw_data(pixels), 4*mu.DEFAULT_ATLAS_WIDTH) {
66 fmt.eprintln("SDL.UpdateTexture: %s", SDL.GetError())
67 return
68 }
69
70
71
72
73
74
75
76
77 if !SDL.StartTextInput(state.window) {
78 fmt.eprintfln("SDL.StartTextInput: %s", SDL.GetError())
79 return
80 }
81 defer _ = SDL.StopTextInput(state.window)
82
83 ctx := &state.mu_ctx
84 mu.init(ctx,
85 set_clipboard = proc(user_data: rawptr, text: string) -> (ok: bool) {
86 cstr := strings.clone_to_cstring(text)
87 SDL.SetClipboardText(cstr)
88 delete(cstr)
89 return true
90 },
91 get_clipboard = proc(user_data: rawptr) -> (text: string, ok: bool) {
92 if SDL.HasClipboardText() {
93 text = string(cstring(SDL.GetClipboardText()))
94 ok = true
95 }
96 return
97 },
98 )
99
100 ctx.text_width = mu.default_atlas_text_width
101 ctx.text_height = mu.default_atlas_text_height
102
103
104 if !SDL.AddEventWatch(proc "c" (data: rawptr, event: ^SDL.Event) -> bool {
105 if event.type == .WINDOW_RESIZED {
106 state := (^State)(data)
107 render(state)
108 }
109 return false
110 }, &state) {
111 fmt.eprintfln("SDL.AddEventWatch: %s", SDL.GetError())
112 }
113
114 main_loop: for {
115 free_all(context.temp_allocator)
116
117 for e: SDL.Event; SDL.PollEvent(&e); {
118 #partial switch e.type {
119 case .QUIT:
120 break main_loop
121 case .MOUSE_MOTION:
122 mu.input_mouse_move(ctx, i32(e.motion.x), i32(e.motion.y))
123 case .MOUSE_WHEEL:
124 mu.input_scroll(ctx, i32(e.wheel.x) * 30, i32(e.wheel.y) * -30)
125 case .TEXT_INPUT:
126 mu.input_text(ctx, string(e.text.text))
127
128 case .MOUSE_BUTTON_DOWN, .MOUSE_BUTTON_UP:
129 fn := mu.input_mouse_down if e.button.down else mu.input_mouse_up
130 switch e.button.button {
131 case 1: fn(ctx, i32(e.button.x), i32(e.button.y), .LEFT)
132 case 3: fn(ctx, i32(e.button.x), i32(e.button.y), .MIDDLE)
133 case 2: fn(ctx, i32(e.button.x), i32(e.button.y), .RIGHT)
134 }
135
136 case .KEY_DOWN, .KEY_UP:
137 if e.type == .KEY_UP && e.key.scancode == .ESCAPE {
138 _ = SDL.PushEvent(&SDL.Event{type = .QUIT})
139 }
140
141 fn := mu.input_key_down if e.key.down else mu.input_key_up
142
143 switch e.key.key {
144 case SDL.K_LSHIFT: fn(ctx, .SHIFT)
145 case SDL.K_RSHIFT: fn(ctx, .SHIFT)
146 case SDL.K_LCTRL: fn(ctx, .CTRL)
147 case SDL.K_RCTRL: fn(ctx, .CTRL)
148 case SDL.K_LALT: fn(ctx, .ALT)
149 case SDL.K_RALT: fn(ctx, .ALT)
150 case SDL.K_RETURN: fn(ctx, .RETURN)
151 case SDL.K_KP_ENTER: fn(ctx, .RETURN)
152 case SDL.K_BACKSPACE: fn(ctx, .BACKSPACE)
153
154 case SDL.K_LEFT: fn(ctx, .LEFT)
155 case SDL.K_RIGHT: fn(ctx, .RIGHT)
156 case SDL.K_HOME: fn(ctx, .HOME)
157 case SDL.K_END: fn(ctx, .END)
158 case SDL.K_A: fn(ctx, .A)
159 case SDL.K_X: fn(ctx, .X)
160 case SDL.K_C: fn(ctx, .C)
161 case SDL.K_V: fn(ctx, .V)
162 }
163 }
164 }
165
166 mu.begin(ctx)
167 all_windows(ctx)
168 mu.end(ctx)
169
170 render(&state)
171 }
172}
173
174
175render :: proc "contextless" (state: ^State) {
176 render_texture :: proc "contextless" (renderer: ^SDL.Renderer, dst: ^SDL.FRect, src: mu.Rect, color: mu.Color) {
177 dst.w = f32(src.w)
178 dst.h = f32(src.h)
179
180 SDL.SetTextureAlphaMod(state.atlas_texture, color.a)
181 SDL.SetTextureColorMod(state.atlas_texture, color.r, color.g, color.b)
182 SDL.RenderTexture(renderer, state.atlas_texture, &SDL.FRect{f32(src.x), f32(src.y), f32(src.w), f32(src.h)}, dst)
183 }
184
185 viewport_rect := &SDL.Rect{}
186 SDL.GetRenderOutputSize(state.renderer, &viewport_rect.w, &viewport_rect.h)
187 SDL.SetRenderViewport(state.renderer, viewport_rect)
188 SDL.SetRenderClipRect(state.renderer, viewport_rect)
189 SDL.SetRenderDrawColor(state.renderer, state.bg.r, state.bg.g, state.bg.b, state.bg.a)
190 SDL.RenderClear(state.renderer)
191
192 command_backing: ^mu.Command
193 for variant in mu.next_command_iterator(&state.mu_ctx, &command_backing) {
194 switch cmd in variant {
195 case ^mu.Command_Text:
196 dst := SDL.FRect{f32(cmd.pos.x), f32(cmd.pos.y), 0, 0}
197 for ch in cmd.str {
198 if ch&0xc0 != 0x80 {
199 r := min(int(ch), 127)
200 src := mu.default_atlas[mu.DEFAULT_ATLAS_FONT + r]
201 render_texture(state.renderer, &dst, src, cmd.color)
202 dst.x += dst.w
203 }
204 }
205 case ^mu.Command_Rect:
206 SDL.SetRenderDrawColor(state.renderer, cmd.color.r, cmd.color.g, cmd.color.b, cmd.color.a)
207 SDL.RenderFillRect(state.renderer, &SDL.FRect{f32(cmd.rect.x), f32(cmd.rect.y), f32(cmd.rect.w), f32(cmd.rect.h)})
208 case ^mu.Command_Icon:
209 src := mu.default_atlas[cmd.id]
210 x := cmd.rect.x + (cmd.rect.w - src.w)/2
211 y := cmd.rect.y + (cmd.rect.h - src.h)/2
212 render_texture(state.renderer, &SDL.FRect{f32(x), f32(y), 0, 0}, src, cmd.color)
213 case ^mu.Command_Clip:
214 SDL.SetRenderClipRect(state.renderer, &SDL.Rect{cmd.rect.x, cmd.rect.y, cmd.rect.w, cmd.rect.h})
215 case ^mu.Command_Jump:
216 unreachable()
217 }
218 }
219
220 SDL.RenderPresent(state.renderer)
221}
222
223u8_slider :: proc(ctx: ^mu.Context, val: ^u8, lo, hi: u8) -> (res: mu.Result_Set) {
224 mu.push_id(ctx, uintptr(val))
225
226 @static tmp: mu.Real
227 tmp = mu.Real(val^)
228 res = mu.slider(ctx, &tmp, mu.Real(lo), mu.Real(hi), 0, "%.0f", {.ALIGN_CENTER})
229 val^ = u8(tmp)
230 mu.pop_id(ctx)
231 return
232}
233
234write_log :: proc(str: string) {
235 state.log_buf_len += copy(state.log_buf[state.log_buf_len:], str)
236 state.log_buf_len += copy(state.log_buf[state.log_buf_len:], "\n")
237 state.log_buf_updated = true
238}
239
240read_log :: proc() -> string {
241 return string(state.log_buf[:state.log_buf_len])
242}
243reset_log :: proc() {
244 state.log_buf_updated = true
245 state.log_buf_len = 0
246}
247
248
249all_windows :: proc(ctx: ^mu.Context) {
250 @static opts := mu.Options{.NO_CLOSE}
251
252 if mu.window(ctx, "Demo Window", {40, 40, 300, 450}, opts) {
253 if .ACTIVE in mu.header(ctx, "Window Info") {
254 win := mu.get_current_container(ctx)
255 mu.layout_row(ctx, {54, -1}, 0)
256 mu.label(ctx, "Position:")
257 mu.label(ctx, fmt.tprintf("%d, %d", win.rect.x, win.rect.y))
258 mu.label(ctx, "Size:")
259 mu.label(ctx, fmt.tprintf("%d, %d", win.rect.w, win.rect.h))
260 }
261
262 if .ACTIVE in mu.header(ctx, "Window Options") {
263 mu.layout_row(ctx, {120, 120, 120}, 0)
264 for opt in mu.Opt {
265 state := opt in opts
266 if .CHANGE in mu.checkbox(ctx, fmt.tprintf("%v", opt), &state) {
267 if state {
268 opts += {opt}
269 } else {
270 opts -= {opt}
271 }
272 }
273 }
274 }
275
276 if .ACTIVE in mu.header(ctx, "Test Buttons", {.EXPANDED}) {
277 mu.layout_row(ctx, {86, -110, -1})
278 mu.label(ctx, "Test buttons 1:")
279 if .SUBMIT in mu.button(ctx, "Button 1") { write_log("Pressed button 1") }
280 if .SUBMIT in mu.button(ctx, "Button 2") { write_log("Pressed button 2") }
281 mu.label(ctx, "Test buttons 2:")
282 if .SUBMIT in mu.button(ctx, "Button 3") { write_log("Pressed button 3") }
283 if .SUBMIT in mu.button(ctx, "Button 4") { write_log("Pressed button 4") }
284 }
285
286 if .ACTIVE in mu.header(ctx, "Tree and Text", {.EXPANDED}) {
287 mu.layout_row(ctx, {140, -1})
288 mu.layout_begin_column(ctx)
289 if .ACTIVE in mu.treenode(ctx, "Test 1") {
290 if .ACTIVE in mu.treenode(ctx, "Test 1a") {
291 mu.label(ctx, "Hello")
292 mu.label(ctx, "world")
293 }
294 if .ACTIVE in mu.treenode(ctx, "Test 1b") {
295 if .SUBMIT in mu.button(ctx, "Button 1") { write_log("Pressed button 1") }
296 if .SUBMIT in mu.button(ctx, "Button 2") { write_log("Pressed button 2") }
297 }
298 }
299 if .ACTIVE in mu.treenode(ctx, "Test 2") {
300 mu.layout_row(ctx, {53, 53})
301 if .SUBMIT in mu.button(ctx, "Button 3") { write_log("Pressed button 3") }
302 if .SUBMIT in mu.button(ctx, "Button 4") { write_log("Pressed button 4") }
303 if .SUBMIT in mu.button(ctx, "Button 5") { write_log("Pressed button 5") }
304 if .SUBMIT in mu.button(ctx, "Button 6") { write_log("Pressed button 6") }
305 }
306 if .ACTIVE in mu.treenode(ctx, "Test 3") {
307 @static checks := [3]bool{true, false, true}
308 mu.checkbox(ctx, "Checkbox 1", &checks[0])
309 mu.checkbox(ctx, "Checkbox 2", &checks[1])
310 mu.checkbox(ctx, "Checkbox 3", &checks[2])
311
312 }
313 mu.layout_end_column(ctx)
314
315 mu.layout_begin_column(ctx)
316 mu.layout_row(ctx, {-1})
317 mu.text(ctx,
318 "Lorem ipsum dolor sit amet, consectetur adipiscing "+
319 "elit. Maecenas lacinia, sem eu lacinia molestie, mi risus faucibus "+
320 "ipsum, eu varius magna felis a nulla.",
321 )
322 mu.layout_end_column(ctx)
323 }
324
325 if .ACTIVE in mu.header(ctx, "Background Colour", {.EXPANDED}) {
326 mu.layout_row(ctx, {-78, -1}, 68)
327 mu.layout_begin_column(ctx)
328 {
329 mu.layout_row(ctx, {46, -1}, 0)
330 mu.label(ctx, "Red:"); u8_slider(ctx, &state.bg.r, 0, 255)
331 mu.label(ctx, "Green:"); u8_slider(ctx, &state.bg.g, 0, 255)
332 mu.label(ctx, "Blue:"); u8_slider(ctx, &state.bg.b, 0, 255)
333 }
334 mu.layout_end_column(ctx)
335
336 r := mu.layout_next(ctx)
337 mu.draw_rect(ctx, r, state.bg)
338 mu.draw_box(ctx, mu.expand_rect(r, 1), ctx.style.colors[.BORDER])
339 mu.draw_control_text(ctx, fmt.tprintf("#%02x%02x%02x", state.bg.r, state.bg.g, state.bg.b), r, .TEXT, {.ALIGN_CENTER})
340 }
341 }
342
343 if mu.window(ctx, "Log Window", {350, 40, 300, 200}, opts) {
344 mu.layout_row(ctx, {-1}, -28)
345 mu.begin_panel(ctx, "Log")
346 mu.layout_row(ctx, {-1}, -1)
347 mu.text(ctx, read_log())
348 if state.log_buf_updated {
349 panel := mu.get_current_container(ctx)
350 panel.scroll.y = panel.content_size.y
351 state.log_buf_updated = false
352 }
353 mu.end_panel(ctx)
354
355 @static buf: [128]byte
356 @static buf_len: int
357 submitted := false
358 mu.layout_row(ctx, {-70, -1})
359 if .SUBMIT in mu.textbox(ctx, buf[:], &buf_len) {
360 mu.set_focus(ctx, ctx.last_id)
361 submitted = true
362 }
363 if .SUBMIT in mu.button(ctx, "Submit") {
364 submitted = true
365 }
366 if submitted {
367 write_log(string(buf[:buf_len]))
368 buf_len = 0
369 }
370 }
371
372 if mu.window(ctx, "Style Window", {350, 250, 300, 240}) {
373 @static colors := [mu.Color_Type]string{
374 .TEXT = "text",
375 .BORDER = "border",
376 .WINDOW_BG = "window bg",
377 .TITLE_BG = "title bg",
378 .TITLE_TEXT = "title text",
379 .PANEL_BG = "panel bg",
380 .BUTTON = "button",
381 .BUTTON_HOVER = "button hover",
382 .BUTTON_FOCUS = "button focus",
383 .BASE = "base",
384 .BASE_HOVER = "base hover",
385 .BASE_FOCUS = "base focus",
386 .SCROLL_BASE = "scroll base",
387 .SCROLL_THUMB = "scroll thumb",
388 .SELECTION_BG = "selection bg",
389 }
390
391 sw := i32(f32(mu.get_current_container(ctx).body.w) * 0.14)
392 mu.layout_row(ctx, {80, sw, sw, sw, sw, -1})
393 for label, col in colors {
394 mu.label(ctx, label)
395 u8_slider(ctx, &ctx.style.colors[col].r, 0, 255)
396 u8_slider(ctx, &ctx.style.colors[col].g, 0, 255)
397 u8_slider(ctx, &ctx.style.colors[col].b, 0, 255)
398 u8_slider(ctx, &ctx.style.colors[col].a, 0, 255)
399 mu.draw_rect(ctx, mu.layout_next(ctx), ctx.style.colors[col])
400 }
401 }
402}