wgpu/microui Web

Code

main.odin ¶
229 linesSource

1package vendor_wgpu_example_microui
2
3import    "base:runtime"
4
5import    "core:fmt"
6
7import mu "vendor:microui"
8
9state := struct {
10	ctx:             runtime.Context,
11
12	mu_ctx:          mu.Context,
13	log_buf:         [1<<16]byte,
14	log_buf_len:     int,
15	log_buf_updated: bool,
16	bg:              mu.Color,
17
18	os:              OS,
19	renderer:        Renderer,
20
21	cursor:          [2]i32,
22}{
23	bg = {90, 95, 100, 255},
24}
25
26main :: proc() {
27	state.ctx = context
28
29	mu.init(&state.mu_ctx, os_set_clipboard, os_get_clipboard, nil)
30	state.mu_ctx.text_width  = mu.default_atlas_text_width
31	state.mu_ctx.text_height = mu.default_atlas_text_height
32
33	os_init()
34	r_init_and_run()
35}
36
37frame :: proc(dt: f32) {
38	free_all(context.temp_allocator)
39
40	mc := &state.mu_ctx
41
42	mu.begin(mc)
43
44	demo_windows(mc)
45
46	mu.end(mc)
47
48	r_render()
49}
50
51u8_slider :: proc(ctx: ^mu.Context, val: ^u8, lo, hi: u8) -> (res: mu.Result_Set) {
52	mu.push_id(ctx, uintptr(val))
53
54	@static tmp: mu.Real
55	tmp = mu.Real(val^)
56	res = mu.slider(ctx, &tmp, mu.Real(lo), mu.Real(hi), 0, "%.0f", {.ALIGN_CENTER})
57	val^ = u8(tmp)
58	mu.pop_id(ctx)
59	return
60}
61
62write_log :: proc(str: string) {
63	state.log_buf_len += copy(state.log_buf[state.log_buf_len:], str)
64	state.log_buf_len += copy(state.log_buf[state.log_buf_len:], "\n")
65	state.log_buf_updated = true
66}
67
68read_log :: proc() -> string {
69	return string(state.log_buf[:state.log_buf_len])
70}
71reset_log :: proc() {
72	state.log_buf_updated = true
73	state.log_buf_len = 0
74}
75
76demo_windows :: proc(ctx: ^mu.Context) {
77	@static opts := mu.Options{.NO_CLOSE}
78
79	if mu.window(ctx, "Demo Window", {40, 40, 300, 450}, opts) {
80		if .ACTIVE in mu.header(ctx, "Window Info") {
81			win := mu.get_current_container(ctx)
82			mu.layout_row(ctx, {54, -1}, 0)
83			mu.label(ctx, "Position:")
84			mu.label(ctx, fmt.tprintf("%d, %d", win.rect.x, win.rect.y))
85			mu.label(ctx, "Size:")
86			mu.label(ctx, fmt.tprintf("%d, %d", win.rect.w, win.rect.h))
87		}
88
89		if .ACTIVE in mu.header(ctx, "Window Options") {
90			mu.layout_row(ctx, {120, 120, 120}, 0)
91			for opt in mu.Opt {
92				state := opt in opts
93				if .CHANGE in mu.checkbox(ctx, fmt.tprintf("%v", opt), &state)  {
94					if state {
95						opts += {opt}
96					} else {
97						opts -= {opt}
98					}
99				}
100			}
101		}
102
103		if .ACTIVE in mu.header(ctx, "Test Buttons", {.EXPANDED}) {
104			mu.layout_row(ctx, {86, -110, -1})
105			mu.label(ctx, "Test buttons 1:")
106			if .SUBMIT in mu.button(ctx, "Button 1") { write_log("Pressed button 1") }
107			if .SUBMIT in mu.button(ctx, "Button 2") { write_log("Pressed button 2") }
108			mu.label(ctx, "Test buttons 2:")
109			if .SUBMIT in mu.button(ctx, "Button 3") { write_log("Pressed button 3") }
110			if .SUBMIT in mu.button(ctx, "Button 4") { write_log("Pressed button 4") }
111		}
112
113		if .ACTIVE in mu.header(ctx, "Tree and Text", {.EXPANDED}) {
114			mu.layout_row(ctx, {140, -1})
115			mu.layout_begin_column(ctx)
116			if .ACTIVE in mu.treenode(ctx, "Test 1") {
117				if .ACTIVE in mu.treenode(ctx, "Test 1a") {
118					mu.label(ctx, "Hello")
119					mu.label(ctx, "world")
120				}
121				if .ACTIVE in mu.treenode(ctx, "Test 1b") {
122					if .SUBMIT in mu.button(ctx, "Button 1") { write_log("Pressed button 1") }
123					if .SUBMIT in mu.button(ctx, "Button 2") { write_log("Pressed button 2") }
124				}
125			}
126			if .ACTIVE in mu.treenode(ctx, "Test 2") {
127				mu.layout_row(ctx, {53, 53})
128				if .SUBMIT in mu.button(ctx, "Button 3") { write_log("Pressed button 3") }
129				if .SUBMIT in mu.button(ctx, "Button 4") { write_log("Pressed button 4") }
130				if .SUBMIT in mu.button(ctx, "Button 5") { write_log("Pressed button 5") }
131				if .SUBMIT in mu.button(ctx, "Button 6") { write_log("Pressed button 6") }
132			}
133			if .ACTIVE in mu.treenode(ctx, "Test 3") {
134				@static checks := [3]bool{true, false, true}
135				mu.checkbox(ctx, "Checkbox 1", &checks[0])
136				mu.checkbox(ctx, "Checkbox 2", &checks[1])
137				mu.checkbox(ctx, "Checkbox 3", &checks[2])
138
139			}
140			mu.layout_end_column(ctx)
141
142			mu.layout_begin_column(ctx)
143			mu.layout_row(ctx, {-1})
144			mu.text(ctx,
145				"Lorem ipsum dolor sit amet, consectetur adipiscing "+
146				"elit. Maecenas lacinia, sem eu lacinia molestie, mi risus faucibus "+
147				"ipsum, eu varius magna felis a nulla.",
148			)
149			mu.layout_end_column(ctx)
150		}
151
152		if .ACTIVE in mu.header(ctx, "Background Colour", {.EXPANDED}) {
153			mu.layout_row(ctx, {-78, -1}, 68)
154			mu.layout_begin_column(ctx)
155			{
156				mu.layout_row(ctx, {46, -1}, 0)
157				mu.label(ctx, "Red:");   u8_slider(ctx, &state.bg.r, 0, 255)
158				mu.label(ctx, "Green:"); u8_slider(ctx, &state.bg.g, 0, 255)
159				mu.label(ctx, "Blue:");  u8_slider(ctx, &state.bg.b, 0, 255)
160			}
161			mu.layout_end_column(ctx)
162
163			r := mu.layout_next(ctx)
164			mu.draw_rect(ctx, r, state.bg)
165			mu.draw_box(ctx, mu.expand_rect(r, 1), ctx.style.colors[.BORDER])
166			mu.draw_control_text(ctx, fmt.tprintf("#%02x%02x%02x", state.bg.r, state.bg.g, state.bg.b), r, .TEXT, {.ALIGN_CENTER})
167		}
168	}
169
170	if mu.window(ctx, "Log Window", {350, 40, 300, 200}, opts) {
171		mu.layout_row(ctx, {-1}, -28)
172		mu.begin_panel(ctx, "Log")
173		mu.layout_row(ctx, {-1}, -1)
174		mu.text(ctx, read_log())
175		if state.log_buf_updated {
176			panel := mu.get_current_container(ctx)
177			panel.scroll.y = panel.content_size.y
178			state.log_buf_updated = false
179		}
180		mu.end_panel(ctx)
181
182		@static buf: [128]byte
183		@static buf_len: int
184		submitted := false
185		mu.layout_row(ctx, {-70, -1})
186		if .SUBMIT in mu.textbox(ctx, buf[:], &buf_len) {
187			mu.set_focus(ctx, ctx.last_id)
188			submitted = true
189		}
190		if .SUBMIT in mu.button(ctx, "Submit") {
191			submitted = true
192		}
193		if submitted {
194			write_log(string(buf[:buf_len]))
195			buf_len = 0
196		}
197	}
198
199	if mu.window(ctx, "Style Window", {350, 250, 300, 240}) {
200		@static colors := [mu.Color_Type]string{
201			.TEXT         = "text",
202			.BORDER       = "border",
203			.WINDOW_BG    = "window bg",
204			.TITLE_BG     = "title bg",
205			.TITLE_TEXT   = "title text",
206			.PANEL_BG     = "panel bg",
207			.BUTTON       = "button",
208			.BUTTON_HOVER = "button hover",
209			.BUTTON_FOCUS = "button focus",
210			.BASE         = "base",
211			.BASE_HOVER   = "base hover",
212			.BASE_FOCUS   = "base focus",
213			.SCROLL_BASE  = "scroll base",
214			.SCROLL_THUMB = "scroll thumb",
215			.SELECTION_BG = "selection bg",
216		}
217
218		sw := i32(f32(mu.get_current_container(ctx).body.w) * 0.14)
219		mu.layout_row(ctx, {80, sw, sw, sw, sw, -1})
220		for label, col in colors {
221			mu.label(ctx, label)
222			u8_slider(ctx, &ctx.style.colors[col].r, 0, 255)
223			u8_slider(ctx, &ctx.style.colors[col].g, 0, 255)
224			u8_slider(ctx, &ctx.style.colors[col].b, 0, 255)
225			u8_slider(ctx, &ctx.style.colors[col].a, 0, 255)
226			mu.draw_rect(ctx, mu.layout_next(ctx), ctx.style.colors[col])
227		}
228	}
229}

