sdl2/microui

Code

microui_sdl_demo.odin ¶
404 linesSource

1package microui_sdl
2
3import "core:c"
4import "core:fmt"
5import "core:strings"
6import SDL "vendor:sdl2"
7import mu "vendor:microui"
8
9state := struct {
10	mu_ctx: mu.Context,
11	log_buf:         [1<<16]byte,
12	log_buf_len:     int,
13	log_buf_updated: bool,
14	bg: mu.Color,
15
16	atlas_texture: ^SDL.Texture,
17}{
18	bg = {90, 95, 100, 255},
19}
20
21main :: proc() {
22	if err := SDL.Init({.VIDEO}); err != 0 {
23		fmt.eprintln(err)
24		return
25	}
26	defer SDL.Quit()
27
28	window := SDL.CreateWindow("microui-odin", SDL.WINDOWPOS_UNDEFINED, SDL.WINDOWPOS_UNDEFINED, 960, 540, {.SHOWN, .RESIZABLE})
29	if window == nil {
30		fmt.eprintln(SDL.GetError())
31		return
32	}
33	defer SDL.DestroyWindow(window)
34
35	backend_idx: i32 = -1
36	if n := SDL.GetNumRenderDrivers(); n <= 0 {
37		fmt.eprintln("No render drivers available")
38		return
39	} else {
40		for i in 0..<n {
41			info: SDL.RendererInfo
42			if err := SDL.GetRenderDriverInfo(i, &info); err == 0 {
43				// NOTE(bill): "direct3d" seems to not work correctly
44				if info.name == "opengl" {
45					backend_idx = i
46					break
47				}
48			}
49		}
50	}
51
52	renderer := SDL.CreateRenderer(window, backend_idx, {.ACCELERATED, .PRESENTVSYNC})
53	if renderer == nil {
54		fmt.eprintln("SDL.CreateRenderer:", SDL.GetError())
55		return
56	}
57	defer SDL.DestroyRenderer(renderer)
58
59	state.atlas_texture = SDL.CreateTexture(renderer, .RGBA32, .TARGET, mu.DEFAULT_ATLAS_WIDTH, mu.DEFAULT_ATLAS_HEIGHT)
60	assert(state.atlas_texture != nil)
61	if err := SDL.SetTextureBlendMode(state.atlas_texture, .BLEND); err != 0 {
62		fmt.eprintln("SDL.SetTextureBlendMode:", err)
63		return
64	}
65
66	pixels := make([][4]u8, mu.DEFAULT_ATLAS_WIDTH*mu.DEFAULT_ATLAS_HEIGHT)
67	for alpha, i in mu.default_atlas_alpha {
68		pixels[i].rgb = 0xff
69		pixels[i].a   = alpha
70	}
71
72	if err := SDL.UpdateTexture(state.atlas_texture, nil, raw_data(pixels), 4*mu.DEFAULT_ATLAS_WIDTH); err != 0 {
73		fmt.eprintln("SDL.UpdateTexture:", err)
74		return
75	}
76
77	ctx := &state.mu_ctx
78	mu.init(ctx,
79		set_clipboard = proc(user_data: rawptr, text: string) -> (ok: bool) {
80			cstr := strings.clone_to_cstring(text)
81			SDL.SetClipboardText(cstr)
82			delete(cstr)
83			return true
84		},
85		get_clipboard = proc(user_data: rawptr) -> (text: string, ok: bool) {
86			if SDL.HasClipboardText() {
87				text = string(SDL.GetClipboardText())
88				ok = true
89			}
90			return
91		},
92	)
93
94	ctx.text_width = mu.default_atlas_text_width
95	ctx.text_height = mu.default_atlas_text_height
96
97
98	// NOTE: This allows for continuous rendering as the window resizes rather than waiting for the resize to finish
99	Resize_Data :: struct {
100		ctx:      ^mu.Context,
101		renderer: ^SDL.Renderer,
102	}
103	SDL.AddEventWatch(proc "c" (data: rawptr, event: ^SDL.Event) -> c.int {
104		if event.type == .WINDOWEVENT && event.window.event == .RESIZED {
105			resize_data := (^Resize_Data)(data)
106			render(resize_data.ctx, resize_data.renderer)
107		}
108		return 0
109	}, &Resize_Data{
110		ctx = ctx,
111		renderer = renderer,
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 .MOUSEMOTION:
122				mu.input_mouse_move(ctx, e.motion.x, e.motion.y)
123			case .MOUSEWHEEL:
124				mu.input_scroll(ctx, e.wheel.x * 30, e.wheel.y * -30)
125			case .TEXTINPUT:
126				mu.input_text(ctx, string(cstring(&e.text.text[0])))
127
128			case .MOUSEBUTTONDOWN, .MOUSEBUTTONUP:
129				fn := mu.input_mouse_down if e.type == .MOUSEBUTTONDOWN else mu.input_mouse_up
130				switch e.button.button {
131				case SDL.BUTTON_LEFT:   fn(ctx, e.button.x, e.button.y, .LEFT)
132				case SDL.BUTTON_MIDDLE: fn(ctx, e.button.x, e.button.y, .MIDDLE)
133				case SDL.BUTTON_RIGHT:  fn(ctx, e.button.x, e.button.y, .RIGHT)
134				}
135
136			case .KEYDOWN, .KEYUP:
137				if e.type == .KEYUP && e.key.keysym.sym == .ESCAPE {
138					SDL.PushEvent(&SDL.Event{type = .QUIT})
139				}
140
141				fn := mu.input_key_down if e.type == .KEYDOWN else mu.input_key_up
142
143				#partial switch e.key.keysym.sym {
144				case .LSHIFT:    fn(ctx, .SHIFT)
145				case .RSHIFT:    fn(ctx, .SHIFT)
146				case .LCTRL:     fn(ctx, .CTRL)
147				case .RCTRL:     fn(ctx, .CTRL)
148				case .LALT:      fn(ctx, .ALT)
149				case .RALT:      fn(ctx, .ALT)
150				case .RETURN:    fn(ctx, .RETURN)
151				case .KP_ENTER:  fn(ctx, .RETURN)
152				case .BACKSPACE: fn(ctx, .BACKSPACE)
153
154				case .LEFT:  fn(ctx, .LEFT)
155				case .RIGHT: fn(ctx, .RIGHT)
156				case .HOME:  fn(ctx, .HOME)
157				case .END:   fn(ctx, .END)
158				case .A:     fn(ctx, .A)
159				case .X:     fn(ctx, .X)
160				case .C:     fn(ctx, .C)
161				case .V:     fn(ctx, .V)
162				}
163			}
164		}
165
166		mu.begin(ctx)
167		all_windows(ctx)
168		mu.end(ctx)
169
170		render(ctx, renderer)
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" (ctx: ^mu.Context, renderer: ^SDL.Renderer) {
176	render_texture :: proc "contextless" (renderer: ^SDL.Renderer, dst: ^SDL.Rect, src: mu.Rect, color: mu.Color) {
177		dst.w = src.w
178		dst.h = 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.RenderCopy(renderer, state.atlas_texture, &SDL.Rect{src.x, src.y, src.w, src.h}, dst)
183	}
184
185	viewport_rect := &SDL.Rect{}
186	SDL.GetRendererOutputSize(renderer, &viewport_rect.w, &viewport_rect.h)
187	SDL.RenderSetViewport(renderer, viewport_rect)
188	SDL.RenderSetClipRect(renderer, viewport_rect)
189	SDL.SetRenderDrawColor(renderer, state.bg.r, state.bg.g, state.bg.b, state.bg.a)
190	SDL.RenderClear(renderer)
191
192	command_backing: ^mu.Command
193	for variant in mu.next_command_iterator(ctx, &command_backing) {
194		switch cmd in variant {
195		case ^mu.Command_Text:
196			dst := SDL.Rect{cmd.pos.x, 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(renderer, &dst, src, cmd.color)
202					dst.x += dst.w
203				}
204			}
205		case ^mu.Command_Rect:
206			SDL.SetRenderDrawColor(renderer, cmd.color.r, cmd.color.g, cmd.color.b, cmd.color.a)
207			SDL.RenderFillRect(renderer, &SDL.Rect{cmd.rect.x, cmd.rect.y, cmd.rect.w, 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(renderer, &SDL.Rect{x, y, 0, 0}, src, cmd.color)
213		case ^mu.Command_Clip:
214			SDL.RenderSetClipRect(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(renderer)
221}
222
223
224u8_slider :: proc(ctx: ^mu.Context, val: ^u8, lo, hi: u8) -> (res: mu.Result_Set) {
225	mu.push_id(ctx, uintptr(val))
226
227	@static tmp: mu.Real
228	tmp = mu.Real(val^)
229	res = mu.slider(ctx, &tmp, mu.Real(lo), mu.Real(hi), 0, "%.0f", {.ALIGN_CENTER})
230	val^ = u8(tmp)
231	mu.pop_id(ctx)
232	return
233}
234
235write_log :: proc(str: string) {
236	state.log_buf_len += copy(state.log_buf[state.log_buf_len:], str)
237	state.log_buf_len += copy(state.log_buf[state.log_buf_len:], "\n")
238	state.log_buf_updated = true
239}
240
241read_log :: proc() -> string {
242	return string(state.log_buf[:state.log_buf_len])
243}
244reset_log :: proc() {
245	state.log_buf_updated = true
246	state.log_buf_len = 0
247}
248
249
250all_windows :: proc(ctx: ^mu.Context) {
251	@static opts := mu.Options{.NO_CLOSE}
252
253	if mu.window(ctx, "Demo Window", {40, 40, 300, 450}, opts) {
254		if .ACTIVE in mu.header(ctx, "Window Info") {
255			win := mu.get_current_container(ctx)
256			mu.layout_row(ctx, {54, -1}, 0)
257			mu.label(ctx, "Position:")
258			mu.label(ctx, fmt.tprintf("%d, %d", win.rect.x, win.rect.y))
259			mu.label(ctx, "Size:")
260			mu.label(ctx, fmt.tprintf("%d, %d", win.rect.w, win.rect.h))
261		}
262
263		if .ACTIVE in mu.header(ctx, "Window Options") {
264			mu.layout_row(ctx, {120, 120, 120}, 0)
265			for opt in mu.Opt {
266				state := opt in opts
267				if .CHANGE in mu.checkbox(ctx, fmt.tprintf("%v", opt), &state)  {
268					if state {
269						opts += {opt}
270					} else {
271						opts -= {opt}
272					}
273				}
274			}
275		}
276
277		if .ACTIVE in mu.header(ctx, "Test Buttons", {.EXPANDED}) {
278			mu.layout_row(ctx, {86, -110, -1})
279			mu.label(ctx, "Test buttons 1:")
280			if .SUBMIT in mu.button(ctx, "Button 1") { write_log("Pressed button 1") }
281			if .SUBMIT in mu.button(ctx, "Button 2") { write_log("Pressed button 2") }
282			mu.label(ctx, "Test buttons 2:")
283			if .SUBMIT in mu.button(ctx, "Button 3") { write_log("Pressed button 3") }
284			if .SUBMIT in mu.button(ctx, "Button 4") { write_log("Pressed button 4") }
285		}
286
287		if .ACTIVE in mu.header(ctx, "Tree and Text", {.EXPANDED}) {
288			mu.layout_row(ctx, {140, -1})
289			mu.layout_begin_column(ctx)
290			if .ACTIVE in mu.treenode(ctx, "Test 1") {
291				if .ACTIVE in mu.treenode(ctx, "Test 1a") {
292					mu.label(ctx, "Hello")
293					mu.label(ctx, "world")
294				}
295				if .ACTIVE in mu.treenode(ctx, "Test 1b") {
296					if .SUBMIT in mu.button(ctx, "Button 1") { write_log("Pressed button 1") }
297					if .SUBMIT in mu.button(ctx, "Button 2") { write_log("Pressed button 2") }
298				}
299			}
300			if .ACTIVE in mu.treenode(ctx, "Test 2") {
301				mu.layout_row(ctx, {53, 53})
302				if .SUBMIT in mu.button(ctx, "Button 3") { write_log("Pressed button 3") }
303				if .SUBMIT in mu.button(ctx, "Button 4") { write_log("Pressed button 4") }
304				if .SUBMIT in mu.button(ctx, "Button 5") { write_log("Pressed button 5") }
305				if .SUBMIT in mu.button(ctx, "Button 6") { write_log("Pressed button 6") }
306			}
307			if .ACTIVE in mu.treenode(ctx, "Test 3") {
308				@static checks := [3]bool{true, false, true}
309				mu.checkbox(ctx, "Checkbox 1", &checks[0])
310				mu.checkbox(ctx, "Checkbox 2", &checks[1])
311				mu.checkbox(ctx, "Checkbox 3", &checks[2])
312
313			}
314			mu.layout_end_column(ctx)
315
316			mu.layout_begin_column(ctx)
317			mu.layout_row(ctx, {-1})
318			mu.text(ctx,
319				"Lorem ipsum dolor sit amet, consectetur adipiscing "+
320				"elit. Maecenas lacinia, sem eu lacinia molestie, mi risus faucibus "+
321				"ipsum, eu varius magna felis a nulla.",
322			)
323			mu.layout_end_column(ctx)
324		}
325
326		if .ACTIVE in mu.header(ctx, "Background Colour", {.EXPANDED}) {
327			mu.layout_row(ctx, {-78, -1}, 68)
328			mu.layout_begin_column(ctx)
329			{
330				mu.layout_row(ctx, {46, -1}, 0)
331				mu.label(ctx, "Red:");   u8_slider(ctx, &state.bg.r, 0, 255)
332				mu.label(ctx, "Green:"); u8_slider(ctx, &state.bg.g, 0, 255)
333				mu.label(ctx, "Blue:");  u8_slider(ctx, &state.bg.b, 0, 255)
334			}
335			mu.layout_end_column(ctx)
336
337			r := mu.layout_next(ctx)
338			mu.draw_rect(ctx, r, state.bg)
339			mu.draw_box(ctx, mu.expand_rect(r, 1), ctx.style.colors[.BORDER])
340			mu.draw_control_text(ctx, fmt.tprintf("#%02x%02x%02x", state.bg.r, state.bg.g, state.bg.b), r, .TEXT, {.ALIGN_CENTER})
341		}
342	}
343
344	if mu.window(ctx, "Log Window", {350, 40, 300, 200}, opts) {
345		mu.layout_row(ctx, {-1}, -28)
346		mu.begin_panel(ctx, "Log")
347		mu.layout_row(ctx, {-1}, -1)
348		mu.text(ctx, read_log())
349		if state.log_buf_updated {
350			panel := mu.get_current_container(ctx)
351			panel.scroll.y = panel.content_size.y
352			state.log_buf_updated = false
353		}
354		mu.end_panel(ctx)
355
356		@static buf: [128]byte
357		@static buf_len: int
358		submitted := false
359		mu.layout_row(ctx, {-70, -1})
360		if .SUBMIT in mu.textbox(ctx, buf[:], &buf_len) {
361			mu.set_focus(ctx, ctx.last_id)
362			submitted = true
363		}
364		if .SUBMIT in mu.button(ctx, "Submit") {
365			submitted = true
366		}
367		if submitted {
368			write_log(string(buf[:buf_len]))
369			buf_len = 0
370		}
371	}
372
373	if mu.window(ctx, "Style Window", {350, 250, 300, 240}) {
374		@static colors := [mu.Color_Type]string{
375			.TEXT         = "text",
376			.BORDER       = "border",
377			.WINDOW_BG    = "window bg",
378			.TITLE_BG     = "title bg",
379			.TITLE_TEXT   = "title text",
380			.PANEL_BG     = "panel bg",
381			.BUTTON       = "button",
382			.BUTTON_HOVER = "button hover",
383			.BUTTON_FOCUS = "button focus",
384			.BASE         = "base",
385			.BASE_HOVER   = "base hover",
386			.BASE_FOCUS   = "base focus",
387			.SCROLL_BASE  = "scroll base",
388			.SCROLL_THUMB = "scroll thumb",
389			.SELECTION_BG = "selection bg",
390		}
391
392		sw := i32(f32(mu.get_current_container(ctx).body.w) * 0.14)
393		mu.layout_row(ctx, {80, sw, sw, sw, sw, -1})
394		for label, col in colors {
395			mu.label(ctx, label)
396			u8_slider(ctx, &ctx.style.colors[col].r, 0, 255)
397			u8_slider(ctx, &ctx.style.colors[col].g, 0, 255)
398			u8_slider(ctx, &ctx.style.colors[col].b, 0, 255)
399			u8_slider(ctx, &ctx.style.colors[col].a, 0, 255)
400			mu.draw_rect(ctx, mu.layout_next(ctx), ctx.style.colors[col])
401		}
402	}
403
404}

Declarations Used 96