sdl3/microui

Code

microui_sdl_demo.odin ¶
402 linesSource

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	// NOTE(Jeroen): We don't see text edit events from SDL 3 unless we enable it.
71	//               Microui doesn't give you a handle to its widgets so you can ask if a given
72	//               widget or class of widget has focus, but it thankfully ignores spurious text edit
73	//               events whenever a `mu.textbox` doesn't have focus, so we can just enable it globally.
74	//
75	//               A well-designed application may want to consider selectively enabling these events
76	//               only when a text box has focus.
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	// NOTE: This allows for continuous rendering as the window resizes rather than waiting for the resize to finish
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// NOTE: This is marked as "contextless" so that the 'context' does need to be set up in the `SDL.AddEventWatch` callback
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}

Declarations Used 117