os_glfw.odin ¶ !js
158 linesSource

1#+build !js
2package vendor_wgpu_example_microui
3
4import     "core:log"
5import     "core:math"
6import     "core:strings"
7import     "core:time"
8import     "core:unicode/utf8"
9
10import     "vendor:glfw"
11import     "vendor:wgpu"
12import     "vendor:wgpu/glfwglue"
13import mu  "vendor:microui"
14
15OS :: struct {
16	window: glfw.WindowHandle,
17}
18
19os_init :: proc() {
20	if !glfw.Init() {
21		panic("[glfw] init failure")
22	}
23
24	glfw.WindowHint(glfw.CLIENT_API, glfw.NO_API)
25	state.os.window = glfw.CreateWindow(960, 540, "ODIN / GLFW / MICROUI / WGPU", nil, nil)
26	assert(state.os.window != nil)
27
28	glfw.SetKeyCallback(state.os.window, key_callback)
29	glfw.SetMouseButtonCallback(state.os.window, mouse_button_callback)
30	glfw.SetCursorPosCallback(state.os.window, cursor_pos_callback)
31	glfw.SetScrollCallback(state.os.window, scroll_callback)
32	glfw.SetCharCallback(state.os.window, char_callback)
33	glfw.SetFramebufferSizeCallback(state.os.window, size_callback)
34}
35
36os_run :: proc() {
37	for !glfw.WindowShouldClose(state.os.window) {
38		glfw.PollEvents()
39		do_frame()
40	}
41
42	glfw.DestroyWindow(state.os.window)
43	glfw.Terminate()
44}
45
46@(private="file")
47do_frame :: proc() {
48	@static frame_time: time.Tick
49	if frame_time == {} {
50		frame_time = time.tick_now()
51	}
52
53	new_frame_time := time.tick_now()
54	dt := time.tick_diff(frame_time, new_frame_time)
55	frame_time = new_frame_time
56
57	frame(f32(time.duration_seconds(dt)))
58}
59
60os_get_render_bounds :: proc() -> (width, height: u32) {
61	iw, ih := glfw.GetFramebufferSize(state.os.window)
62	return u32(iw), u32(ih)
63}
64
65os_get_dpi :: proc() -> f32 {
66	sw, sh := glfw.GetWindowContentScale(state.os.window)
67	if sw != sh {
68		log.warnf("dpi x (%v) and y (%v) not the same", sw, sh)
69	}
70	return sw
71}
72
73os_get_surface :: proc(instance: wgpu.Instance) -> wgpu.Surface {
74	return glfwglue.GetSurface(instance, state.os.window)
75}
76
77os_set_clipboard :: proc(_: rawptr, text: string) -> bool {
78	glfw.SetClipboardString(state.os.window, strings.clone_to_cstring(text, context.temp_allocator))
79	return true
80}
81
82os_get_clipboard :: proc(_: rawptr) -> (string, bool) {
83	clipboard := glfw.GetClipboardString(state.os.window)
84	return clipboard, true
85}
86
87@(private="file")
88key_callback :: proc "c" (window: glfw.WindowHandle, key, scancode, action, mods: i32) {
89	context = state.ctx
90
91	mu_key: mu.Key; switch key {
92	case glfw.KEY_LEFT_SHIFT, glfw.KEY_RIGHT_SHIFT:     mu_key = .ALT
93	case glfw.KEY_LEFT_CONTROL, glfw.KEY_RIGHT_CONTROL,
94	     glfw.KEY_LEFT_SUPER, glfw.KEY_RIGHT_SUPER:     mu_key = .CTRL
95	case glfw.KEY_LEFT_ALT, glfw.KEY_RIGHT_ALT:         mu_key = .ALT
96	case glfw.KEY_BACKSPACE:                            mu_key = .BACKSPACE
97	case glfw.KEY_DELETE:                               mu_key = .DELETE
98	case glfw.KEY_ENTER:                                mu_key = .RETURN
99	case glfw.KEY_LEFT:                                 mu_key = .LEFT
100	case glfw.KEY_RIGHT:                                mu_key = .RIGHT
101	case glfw.KEY_HOME:                                 mu_key = .HOME
102	case glfw.KEY_END:                                  mu_key = .END
103	case glfw.KEY_A:                                    mu_key = .A
104	case glfw.KEY_X:                                    mu_key = .X
105	case glfw.KEY_C:                                    mu_key = .C
106	case glfw.KEY_V:                                    mu_key = .V
107	case:                                               return
108	}
109
110	switch action {
111	case glfw.PRESS, glfw.REPEAT: mu.input_key_down(&state.mu_ctx, mu_key)
112	case glfw.RELEASE:            mu.input_key_up  (&state.mu_ctx, mu_key)
113	case:                         return
114	}
115}
116
117@(private="file")
118mouse_button_callback :: proc "c" (window: glfw.WindowHandle, key, action, mods: i32) {
119	context = state.ctx
120
121	mu_key: mu.Mouse; switch key {
122	case glfw.MOUSE_BUTTON_MIDDLE: mu_key = .MIDDLE
123	case glfw.MOUSE_BUTTON_LEFT:   mu_key = .LEFT
124	case glfw.MOUSE_BUTTON_RIGHT:  mu_key = .RIGHT
125	}
126
127	switch action {
128	case glfw.PRESS, glfw.REPEAT: mu.input_mouse_down(&state.mu_ctx, state.cursor.x, state.cursor.y, mu_key)
129	case glfw.RELEASE:            mu.input_mouse_up  (&state.mu_ctx, state.cursor.x, state.cursor.y, mu_key)
130	}
131}
132
133@(private="file")
134cursor_pos_callback :: proc "c" (window: glfw.WindowHandle, x, y: f64) {
135	context = state.ctx
136	state.cursor = {i32(math.round(x)), i32(math.round(y))}
137	mu.input_mouse_move(&state.mu_ctx, state.cursor.x, state.cursor.y)
138}
139
140@(private="file")
141scroll_callback :: proc "c" (window: glfw.WindowHandle, x, y: f64) {
142	context = state.ctx
143	mu.input_scroll(&state.mu_ctx, -i32(math.round(x)), -i32(math.round(y)))
144}
145
146@(private="file")
147char_callback :: proc "c" (window: glfw.WindowHandle, ch: rune) {
148	context = state.ctx
149	bytes, size := utf8.encode_rune(ch)
150	mu.input_text(&state.mu_ctx, string(bytes[:size]))
151}
152
153@(private="file")
154size_callback :: proc "c" (window: glfw.WindowHandle, width, height: i32) {
155	context = state.ctx
156	r_resize()
157	do_frame()
158}

os_js.odin ¶
199 linesSource

1#+feature dynamic-literals
2package vendor_wgpu_example_microui
3
4import    "core:sys/wasm/js"
5import    "core:unicode"
6import    "core:unicode/utf8"
7
8import    "vendor:wgpu"
9import mu "vendor:microui"
10
11OS :: struct {
12	initialized: bool,
13	clipboard:   [dynamic]byte,
14}
15
16os_init :: proc() {
17	state.os.clipboard.allocator = context.allocator
18	assert(js.add_window_event_listener(.Key_Down, nil, key_down_callback))
19	assert(js.add_window_event_listener(.Key_Up, nil, key_up_callback))
20	assert(js.add_window_event_listener(.Mouse_Down, nil, mouse_down_callback))
21	assert(js.add_window_event_listener(.Mouse_Up, nil, mouse_up_callback))
22	assert(js.add_event_listener("wgpu-canvas", .Mouse_Move, nil, mouse_move_callback))
23	assert(js.add_window_event_listener(.Wheel, nil, scroll_callback))
24	assert(js.add_window_event_listener(.Resize, nil, size_callback))
25	assert(js.add_window_event_listener(.Blur, nil, blur_callback))
26}
27
28// NOTE: frame loop is done by the runtime.js repeatedly calling `step`.
29os_run :: proc() {
30	state.os.initialized = true
31}
32
33@(private="file", export)
34step :: proc(dt: f32) -> bool {
35	context = state.ctx
36
37	if !state.os.initialized {
38		return true
39	}
40
41	frame(dt)
42	return true
43}
44
45
46os_fini :: proc() {
47	js.remove_window_event_listener(.Key_Down, nil, key_down_callback)
48	js.remove_window_event_listener(.Key_Up, nil, key_up_callback)
49	js.remove_window_event_listener(.Mouse_Down, nil, mouse_down_callback)
50	js.remove_window_event_listener(.Mouse_Up, nil, mouse_up_callback)
51	js.remove_event_listener("wgpu-canvas", .Mouse_Move, nil, mouse_move_callback)
52	js.remove_window_event_listener(.Wheel, nil, scroll_callback)
53	js.remove_window_event_listener(.Resize, nil, size_callback)
54	js.remove_window_event_listener(.Blur, nil, blur_callback)
55}
56
57os_get_render_bounds :: proc() -> (width, height: u32) {
58	rect := js.get_bounding_client_rect("body")
59	dpi := os_get_dpi()
60	return u32(f32(rect.width) * dpi), u32(f32(rect.height) * dpi)
61}
62
63os_get_dpi :: proc() -> f32 {
64	ratio := f32(js.device_pixel_ratio())
65	return ratio
66}
67
68os_get_surface :: proc(instance: wgpu.Instance) -> wgpu.Surface {
69	return wgpu.InstanceCreateSurface(
70		instance,
71		&wgpu.SurfaceDescriptor{
72			nextInChain = &wgpu.SurfaceSourceCanvasHTMLSelector{
73				sType = .SurfaceSourceCanvasHTMLSelector,
74				selector = "#wgpu-canvas",
75			},
76		},
77	)
78}
79
80os_set_clipboard :: proc(_: rawptr, text: string) -> bool {
81	// TODO: Use browser APIs
82	clear(&state.os.clipboard)
83	append(&state.os.clipboard, text)
84	return true
85}
86
87os_get_clipboard :: proc(_: rawptr) -> (string, bool) {
88	// TODO: Use browser APIs
89	return string(state.os.clipboard[:]), true
90}
91
92@(private="file")
93KEY_MAP := map[string]mu.Key{
94	"ShiftLeft"    = .SHIFT,
95	"ShiftRight"   = .SHIFT,
96	"ControlLeft"  = .CTRL,
97	"ControlRight" = .CTRL,
98	"MetaLeft"     = .CTRL,
99	"MetaRight"    = .CTRL,
100	"AltLeft"      = .ALT,
101	"AltRight"     = .ALT,
102	"Backspace"    = .BACKSPACE,
103	"Delete"       = .DELETE,
104	"Enter"        = .RETURN,
105	"ArrowLeft"    = .LEFT,
106	"ArrowRight"   = .RIGHT,
107	"Home"         = .HOME,
108	"End"          = .END,
109	"KeyA"         = .A,
110	"KeyX"         = .X,
111	"KeyC"         = .C,
112	"KeyV"         = .V,
113}
114
115@(private="file")
116key_down_callback :: proc(e: js.Event) {
117	context = state.ctx
118
119	js.event_prevent_default()
120
121	if k, ok := KEY_MAP[e.data.key.code]; ok {
122		mu.input_key_down(&state.mu_ctx, k)
123	}
124
125	if .CTRL in state.mu_ctx.key_down_bits {
126		return
127	}
128
129	ch, size := utf8.decode_rune(e.data.key.key)
130	if len(e.data.key.key) == size && unicode.is_print(ch) {
131		mu.input_text(&state.mu_ctx, e.data.key.key)
132	}
133}
134
135@(private="file")
136key_up_callback :: proc(e: js.Event) {
137	context = state.ctx
138
139	if k, ok := KEY_MAP[e.data.key.code]; ok {
140		mu.input_key_up(&state.mu_ctx, k)
141	}
142
143	js.event_prevent_default()
144}
145
146@(private="file")
147mouse_down_callback :: proc(e: js.Event) {
148	context = state.ctx
149
150	switch e.data.mouse.button {
151	case 0: mu.input_mouse_down(&state.mu_ctx, state.cursor.x, state.cursor.y, .LEFT)
152	case 1: mu.input_mouse_down(&state.mu_ctx, state.cursor.x, state.cursor.y, .MIDDLE)
153	case 2: mu.input_mouse_down(&state.mu_ctx, state.cursor.x, state.cursor.y, .RIGHT)
154	}
155
156	js.event_prevent_default()
157}
158
159@(private="file")
160mouse_up_callback :: proc(e: js.Event) {
161	context = state.ctx
162
163	switch e.data.mouse.button {
164	case 0: mu.input_mouse_up(&state.mu_ctx, state.cursor.x, state.cursor.y, .LEFT)
165	case 1: mu.input_mouse_up(&state.mu_ctx, state.cursor.x, state.cursor.y, .MIDDLE)
166	case 2: mu.input_mouse_up(&state.mu_ctx, state.cursor.x, state.cursor.y, .RIGHT)
167	}
168
169	js.event_prevent_default()
170}
171
172@(private="file")
173mouse_move_callback :: proc(e: js.Event) {
174	context = state.ctx
175	state.cursor = {i32(e.data.mouse.offset.x), i32(e.data.mouse.offset.y)}
176	mu.input_mouse_move(&state.mu_ctx, state.cursor.x, state.cursor.y)
177}
178
179@(private="file")
180scroll_callback :: proc(e: js.Event) {
181	context = state.ctx
182	mu.input_scroll(&state.mu_ctx, i32(e.data.wheel.delta.x), i32(e.data.wheel.delta.y))
183}
184
185@(private="file")
186size_callback :: proc(e: js.Event) {
187	context = state.ctx
188	r_resize()
189}
190
191@(private="file")
192blur_callback :: proc(e: js.Event) {
193	// Release all mouse buttons and keys when focus is lost.
194
195	state.mu_ctx.mouse_released_bits = state.mu_ctx.mouse_down_bits
196	state.mu_ctx.mouse_down_bits = {}
197
198	state.mu_ctx.key_down_bits = {}
199}

renderer.odin ¶
545 linesSource

1package vendor_wgpu_example_microui
2
3import intr "base:intrinsics"
4
5import      "core:fmt"
6import      "core:math/linalg"
7
8import mu   "vendor:microui"
9import      "vendor:wgpu"
10
11BUFFER_SIZE :: 16384
12
13Renderer :: struct {
14	instance: wgpu.Instance,
15	surface:  wgpu.Surface,
16	adapter:  wgpu.Adapter,
17	device:   wgpu.Device,
18	config:   wgpu.SurfaceConfiguration,
19
20	module: wgpu.ShaderModule,
21
22	atlas_texture:      wgpu.Texture,
23	atlas_texture_view: wgpu.TextureView,
24
25	pipeline_layout: wgpu.PipelineLayout,
26	pipeline:        wgpu.RenderPipeline,
27
28	const_buffer: wgpu.Buffer,
29
30	tex_buffer:     wgpu.Buffer,
31	vertex_buffer:  wgpu.Buffer,
32	color_buffer:   wgpu.Buffer,
33	index_buffer:   wgpu.Buffer,
34
35	sampler: wgpu.Sampler,
36
37	bind_group_layout: wgpu.BindGroupLayout,
38	bind_group:        wgpu.BindGroup,
39
40	queue: wgpu.Queue,
41
42	curr_encoder: wgpu.CommandEncoder,
43	curr_pass:    wgpu.RenderPassEncoder,
44	curr_texture: wgpu.SurfaceTexture,
45	curr_view:    wgpu.TextureView,
46
47	tex_buf:   [BUFFER_SIZE * 8]f32,
48	vert_buf:  [BUFFER_SIZE * 8]f32,
49	color_buf: [BUFFER_SIZE * 16]u8,
50	index_buf: [BUFFER_SIZE * 6]u32,
51	prev_buf_idx: u32,
52	buf_idx:      u32,
53}
54
55r_init_and_run :: proc() {
56	r := &state.renderer
57
58	r.instance = wgpu.CreateInstance(nil)
59	r.surface = os_get_surface(r.instance)
60
61	wgpu.InstanceRequestAdapter(r.instance, &{ compatibleSurface = r.surface }, { callback = handle_request_adapter })
62}
63
64@(private="file")
65handle_request_adapter :: proc "c" (status: wgpu.RequestAdapterStatus, adapter: wgpu.Adapter, message: string, userdata1, userdata2: rawptr) {
66	context = state.ctx
67	if status != .Success || adapter == nil {
68		fmt.panicf("request adapter failure: [%v] %s", status, message)
69	}
70	state.renderer.adapter = adapter
71	wgpu.AdapterRequestDevice(adapter, nil, { callback = handle_request_device })
72}
73
74@(private="file")
75handle_request_device :: proc "c" (status: wgpu.RequestDeviceStatus, device: wgpu.Device, message: string, userdata1, userdata2: rawptr) {
76	context = state.ctx
77	if status != .Success || device == nil {
78		fmt.panicf("request device failure: [%v] %s", status, message)
79	}
80	state.renderer.device = device 
81	on_adapter_and_device()
82}
83
84@(private="file")
85on_adapter_and_device :: proc() {
86	r := &state.renderer
87
88	width, height := os_get_render_bounds()
89
90	r.config = wgpu.SurfaceConfiguration {
91		device      = r.device,
92		usage       = { .RenderAttachment },
93		format      = .BGRA8Unorm,
94		width       = width,
95		height      = height,
96		presentMode = .Fifo,
97		alphaMode   = .Opaque,
98	}
99
100	r.const_buffer = wgpu.DeviceCreateBuffer(r.device, &{
101		label = "Constant buffer",
102		usage = { .Uniform, .CopyDst },
103		size  = size_of(matrix[4, 4]f32),
104	})
105
106	r.atlas_texture = wgpu.DeviceCreateTexture(r.device, &{
107		usage = { .TextureBinding, .CopyDst },
108		dimension = ._2D,
109		size = { mu.DEFAULT_ATLAS_WIDTH, mu.DEFAULT_ATLAS_HEIGHT, 1 },
110		format = .R8Unorm,
111		mipLevelCount = 1,
112		sampleCount = 1,
113	})
114	r.atlas_texture_view = wgpu.TextureCreateView(r.atlas_texture, nil)
115
116	r.sampler = wgpu.DeviceCreateSampler(r.device, &{
117		addressModeU  = .ClampToEdge,
118		addressModeV  = .ClampToEdge,
119		addressModeW  = .ClampToEdge,
120		magFilter     = .Nearest,
121		minFilter     = .Nearest,
122		mipmapFilter  = .Nearest,
123		lodMinClamp   = 0,
124		lodMaxClamp   = 32,
125		compare       = .Undefined,
126		maxAnisotropy = 1,
127	})
128
129	r.vertex_buffer = wgpu.DeviceCreateBuffer(r.device, &{
130		label = "Vertex Buffer",
131		usage = { .Vertex, .CopyDst },
132		size  = size_of(r.vert_buf),
133	})
134
135	r.tex_buffer = wgpu.DeviceCreateBuffer(r.device, &{
136		label = "Texture Buffer",
137		usage = { .Vertex, .CopyDst },
138		size  = size_of(r.tex_buf),
139	})
140
141	r.color_buffer = wgpu.DeviceCreateBuffer(r.device, &{
142		label = "Color Buffer",
143		usage = { .Vertex, .CopyDst },
144		size  = size_of(r.color_buf),
145	})
146
147	r.index_buffer = wgpu.DeviceCreateBuffer(r.device, &{
148		label = "Index Buffer",
149		usage = { .Index, .CopyDst },
150		size  = size_of(r.index_buf),
151	})
152
153	r.bind_group_layout = wgpu.DeviceCreateBindGroupLayout(r.device, &{
154		entryCount = 3,
155		entries = raw_data([]wgpu.BindGroupLayoutEntry{
156			{
157				binding = 0,
158				visibility = { .Fragment },
159				sampler = {
160					type = .Filtering,
161				},
162			},
163			{
164				binding = 1,
165				visibility = { .Fragment },
166				texture = {
167					sampleType = .Float,
168					viewDimension = ._2D,
169					multisampled = false,
170				},
171			},
172			{
173				binding = 2,
174				visibility = { .Vertex },
175				buffer = {
176					type = .Uniform,
177					minBindingSize = size_of(matrix[4, 4]f32),
178				},
179			},
180		}),
181	})
182
183	r.bind_group = wgpu.DeviceCreateBindGroup(r.device, &{
184		layout = r.bind_group_layout,
185		entryCount = 3,
186		entries = raw_data([]wgpu.BindGroupEntry{
187			{
188				binding = 0,
189				sampler = r.sampler,
190			},
191			{
192				binding = 1,
193				textureView = r.atlas_texture_view,
194			},
195			{
196				binding = 2,
197				buffer = r.const_buffer,
198				size = size_of(matrix[4, 4]f32),
199			},
200		}),
201	})
202
203	r.module = wgpu.DeviceCreateShaderModule(r.device, &{
204		nextInChain = &wgpu.ShaderSourceWGSL{
205			sType = .ShaderSourceWGSL,
206			code  = #load("shader.wgsl"),
207		},
208	})
209
210	r.pipeline_layout = wgpu.DeviceCreatePipelineLayout(r.device, &{
211		bindGroupLayoutCount = 1,
212		bindGroupLayouts = &r.bind_group_layout,
213	})
214	r.pipeline = wgpu.DeviceCreateRenderPipeline(r.device, &{
215		layout = r.pipeline_layout,
216		vertex = {
217			module = r.module,
218			entryPoint = "vs_main",
219			bufferCount = 3,
220			buffers = raw_data([]wgpu.VertexBufferLayout{
221				{
222					stepMode = .Vertex,
223					arrayStride = 8,
224					attributeCount = 1,
225					attributes = &wgpu.VertexAttribute{
226						format = .Float32x2,
227						shaderLocation = 0,
228					},
229				},
230				{
231					stepMode = .Vertex,
232					arrayStride = 8,
233					attributeCount = 1,
234					attributes = &wgpu.VertexAttribute{
235						format = .Float32x2,
236						shaderLocation = 1,
237					},
238				},
239				{
240					stepMode = .Vertex,
241					arrayStride = 4,
242					attributeCount = 1,
243					attributes = &wgpu.VertexAttribute{
244						format = .Uint32,
245						shaderLocation = 2,
246					},
247				},
248			}),
249		},
250		fragment = &{
251			module = r.module,
252			entryPoint = "fs_main",
253			targetCount = 1,
254			targets = &wgpu.ColorTargetState{
255				format = .BGRA8Unorm,
256				blend = &{
257					alpha = {
258						srcFactor = .SrcAlpha,
259						dstFactor = .OneMinusSrcAlpha,
260						operation = .Add,
261					},
262					color = {
263						srcFactor = .SrcAlpha,
264						dstFactor = .OneMinusSrcAlpha,
265						operation = .Add,
266					},
267				},
268				writeMask = wgpu.ColorWriteMaskFlags_All,
269			},
270		},
271		primitive = {
272			topology  = .TriangleList,
273			cullMode  = .None,
274		},
275		multisample = {
276			count = 1,
277			mask = 0xFFFFFFFF,
278		},
279	})
280
281	r.queue = wgpu.DeviceGetQueue(r.device)
282
283	wgpu.QueueWriteTexture(
284		r.queue,
285		&{ texture = r.atlas_texture },
286		&mu.default_atlas_alpha,
287		mu.DEFAULT_ATLAS_WIDTH*mu.DEFAULT_ATLAS_HEIGHT,
288		&{
289			bytesPerRow  = mu.DEFAULT_ATLAS_WIDTH,
290			rowsPerImage = mu.DEFAULT_ATLAS_HEIGHT,
291		},
292		&{ mu.DEFAULT_ATLAS_WIDTH, mu.DEFAULT_ATLAS_HEIGHT, 1 },
293	)
294
295	r_write_consts()
296
297	wgpu.SurfaceConfigure(r.surface, &r.config)
298
299	os_run()
300}
301
302r_resize :: proc() {
303	r := &state.renderer
304
305	width, height := os_get_render_bounds()
306	r.config.width, r.config.height = width, height
307	wgpu.SurfaceConfigure(r.surface, &r.config)
308
309	r_write_consts()
310}
311
312r_write_consts :: proc() {
313	r := &state.renderer
314
315	// Transformation matrix to convert from screen to device pixels and scale based on DPI.
316	dpi := os_get_dpi()
317	width, height := os_get_render_bounds()
318	fw, fh := f32(width), f32(height)
319	transform := linalg.matrix_ortho3d(0, fw, fh, 0, -1, 1) * linalg.matrix4_scale(dpi)
320
321	wgpu.QueueWriteBuffer(r.queue, r.const_buffer, 0, &transform, size_of(transform))
322}
323
324r_clear :: proc(color: mu.Color) -> bool {
325	r := &state.renderer
326
327	r.buf_idx = 0
328	r.prev_buf_idx = 0
329
330	r.curr_texture = wgpu.SurfaceGetCurrentTexture(r.surface)
331	switch r.curr_texture.status {
332	case .SuccessOptimal, .SuccessSuboptimal:
333	// All good, could handle suboptimal here.
334	case .Timeout, .Outdated, .Lost:
335		if r.curr_texture.texture != nil {
336			wgpu.TextureRelease(r.curr_texture.texture)
337		}
338		r_resize()
339		return false
340	case .Occluded:
341		// Window is occluded (e.g. minimized), skip this frame.
342		return false
343	case .Error:
344		fmt.panicf("get_current_texture status=%v", r.curr_texture.status)
345	}
346
347	r.curr_view = wgpu.TextureCreateView(r.curr_texture.texture, nil)
348
349	r.curr_encoder = wgpu.DeviceCreateCommandEncoder(r.device, nil)
350
351	r.curr_pass = wgpu.CommandEncoderBeginRenderPass(r.curr_encoder, &{
352		colorAttachmentCount = 1,
353		colorAttachments = raw_data([]wgpu.RenderPassColorAttachment{
354			{
355				view = r.curr_view,
356				loadOp = .Clear,
357				storeOp = .Store,
358				clearValue = {f64(color.r)/255, f64(color.g)/255, f64(color.b)/255, f64(color.a)/255},
359				depthSlice = wgpu.DEPTH_SLICE_UNDEFINED,
360			},
361		}),
362	})
363
364	r_bind()
365
366	return true
367}
368
369r_bind :: proc() {
370	r := &state.renderer
371
372	wgpu.RenderPassEncoderSetPipeline(r.curr_pass, r.pipeline)
373	wgpu.RenderPassEncoderSetBindGroup(r.curr_pass, 0, r.bind_group)
374	wgpu.RenderPassEncoderSetVertexBuffer(r.curr_pass, 0, r.vertex_buffer, 0, size_of(r.vert_buf))
375	wgpu.RenderPassEncoderSetVertexBuffer(r.curr_pass, 1, r.tex_buffer,    0, size_of(r.tex_buf))
376	wgpu.RenderPassEncoderSetVertexBuffer(r.curr_pass, 2, r.color_buffer,  0, size_of(r.color_buf))
377	wgpu.RenderPassEncoderSetIndexBuffer(r.curr_pass, r.index_buffer, .Uint32, 0, size_of(r.index_buf))
378}
379
380r_flush :: proc() {
381	r := &state.renderer
382
383	if r.buf_idx == 0 || r.buf_idx == r.prev_buf_idx { return }
384
385	delta := uint(r.buf_idx - r.prev_buf_idx)
386	wgpu.RenderPassEncoderDrawIndexed(r.curr_pass, u32(delta * 6), 1, r.prev_buf_idx*6, 0, 0)
387
388	r.prev_buf_idx = r.buf_idx
389}
390
391r_full_flush :: proc() {
392	r := &state.renderer
393
394	r_submit()
395
396	r.buf_idx = 0
397	r.prev_buf_idx = 0
398
399	r.curr_encoder = wgpu.DeviceCreateCommandEncoder(r.device, nil)
400	r.curr_pass = wgpu.CommandEncoderBeginRenderPass(r.curr_encoder, &{
401		colorAttachmentCount = 1,
402		colorAttachments = &wgpu.RenderPassColorAttachment{
403			view = r.curr_view,
404			loadOp = .Load,
405			storeOp = .Store,
406		},
407	})
408
409	r_bind()
410}
411
412r_submit :: proc() {
413	r := &state.renderer
414
415	r_flush()
416
417	wgpu.QueueWriteBuffer(r.queue, r.vertex_buffer, 0, &r.vert_buf,  uint(r.buf_idx*8*size_of(f32)))
418	wgpu.QueueWriteBuffer(r.queue, r.tex_buffer,    0, &r.tex_buf,   uint(r.buf_idx*8*size_of(f32)))
419	wgpu.QueueWriteBuffer(r.queue, r.color_buffer,  0, &r.color_buf, uint(r.buf_idx*16))
420	wgpu.QueueWriteBuffer(r.queue, r.index_buffer,  0, &r.index_buf, uint(r.buf_idx*6*size_of(u32)))
421
422	wgpu.RenderPassEncoderEnd(r.curr_pass)
423	wgpu.RenderPassEncoderRelease(r.curr_pass)
424
425	command_buffer := wgpu.CommandEncoderFinish(r.curr_encoder, nil)
426	wgpu.QueueSubmit(r.queue, { command_buffer })
427
428	wgpu.CommandBufferRelease(command_buffer)
429	wgpu.CommandEncoderRelease(r.curr_encoder)
430}
431
432r_present :: proc() {
433	r := &state.renderer
434
435	r_submit()
436
437	wgpu.SurfacePresent(r.surface)
438
439	wgpu.TextureViewRelease(r.curr_view)
440	wgpu.TextureRelease(r.curr_texture.texture)
441}
442
443r_render :: proc() {
444	if !r_clear(state.bg) {
445		return
446	}
447
448	command_backing: ^mu.Command
449	for variant in mu.next_command_iterator(&state.mu_ctx, &command_backing) {
450		switch cmd in variant {
451		case ^mu.Command_Text: r_draw_text(cmd.str, cmd.pos, cmd.color)
452		case ^mu.Command_Rect: r_draw_rect(cmd.rect, cmd.color)
453		case ^mu.Command_Icon: r_draw_icon(cmd.id, cmd.rect, cmd.color)
454		case ^mu.Command_Clip: r_set_clip_rect(cmd.rect)
455		case ^mu.Command_Jump: unreachable() 
456		}
457	}
458
459	r_present()
460}
461
462push_quad :: proc(dst, src: mu.Rect, color: mu.Color) #no_bounds_check {
463	r := &state.renderer
464
465	if (r.buf_idx == BUFFER_SIZE) {
466		r_full_flush()
467	}
468
469	textvert_idx := r.buf_idx * 8
470	color_idx    := r.buf_idx * 16
471	element_idx  := u32(r.buf_idx * 4)
472	index_idx    := r.buf_idx * 6
473
474	r.buf_idx += 1
475
476	x := f32(src.x) / mu.DEFAULT_ATLAS_WIDTH
477	y := f32(src.y) / mu.DEFAULT_ATLAS_HEIGHT
478	w := f32(src.w) / mu.DEFAULT_ATLAS_WIDTH
479	h := f32(src.h) / mu.DEFAULT_ATLAS_HEIGHT
480	copy(r.tex_buf[textvert_idx:], []f32{
481		x,     y,
482		x + w, y,
483		x,     y + h,
484		x + w, y + h,
485	})
486
487	dx, dy, dw, dh := f32(dst.x), f32(dst.y), f32(dst.w), f32(dst.h)
488	copy(r.vert_buf[textvert_idx:], []f32{
489		dx,      dy,
490		dx + dw, dy,
491		dx,      dy + dh,
492		dx + dw, dy + dh,
493	})
494
495	color := color
496	intr.mem_copy_non_overlapping(raw_data(r.color_buf[color_idx + 0:]),  &color, 4)
497	intr.mem_copy_non_overlapping(raw_data(r.color_buf[color_idx + 4:]),  &color, 4)
498	intr.mem_copy_non_overlapping(raw_data(r.color_buf[color_idx + 8:]),  &color, 4)
499	intr.mem_copy_non_overlapping(raw_data(r.color_buf[color_idx + 12:]), &color, 4)
500
501	copy(r.index_buf[index_idx:], []u32{
502		element_idx + 0,
503		element_idx + 1,
504		element_idx + 2,
505		element_idx + 2,
506		element_idx + 3,
507		element_idx + 1,
508	})
509}
510
511r_draw_rect :: proc(rect: mu.Rect, color: mu.Color) {
512	push_quad(rect, mu.default_atlas[mu.DEFAULT_ATLAS_WHITE], color)
513}
514
515r_draw_text :: proc(text: string, pos: mu.Vec2, color: mu.Color) {
516	dst := mu.Rect{ pos.x, pos.y, 0, 0 }
517	for ch in text {
518		if ch&0xc0 != 0x80 {
519			r := min(int(ch), 127)
520			src := mu.default_atlas[mu.DEFAULT_ATLAS_FONT + r]
521			dst.w = src.w
522			dst.h = src.h
523			push_quad(dst, src, color)
524			dst.x += dst.w
525		}
526	}
527}
528
529r_draw_icon :: proc(id: mu.Icon, rect: mu.Rect, color: mu.Color) {
530	src := mu.default_atlas[id]
531	x := rect.x + (rect.w - src.w) / 2
532	y := rect.y + (rect.h - src.h) / 2
533	push_quad({x, y, src.w, src.h}, src, color)
534}
535
536r_set_clip_rect :: proc(rect: mu.Rect) {
537	r := &state.renderer
538	r_flush()
539
540	x := min(u32(rect.x), r.config.width)
541	y := min(u32(rect.y), r.config.height)
542	w := min(u32(rect.w), r.config.width-x)
543	h := min(u32(rect.h), r.config.height-y)
544	wgpu.RenderPassEncoderSetScissorRect(r.curr_pass, x, y, w, h)
545}

Shaders

shader.wgsl ¶
34 linesSource

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) texCoord: vec2<f32>,
    @location(1) @interpolate(flat) color: u32,
};

@vertex 
fn vs_main(
@location(0) pos: vec2<f32>,
    @location(1) texCoord: vec2<f32>,
    @location(2) color: u32
) -> VertexOutput {
    var output: VertexOutput;
    output.position = transform * vec4<f32>(pos, 0, 1);
    output.texCoord = texCoord;
    output.color    = color;
    return output;
}

@group(0) @binding(0) var samp: sampler;
@group(0) @binding(1) var text: texture_2d<f32>;
@group(0) @binding(2) var<uniform> transform: mat4x4<f32>;

@fragment 
fn fs_main(@location(0) texCoord: vec2<f32>, @location(1) @interpolate(flat) color: u32) -> @location(0) vec4<f32> {
    // NOTE: this samples rgba, but the texture just contains the alpha channel,
    // so in practice `r` is the alpha, and the rest is junk.
    let texColor = textureSample(text, samp, texCoord);
    let a = texColor.r * f32((color >> 24) & 0xffu) / 255;
    let b = f32((color >> 16) & 0xffu) / 255;
    let g = f32((color >> 8) & 0xffu) / 255;
    let r = f32(color & 0xffu) / 255;
    return vec4<f32>(r, g, b, a);
}

To Build and Run 3

Declarations Used 195