ogl_beamforming

Ultrasound Beamforming Implemented with OpenGL
git clone anongit@rnpnr.xyz:ogl_beamforming.git
Log | Files | Refs | Feed | Submodules | LICENSE

ui.c (29881B)


      1 /* See LICENSE for license details. */
      2 static Color
      3 colour_from_normalized(v4 rgba)
      4 {
      5 	return (Color){.r = rgba.r * 255.0f, .g = rgba.g * 255.0f,
      6 	               .b = rgba.b * 255.0f, .a = rgba.a * 255.0f};
      7 }
      8 
      9 static Color
     10 fade(Color a, f32 alpha)
     11 {
     12 	a.a = (u8)((f32)a.a * alpha);
     13 	return a;
     14 }
     15 
     16 static f32
     17 lerp(f32 a, f32 b, f32 t)
     18 {
     19 	return a + t * (b - a);
     20 }
     21 
     22 static v4
     23 lerp_v4(v4 a, v4 b, f32 t)
     24 {
     25 	return (v4){
     26 		.x = a.x + t * (b.x - a.x),
     27 		.y = a.y + t * (b.y - a.y),
     28 		.z = a.z + t * (b.z - a.z),
     29 		.w = a.w + t * (b.w - a.w),
     30 	};
     31 }
     32 
     33 static v2
     34 measure_text(Font font, s8 text)
     35 {
     36 	v2 result = {.y = font.baseSize};
     37 	for (size i = 0; i < text.len; i++) {
     38 		/* NOTE: assumes font glyphs are ordered ASCII */
     39 		i32 idx   = (i32)text.data[i] - 0x20;
     40 		result.x += font.glyphs[idx].advanceX;
     41 		if (font.glyphs[idx].advanceX == 0)
     42 			result.x += (font.recs[idx].width + font.glyphs[idx].offsetX);
     43 	}
     44 	return result;
     45 }
     46 
     47 static v2
     48 draw_text(Font font, s8 text, v2 pos, f32 rotation, Color colour)
     49 {
     50 	rlPushMatrix();
     51 
     52 	rlTranslatef(pos.x, pos.y, 0);
     53 	rlRotatef(rotation, 0, 0, 1);
     54 
     55 	v2 off = {0};
     56 	for (size i = 0; i < text.len; i++) {
     57 		/* NOTE: assumes font glyphs are ordered ASCII */
     58 		i32 idx = text.data[i] - 0x20;
     59 		Rectangle dst = {
     60 			off.x + font.glyphs[idx].offsetX - font.glyphPadding,
     61 			off.y + font.glyphs[idx].offsetY - font.glyphPadding,
     62 			font.recs[idx].width  + 2.0f * font.glyphPadding,
     63 			font.recs[idx].height + 2.0f * font.glyphPadding
     64 		};
     65 		Rectangle src = {
     66 			font.recs[idx].x - font.glyphPadding,
     67 			font.recs[idx].y - font.glyphPadding,
     68 			font.recs[idx].width  + 2.0f * font.glyphPadding,
     69 			font.recs[idx].height + 2.0f * font.glyphPadding
     70 		};
     71 		DrawTexturePro(font.texture, src, dst, (Vector2){0}, 0, colour);
     72 
     73 		off.x += font.glyphs[idx].advanceX;
     74 		if (font.glyphs[idx].advanceX == 0)
     75 			off.x += font.recs[idx].width;
     76 	}
     77 	rlPopMatrix();
     78 	v2 result = {.x = off.x, .y = font.baseSize};
     79 	return result;
     80 }
     81 
     82 static Rect
     83 scale_rect_centered(Rect r, v2 scale)
     84 {
     85 	Rect or   = r;
     86 	r.size.w *= scale.x;
     87 	r.size.h *= scale.y;
     88 	r.pos.x  += (or.size.w - r.size.w) / 2;
     89 	r.pos.y  += (or.size.h - r.size.h) / 2;
     90 	return r;
     91 }
     92 
     93 static v2
     94 center_align_text_in_rect(Rect r, s8 text, Font font)
     95 {
     96 	v2 ts    = measure_text(font, text);
     97 	v2 delta = { .w = r.size.w - ts.w, .h = r.size.h - ts.h };
     98 	return (v2) {
     99 		.x = r.pos.x + 0.5 * delta.w,
    100 	        .y = r.pos.y + 0.5 * delta.h,
    101 	};
    102 }
    103 
    104 static b32
    105 hover_text(v2 mouse, Rect text_rect, f32 *hover_t, b32 can_advance)
    106 {
    107 	b32 hovering = CheckCollisionPointRec(mouse.rl, text_rect.rl);
    108 	if (hovering && can_advance) *hover_t += TEXT_HOVER_SPEED * dt_for_frame;
    109 	else                         *hover_t -= TEXT_HOVER_SPEED * dt_for_frame;
    110 	*hover_t = CLAMP01(*hover_t);
    111 	return hovering;
    112 }
    113 
    114 /* NOTE: This is kinda sucks no matter how you spin it. If we want the values to be
    115  * left aligned in the center column we need to know the longest prefix length but
    116  * without either hardcoding one of the prefixes as the longest one or measuring all
    117  * of them we can't know this ahead of time. For now we hardcode this and manually
    118  * adjust when needed */
    119 #define LISTING_LEFT_COLUMN_WIDTH 270.0f
    120 #define LISTING_LINE_PAD           6.0f
    121 
    122 static Rect
    123 do_value_listing(s8 prefix, s8 suffix, Arena a, f32 value, Font font, Rect r)
    124 {
    125 	v2 suffix_s = measure_text(font, suffix);
    126 	v2 suffix_p = {.x = r.pos.x + r.size.w - suffix_s.w, .y = r.pos.y};
    127 
    128 	Stream buf = arena_stream(&a);
    129 	stream_append_f64(&buf, value, 100);
    130 	v2 txt_p = {.x = r.pos.x + LISTING_LEFT_COLUMN_WIDTH, .y = r.pos.y};
    131 
    132 	draw_text(font, prefix,             r.pos,    0, colour_from_normalized(FG_COLOUR));
    133 	draw_text(font, stream_to_s8(&buf), txt_p,    0, colour_from_normalized(FG_COLOUR));
    134 	draw_text(font, suffix,             suffix_p, 0, colour_from_normalized(FG_COLOUR));
    135 	r.pos.y  += suffix_s.h + LISTING_LINE_PAD;
    136 	r.size.y -= suffix_s.h + LISTING_LINE_PAD;
    137 
    138 	return r;
    139 }
    140 
    141 static Rect
    142 do_text_input_listing(s8 prefix, s8 suffix, Variable var, BeamformerUI *ui, Rect r,
    143                       v2 mouse, f32 *hover_t)
    144 {
    145 	InputState   *is = &ui->text_input_state;
    146 	b32 text_input_active = (ui->interaction.state == IS_TEXT) &&
    147 	                        (var.store == ui->interaction.active.store);
    148 
    149 	Arena  arena = ui->arena_for_frame;
    150 	Stream buf   = arena_stream(&arena);
    151 	v2 txt_s;
    152 
    153 	if (text_input_active) {
    154 		txt_s = measure_text(ui->font, (s8){.len = is->buf_len, .data = is->buf});
    155 	} else {
    156 		stream_append_variable(&buf, &var);
    157 		txt_s = measure_text(ui->font, stream_to_s8(&buf));
    158 	}
    159 
    160 	Rect edit_rect = {
    161 		.pos  = {.x = r.pos.x + LISTING_LEFT_COLUMN_WIDTH, .y = r.pos.y},
    162 		.size = {.x = txt_s.w + TEXT_BOX_EXTRA_X, .y = txt_s.h}
    163 	};
    164 
    165 	b32 hovering = hover_text(mouse, edit_rect, hover_t, !text_input_active);
    166 	if (hovering)
    167 		ui->interaction.hot = var;
    168 
    169 	/* TODO: where should this go? */
    170 	if (text_input_active && is->cursor == -1) {
    171 		/* NOTE: extra offset to help with putting a cursor at idx 0 */
    172 		#define TEXT_HALF_CHAR_WIDTH 10
    173 		f32 hover_p = CLAMP01((mouse.x - edit_rect.pos.x) / edit_rect.size.w);
    174 		f32 x_off = TEXT_HALF_CHAR_WIDTH, x_bounds = edit_rect.size.w * hover_p;
    175 		i32 i;
    176 		for (i = 0; i < is->buf_len && x_off < x_bounds; i++) {
    177 			/* NOTE: assumes font glyphs are ordered ASCII */
    178 			i32 idx  = is->buf[i] - 0x20;
    179 			x_off   += ui->font.glyphs[idx].advanceX;
    180 			if (ui->font.glyphs[idx].advanceX == 0)
    181 				x_off += ui->font.recs[idx].width;
    182 		}
    183 		is->cursor = i;
    184 	}
    185 
    186 	Color colour = colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, *hover_t));
    187 
    188 	if (text_input_active) {
    189 		s8 buf = {.len = is->buf_len, .data = is->buf};
    190 		v2 ts  = measure_text(ui->font, buf);
    191 		v2 pos = {.x = edit_rect.pos.x, .y = edit_rect.pos.y + (edit_rect.size.y - ts.y) / 2};
    192 
    193 		#define MAX_DISP_CHARS 7
    194 		i32 buf_delta = is->buf_len - MAX_DISP_CHARS;
    195 		if (buf_delta < 0) buf_delta = 0;
    196 		buf.len  -= buf_delta;
    197 		buf.data += buf_delta;
    198 		{
    199 			/* NOTE: drop a char if the subtext still doesn't fit */
    200 			v2 nts = measure_text(ui->font, buf);
    201 			if (nts.w > 0.96 * edit_rect.size.w) {
    202 				buf.data++;
    203 				buf.len--;
    204 			}
    205 		}
    206 		draw_text(ui->font, buf, pos, 0, colour);
    207 
    208 		v4 bg = FOCUSED_COLOUR;
    209 		bg.a  = 0;
    210 		Color cursor_colour = colour_from_normalized(lerp_v4(bg, FOCUSED_COLOUR,
    211 		                                                     CLAMP01(is->cursor_blink_t)));
    212 		buf.len = is->cursor - buf_delta;
    213 		v2 sts = measure_text(ui->font, buf);
    214 		f32 cursor_x = pos.x + sts.x;
    215 		f32 cursor_width;
    216 		if (is->cursor == is->buf_len) cursor_width = 20;
    217 		else                           cursor_width = 4;
    218 		Rect cursor_r = {
    219 			.pos  = {.x = cursor_x,     .y = pos.y},
    220 			.size = {.w = cursor_width, .h = ts.h},
    221 		};
    222 
    223 		DrawRectanglePro(cursor_r.rl, (Vector2){0}, 0, cursor_colour);
    224 	} else {
    225 		draw_text(ui->font, stream_to_s8(&buf), edit_rect.pos, 0, colour);
    226 	}
    227 
    228 	v2 suffix_s = measure_text(ui->font, suffix);
    229 	v2 suffix_p = {.x = r.pos.x + r.size.w - suffix_s.w, .y = r.pos.y};
    230 	draw_text(ui->font, prefix, r.pos,    0, colour_from_normalized(FG_COLOUR));
    231 	draw_text(ui->font, suffix, suffix_p, 0, colour_from_normalized(FG_COLOUR));
    232 
    233 	r.pos.y  += suffix_s.h + LISTING_LINE_PAD;
    234 	r.size.y -= suffix_s.h + LISTING_LINE_PAD;
    235 
    236 	return r;
    237 }
    238 
    239 static Rect
    240 do_text_toggle_listing(s8 prefix, s8 text0, s8 text1, Variable var,
    241                        BeamformerUI *ui, Rect r, v2 mouse, f32 *hover_t)
    242 {
    243 	b32 toggle = *(b32 *)var.store;
    244 	v2 txt_s;
    245 	if (toggle) txt_s = measure_text(ui->font, text1);
    246 	else        txt_s = measure_text(ui->font, text0);
    247 
    248 	Rect edit_rect = {
    249 		.pos  = {.x = r.pos.x + LISTING_LEFT_COLUMN_WIDTH, .y = r.pos.y},
    250 		.size = {.x = txt_s.w + TEXT_BOX_EXTRA_X, .y = txt_s.h}
    251 	};
    252 
    253 	if (hover_text(mouse, edit_rect, hover_t, 1))
    254 		ui->interaction.hot = var;
    255 
    256 	Color colour = colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, *hover_t));
    257 	draw_text(ui->font, prefix, r.pos, 0, colour_from_normalized(FG_COLOUR));
    258 	draw_text(ui->font, toggle? text1: text0, edit_rect.pos, 0, colour);
    259 
    260 	r.pos.y  += txt_s.h + LISTING_LINE_PAD;
    261 	r.size.y -= txt_s.h + LISTING_LINE_PAD;
    262 
    263 	return r;
    264 }
    265 
    266 static b32
    267 do_text_button(BeamformerUI *ui, s8 text, Rect r, v2 mouse, f32 *hover_t)
    268 {
    269 	b32 hovering = hover_text(mouse, r, hover_t, 1);
    270 	b32 pressed  = 0;
    271 	pressed     |= (hovering && IsMouseButtonPressed(MOUSE_BUTTON_LEFT));
    272 	pressed     |= (hovering && IsMouseButtonPressed(MOUSE_BUTTON_RIGHT));
    273 
    274 	f32 param  = lerp(1, 1.04, *hover_t);
    275 	v2  bscale = (v2){
    276 		.x = param + RECT_BTN_BORDER_WIDTH / r.size.w,
    277 		.y = param + RECT_BTN_BORDER_WIDTH / r.size.h,
    278 	};
    279 	Rect sr    = scale_rect_centered(r, (v2){.x = param, .y = param});
    280 	Rect sb    = scale_rect_centered(r, bscale);
    281 	DrawRectangleRounded(sb.rl, RECT_BTN_ROUNDNESS, 0, RECT_BTN_BORDER_COLOUR);
    282 	DrawRectangleRounded(sr.rl, RECT_BTN_ROUNDNESS, 0, RECT_BTN_COLOUR);
    283 
    284 	v2 tpos   = center_align_text_in_rect(r, text, ui->font);
    285 	v2 spos   = {.x = tpos.x + 1.75, .y = tpos.y + 2};
    286 	v4 colour = lerp_v4(FG_COLOUR, HOVERED_COLOUR, *hover_t);
    287 
    288 	draw_text(ui->font, text, spos, 0, fade(BLACK, 0.8));
    289 	draw_text(ui->font, text, tpos, 0, colour_from_normalized(colour));
    290 
    291 	return pressed;
    292 }
    293 
    294 static void
    295 draw_settings_ui(BeamformerCtx *ctx, Rect r, v2 mouse)
    296 {
    297 	BeamformerUI *ui         = ctx->ui;
    298 	BeamformerParameters *bp = &ctx->params->raw;
    299 
    300 	f32 minx = bp->output_min_coordinate.x + 1e-6, maxx = bp->output_max_coordinate.x - 1e-6;
    301 	f32 minz = bp->output_min_coordinate.z + 1e-6, maxz = bp->output_max_coordinate.z - 1e-6;
    302 
    303 	Rect draw_r    = r;
    304 	draw_r.pos.y  += 20;
    305 	draw_r.pos.x  += 20;
    306 	draw_r.size.x -= 20;
    307 	draw_r.size.y -= 20;
    308 
    309 	draw_r = do_value_listing(s8("Sampling Frequency:"), s8("[MHz]"), ui->arena_for_frame,
    310 	                          bp->sampling_frequency * 1e-6, ui->font, draw_r);
    311 
    312 	static f32 hover_t[15];
    313 	i32 idx = 0;
    314 
    315 	Variable var;
    316 
    317 	var.store         = &bp->center_frequency;
    318 	var.type          = VT_F32;
    319 	var.f32_limits    = (v2){.y = 100e6};
    320 	var.flags         = V_CAUSES_COMPUTE;
    321 	var.display_scale = 1e-6;
    322 	var.scroll_scale  = 1e5;
    323 	draw_r = do_text_input_listing(s8("Center Frequency:"), s8("[MHz]"), var, ui, draw_r,
    324 	                               mouse, hover_t + idx++);
    325 
    326 	var.store         = &bp->speed_of_sound;
    327 	var.type          = VT_F32;
    328 	var.f32_limits    = (v2){.y = 1e6};
    329 	var.flags         = V_CAUSES_COMPUTE;
    330 	var.display_scale = 1;
    331 	var.scroll_scale  = 10;
    332 	draw_r = do_text_input_listing(s8("Speed of Sound:"), s8("[m/s]"), var, ui, draw_r,
    333 	                               mouse, hover_t + idx++);
    334 
    335 	var.store         = &bp->output_min_coordinate.x;
    336 	var.type          = VT_F32;
    337 	var.f32_limits    = (v2){.x = -1e3, .y = maxx};
    338 	var.flags         = V_CAUSES_COMPUTE;
    339 	var.display_scale = 1e3;
    340 	var.scroll_scale  = 0.5e-3;
    341 	draw_r = do_text_input_listing(s8("Min Lateral Point:"), s8("[mm]"), var, ui, draw_r,
    342 	                               mouse, hover_t + idx++);
    343 
    344 	var.store         = &bp->output_max_coordinate.x;
    345 	var.type          = VT_F32;
    346 	var.f32_limits    = (v2){.x = minx, .y = 1e3};
    347 	var.flags         = V_CAUSES_COMPUTE;
    348 	var.display_scale = 1e3;
    349 	var.scroll_scale  = 0.5e-3;
    350 	draw_r = do_text_input_listing(s8("Max Lateral Point:"), s8("[mm]"), var, ui, draw_r,
    351 	                               mouse, hover_t + idx++);
    352 
    353 	var.store         = &bp->output_min_coordinate.z;
    354 	var.type          = VT_F32;
    355 	var.f32_limits    = (v2){.y = maxz};
    356 	var.flags         = V_CAUSES_COMPUTE;
    357 	var.display_scale = 1e3;
    358 	var.scroll_scale  = 0.5e-3;
    359 	draw_r = do_text_input_listing(s8("Min Axial Point:"), s8("[mm]"), var, ui, draw_r,
    360 	                               mouse, hover_t + idx++);
    361 
    362 	var.store         = &bp->output_max_coordinate.z;
    363 	var.type          = VT_F32;
    364 	var.f32_limits    = (v2){.x = minz, .y = 1e3};
    365 	var.flags         = V_CAUSES_COMPUTE;
    366 	var.display_scale = 1e3;
    367 	var.scroll_scale  = 0.5e-3;
    368 	draw_r = do_text_input_listing(s8("Max Axial Point:"), s8("[mm]"), var, ui, draw_r,
    369 	                               mouse, hover_t + idx++);
    370 
    371 	var.store         = &bp->off_axis_pos;
    372 	var.type          = VT_F32;
    373 	var.f32_limits    = (v2){.x = minx, .y = maxx};
    374 	var.flags         = V_CAUSES_COMPUTE;
    375 	var.display_scale = 1e3;
    376 	var.scroll_scale  = 0.5e-3;
    377 	draw_r = do_text_input_listing(s8("Off Axis Position:"), s8("[mm]"), var, ui, draw_r,
    378 	                               mouse, hover_t + idx++);
    379 
    380 	var       = (Variable){0};
    381 	var.store = &bp->beamform_plane;
    382 	var.type  = VT_B32;
    383 	var.flags = V_CAUSES_COMPUTE;
    384 	draw_r = do_text_toggle_listing(s8("Beamform Plane:"), s8("XZ"), s8("YZ"), var, ui,
    385 	                                draw_r, mouse, hover_t + idx++);
    386 
    387 	var.store         = &bp->f_number;
    388 	var.type          = VT_F32;
    389 	var.f32_limits    = (v2){.y = 1e3};
    390 	var.flags         = V_CAUSES_COMPUTE;
    391 	var.display_scale = 1;
    392 	var.scroll_scale  = 0.1;
    393 	draw_r = do_text_input_listing(s8("F#:"), s8(""), var, ui, draw_r, mouse, hover_t + idx++);
    394 
    395 	var.store         = &ctx->fsctx.db;
    396 	var.type          = VT_F32;
    397 	var.f32_limits    = (v2){.x = -120};
    398 	var.flags         = V_GEN_MIPMAPS;
    399 	var.display_scale = 1;
    400 	var.scroll_scale  = 1;
    401 	draw_r = do_text_input_listing(s8("Dynamic Range:"), s8("[dB]"), var, ui, draw_r,
    402 	                               mouse, hover_t + idx++);
    403 
    404 	var.store         = &ctx->fsctx.threshold;
    405 	var.type          = VT_F32;
    406 	var.f32_limits    = (v2){.y = 240};
    407 	var.flags         = V_GEN_MIPMAPS;
    408 	var.display_scale = 1;
    409 	var.scroll_scale  = 1;
    410 	draw_r = do_text_input_listing(s8("Threshold:"), s8(""), var, ui, draw_r,
    411 	                               mouse, hover_t + idx++);
    412 
    413 	draw_r.pos.y  += 2 * LISTING_LINE_PAD;
    414 	draw_r.size.y -= 2 * LISTING_LINE_PAD;
    415 
    416 	#if 0
    417 	/* TODO: work this into the work queue */
    418 	bmv = (BPModifiableValue){&ctx->partial_compute_ctx.volume_dim.x, bmv_store_power_of_two,
    419 	                          .ilimits = (iv2){.x = 1, .y = ctx->gl.max_3d_texture_dim},
    420 	                          MV_INT, 1, 1};
    421 	draw_r = do_text_input_listing(s8("Export Dimension X:"), s8(""), bmv, ctx, arena,
    422 	                               draw_r, mouse, hover_t + idx++);
    423 
    424 	bmv = (BPModifiableValue){&ctx->partial_compute_ctx.volume_dim.y, bmv_store_power_of_two,
    425 	                          .ilimits = (iv2){.x = 1, .y = ctx->gl.max_3d_texture_dim},
    426 	                          MV_INT, 1, 1};
    427 	draw_r = do_text_input_listing(s8("Export Dimension Y:"), s8(""), bmv, ctx, arena,
    428 	                               draw_r, mouse, hover_t + idx++);
    429 
    430 	bmv = (BPModifiableValue){&ctx->partial_compute_ctx.volume_dim.z, bmv_store_power_of_two,
    431 	                          .ilimits = (iv2){.x = 1, .y = ctx->gl.max_3d_texture_dim},
    432 	                          MV_INT, 1, 1};
    433 	draw_r = do_text_input_listing(s8("Export Dimension Z:"), s8(""), bmv, ctx, arena,
    434 	                               draw_r, mouse, hover_t + idx++);
    435 
    436 	Rect btn_r = draw_r;
    437 	btn_r.size.h  = ctx->font.baseSize * 1.3;
    438 	btn_r.size.w *= 0.6;
    439 	if (do_text_button(ctx, s8("Dump Raw Volume"), btn_r, mouse, hover_t + idx++)) {
    440 		if (!ctx->partial_compute_ctx.state) {
    441 		}
    442 	}
    443 	#endif
    444 
    445 	/* NOTE: if C compilers didn't suck this would be a static assert */
    446 	ASSERT(idx <= ARRAY_COUNT(hover_t));
    447 }
    448 
    449 static void
    450 draw_debug_overlay(BeamformerCtx *ctx, Arena arena, Rect r)
    451 {
    452 	static s8 labels[CS_LAST] = {
    453 		#define X(e, n, s, h, pn) [CS_##e] = s8(pn ":"),
    454 		COMPUTE_SHADERS
    455 		#undef X
    456 	};
    457 
    458 	BeamformerUI *ui     = ctx->ui;
    459 	ComputeShaderCtx *cs = &ctx->csctx;
    460 	uv2 ws = ctx->window_size;
    461 
    462 	Stream buf = stream_alloc(&arena, 64);
    463 	v2 pos     = {.x = 20, .y = ws.h - 10};
    464 
    465 	f32 compute_time_sum = 0;
    466 	u32 stages = ctx->params->compute_stages_count;
    467 	for (u32 i = 0; i < stages; i++) {
    468 		u32 index  = ctx->params->compute_stages[i];
    469 		pos.y     -= measure_text(ui->font, labels[index]).y;
    470 		draw_text(ui->font, labels[index], pos, 0, colour_from_normalized(FG_COLOUR));
    471 
    472 		buf.widx = 0;
    473 		stream_append_f64_e(&buf, cs->last_frame_time[index]);
    474 		stream_append_s8(&buf, s8(" [s]"));
    475 		v2 txt_fs = measure_text(ui->font, stream_to_s8(&buf));
    476 		v2 rpos   = {.x = r.pos.x + r.size.w - txt_fs.w, .y = pos.y};
    477 		draw_text(ui->font, stream_to_s8(&buf), rpos, 0, colour_from_normalized(FG_COLOUR));
    478 
    479 		compute_time_sum += cs->last_frame_time[index];
    480 	}
    481 
    482 	static s8 totals[2] = {s8("Compute Total:"), s8("Volume Total:")};
    483 	f32 times[2]        = {compute_time_sum, ctx->partial_compute_ctx.runtime};
    484 	for (u32 i = 0; i < ARRAY_COUNT(totals); i++) {
    485 		pos.y    -= measure_text(ui->font, totals[i]).y;
    486 		draw_text(ui->font, totals[i], pos, 0, colour_from_normalized(FG_COLOUR));
    487 
    488 		buf.widx = 0;
    489 		stream_append_f64_e(&buf, times[i]);
    490 		stream_append_s8(&buf, s8(" [s]"));
    491 		v2 txt_fs = measure_text(ui->font, stream_to_s8(&buf));
    492 		v2 rpos   = {.x = r.pos.x + r.size.w - txt_fs.w, .y = pos.y};
    493 		draw_text(ui->font, stream_to_s8(&buf), rpos, 0, colour_from_normalized(FG_COLOUR));
    494 	}
    495 
    496 	{
    497 		static v2 pos       = {.x = 32,  .y = 128};
    498 		static v2 scale     = {.x = 1.0, .y = 1.0};
    499 		static u32 txt_idx  = 0;
    500 		static s8 txt[2]    = { s8("-_-"), s8("^_^") };
    501 		static v2 ts[2];
    502 		if (ts[0].x == 0) {
    503 			ts[0] = measure_text(ui->font, txt[0]);
    504 			ts[1] = measure_text(ui->font, txt[1]);
    505 		}
    506 
    507 		pos.x += 130 * dt_for_frame * scale.x;
    508 		pos.y += 120 * dt_for_frame * scale.y;
    509 
    510 		if (pos.x > (ws.w - ts[txt_idx].x) || pos.x < 0) {
    511 			txt_idx  = !txt_idx;
    512 			pos.x    = CLAMP(pos.x, 0, ws.w - ts[txt_idx].x);
    513 			scale.x *= -1.0;
    514 		}
    515 
    516 		if (pos.y > (ws.h - ts[txt_idx].y) || pos.y < 0) {
    517 			txt_idx  = !txt_idx;
    518 			pos.y    = CLAMP(pos.y, 0, ws.h - ts[txt_idx].y);
    519 			scale.y *= -1.0;
    520 		}
    521 
    522 		draw_text(ui->font, txt[txt_idx], pos, 0, RED);
    523 	}
    524 }
    525 
    526 static void
    527 ui_store_variable(Variable *var, void *new_value)
    528 {
    529 	/* TODO: special cases (eg. power of 2) */
    530 	switch (var->type) {
    531 	case VT_F32: {
    532 		f32  f32_val = *(f32 *)new_value;
    533 		f32 *f32_var = var->store;
    534 		*f32_var     = CLAMP(f32_val, var->f32_limits.x, var->f32_limits.y);
    535 	} break;
    536 	case VT_I32: {
    537 		i32  i32_val = *(i32 *)new_value;
    538 		i32 *i32_var = var->store;
    539 		*i32_var     = CLAMP(i32_val, var->i32_limits.x, var->i32_limits.y);
    540 	} break;
    541 	default: INVALID_CODE_PATH;
    542 	}
    543 }
    544 
    545 static void
    546 begin_text_input(InputState *is, Variable *var)
    547 {
    548 	ASSERT(var->store != NULL);
    549 
    550 	Stream s = {.cap = ARRAY_COUNT(is->buf), .data = is->buf};
    551 	stream_append_variable(&s, var);
    552 	ASSERT(!s.errors);
    553 	is->buf_len = s.widx;
    554 	is->cursor  = -1;
    555 }
    556 
    557 static void
    558 end_text_input(InputState *is, Variable *var)
    559 {
    560 	f32 value = parse_f64((s8){.len = is->buf_len, .data = is->buf}) / var->display_scale;
    561 	ui_store_variable(var, &value);
    562 }
    563 
    564 static void
    565 update_text_input(InputState *is)
    566 {
    567 	if (is->cursor == -1)
    568 		return;
    569 
    570 	is->cursor_blink_t += is->cursor_blink_scale * dt_for_frame;
    571 	if (is->cursor_blink_t >= 1) is->cursor_blink_scale = -1.5f;
    572 	if (is->cursor_blink_t <= 0) is->cursor_blink_scale =  1.5f;
    573 
    574 	/* NOTE: handle multiple input keys on a single frame */
    575 	i32 key = GetCharPressed();
    576 	while (key > 0) {
    577 		if (is->buf_len == ARRAY_COUNT(is->buf))
    578 			break;
    579 
    580 		b32 allow_key = ((key >= '0' && key <= '9') || (key == '.') ||
    581 		                 (key == '-' && is->cursor == 0));
    582 		if (allow_key) {
    583 			mem_move(is->buf + is->cursor,
    584 			         is->buf + is->cursor + 1,
    585 			         is->buf_len - is->cursor + 1);
    586 
    587 			is->buf[is->cursor++] = key;
    588 			is->buf_len++;
    589 		}
    590 		key = GetCharPressed();
    591 	}
    592 
    593 	if ((IsKeyPressed(KEY_LEFT) || IsKeyPressedRepeat(KEY_LEFT)) && is->cursor > 0)
    594 		is->cursor--;
    595 
    596 	if ((IsKeyPressed(KEY_RIGHT) || IsKeyPressedRepeat(KEY_RIGHT)) && is->cursor < is->buf_len)
    597 		is->cursor++;
    598 
    599 	if ((IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE)) && is->cursor > 0) {
    600 		is->cursor--;
    601 		mem_move(is->buf + is->cursor + 1,
    602 		         is->buf + is->cursor,
    603 		         is->buf_len - is->cursor);
    604 		is->buf_len--;
    605 	}
    606 
    607 	if ((IsKeyPressed(KEY_DELETE) || IsKeyPressedRepeat(KEY_DELETE)) && is->cursor < is->buf_len) {
    608 		mem_move(is->buf + is->cursor + 1,
    609 		         is->buf + is->cursor,
    610 		         is->buf_len - is->cursor);
    611 		is->buf_len--;
    612 	}
    613 }
    614 
    615 static b32
    616 ui_can_start_compute(BeamformerCtx *ctx)
    617 {
    618 	BeamformFrame *displayed = ctx->beamform_frames + ctx->displayed_frame_index;
    619 	b32 result  = ctx->beamform_work_queue.compute_in_flight == 0;
    620 	result     &= (displayed->dim.x != 0 || displayed->dim.y != 0);
    621 	result     &= displayed->dim.z != 0;
    622 	return result;
    623 }
    624 
    625 static void
    626 ui_start_compute(BeamformerCtx *ctx)
    627 {
    628 	/* NOTE: we do not allow ui to start a work if no work was previously completed */
    629 	Arena a = {0};
    630 	if (ui_can_start_compute(ctx)) {
    631 		beamform_work_queue_push(ctx, &a, BW_RECOMPUTE);
    632 		BeamformFrameIterator bfi = beamform_frame_iterator(ctx);
    633 		for (BeamformFrame *frame = frame_next(&bfi); frame; frame = frame_next(&bfi)) {
    634 			if (frame->dim.w && frame->textures[frame->dim.w - 1])
    635 				glClearTexImage(frame->textures[frame->dim.w - 1], 0,
    636 				                GL_RED, GL_FLOAT, 0);
    637 		}
    638 	}
    639 	ctx->params->upload = 1;
    640 }
    641 
    642 static void
    643 ui_gen_mipmaps(BeamformerCtx *ctx)
    644 {
    645 	if (ctx->fsctx.output.texture.id)
    646 		ctx->flags |= GEN_MIPMAPS;
    647 }
    648 
    649 static void
    650 ui_begin_interact(BeamformerUI *ui, BeamformerInput *input, b32 scroll)
    651 {
    652 	InteractionState *is = &ui->interaction;
    653 	if (is->hot_state != IS_NONE) {
    654 		is->state = is->hot_state;
    655 	} else {
    656 		switch (is->hot.type) {
    657 		case VT_NULL:  is->state = IS_NOP; break;
    658 		case VT_B32:   is->state = IS_SET; break;
    659 		case VT_GROUP: is->state = IS_SET; break;
    660 		case VT_F32: {
    661 			if (scroll) {
    662 				is->state = IS_SCROLL;
    663 			} else {
    664 				is->state = IS_TEXT;
    665 				begin_text_input(&ui->text_input_state, &is->hot);
    666 			}
    667 		} break;
    668 		}
    669 	}
    670 	if (is->state != IS_NONE) {
    671 		is->active = is->hot;
    672 	}
    673 }
    674 
    675 static void
    676 ui_end_interact(BeamformerCtx *ctx, BeamformerUI *ui)
    677 {
    678 	InteractionState *is = &ui->interaction;
    679 	switch (is->state) {
    680 	case IS_NONE: break;
    681 	case IS_NOP:  break;
    682 	case IS_SET: {
    683 		switch (is->active.type) {
    684 		case VT_B32: {
    685 			b32 *val = is->active.store;
    686 			*val = !(*val);
    687 		} break;
    688 		}
    689 	} break;
    690 	case IS_DISPLAY:
    691 		is->last_mouse_click_p = (v2){0};
    692 		/* FALLTHROUGH */
    693 	case IS_SCROLL: {
    694 		f32 delta = GetMouseWheelMove() * is->active.scroll_scale;
    695 		switch (is->active.type) {
    696 		case VT_B32: {
    697 			b32 *old_val = is->active.store;
    698 			b32  new_val = !(*old_val);
    699 			ui_store_variable(&is->active, &new_val);
    700 		} break;
    701 		case VT_F32: {
    702 			f32 *old_val = is->active.store;
    703 			f32  new_val = *old_val + delta;
    704 			ui_store_variable(&is->active, &new_val);
    705 		} break;
    706 		case VT_I32: {
    707 			i32 *old_val = is->active.store;
    708 			i32  new_val = *old_val + delta;
    709 			ui_store_variable(&is->active, &new_val);
    710 		} break;
    711 		}
    712 	} break;
    713 	case IS_TEXT: end_text_input(&ui->text_input_state, &is->active); break;
    714 	}
    715 
    716 	if (is->active.flags & V_CAUSES_COMPUTE)
    717 		ui_start_compute(ctx);
    718 
    719 	if (is->active.flags & V_GEN_MIPMAPS)
    720 		ui_gen_mipmaps(ctx);
    721 
    722 	is->state  = IS_NONE;
    723 	is->active = NULL_VARIABLE;
    724 }
    725 
    726 static void
    727 ui_interact(BeamformerCtx *ctx, BeamformerInput *input)
    728 {
    729 	BeamformerUI *ui       = ctx->ui;
    730 	InteractionState *is   = &ui->interaction;
    731 	b32 mouse_left_pressed = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
    732 	b32 wheel_moved        = GetMouseWheelMove();
    733 	if (mouse_left_pressed || wheel_moved) {
    734 		if (is->state != IS_NONE)
    735 			ui_end_interact(ctx, ui);
    736 		ui_begin_interact(ui, input, wheel_moved);
    737 	}
    738 
    739 	if (IsKeyPressed(KEY_ENTER) && is->state == IS_TEXT)
    740 		ui_end_interact(ctx, ui);
    741 
    742 	switch (is->state) {
    743 	case IS_DISPLAY: {
    744 		b32 should_end  = wheel_moved || IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) ||
    745 		                  (is->active.store != is->hot.store);
    746 		if (should_end) {
    747 			ui_end_interact(ctx, ui);
    748 		} else if (mouse_left_pressed) {
    749 			is->last_mouse_click_p = input->mouse;
    750 		}
    751 	} break;
    752 	case IS_SCROLL:  ui_end_interact(ctx, ui); break;
    753 	case IS_SET:     ui_end_interact(ctx, ui); break;
    754 	case IS_TEXT:    update_text_input(&ui->text_input_state); break;
    755 	case IS_DRAG: {
    756 		if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
    757 			ui_end_interact(ctx, ui);
    758 		} else {
    759 			switch (is->active.type) {
    760 			}
    761 		}
    762 	} break;
    763 	}
    764 
    765 	is->hot_state = IS_NONE;
    766 	is->hot       = NULL_VARIABLE;
    767 }
    768 
    769 static void
    770 ui_init(BeamformerCtx *ctx, Arena store)
    771 {
    772 	/* NOTE: store the ui at the base of the passed in arena and use the rest for
    773 	 * temporary allocations within the ui. If needed we can recall this function
    774 	 * to completely clear the ui state */
    775 	BeamformerUI *ui = ctx->ui = alloc(&store, typeof(*ctx->ui), 1);
    776 	ui->arena_for_frame = store;
    777 
    778 	/* TODO: build these into the binary */
    779 	ui->font       = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 28, 0, 0);
    780 	ui->small_font = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 22, 0, 0);
    781 }
    782 
    783 static void
    784 draw_ui(BeamformerCtx *ctx, BeamformerInput *input)
    785 {
    786 	BeamformerParameters *bp = &ctx->params->raw;
    787 	BeamformerUI *ui = ctx->ui;
    788 
    789 	end_temp_arena(ui->frame_temporary_arena);
    790 	ui->frame_temporary_arena = begin_temp_arena(&ui->arena_for_frame);
    791 
    792 	/* NOTE: process interactions first because the user interacted with
    793 	 * the ui that was presented last frame */
    794 	ui_interact(ctx, input);
    795 
    796 	BeginDrawing();
    797 		ClearBackground(colour_from_normalized(BG_COLOUR));
    798 
    799 		Texture *output   = &ctx->fsctx.output.texture;
    800 
    801 		/* TODO: this depends on the direction being rendered (x vs y) */
    802 		v2 output_dim = {
    803 			.x = bp->output_max_coordinate.x - bp->output_min_coordinate.x,
    804 			.y = bp->output_max_coordinate.z - bp->output_min_coordinate.z,
    805 		};
    806 
    807 		v2 mouse = input->mouse;
    808 		Rect wr = {.size = {.w = (f32)ctx->window_size.w, .h = (f32)ctx->window_size.h}};
    809 		Rect lr = wr, rr = wr;
    810 		lr.size.w = INFO_COLUMN_WIDTH;
    811 		rr.size.w = wr.size.w - lr.size.w;
    812 		rr.pos.x  = lr.pos.x  + lr.size.w;
    813 
    814 		Rect vr = INVERTED_INFINITY_RECT;
    815 		if (output_dim.x > 1e-6 && output_dim.y > 1e-6) {
    816 			Stream buf = stream_alloc(&ui->arena_for_frame, 64);
    817 			stream_append_f64(&buf, -188.8f, 10);
    818 			stream_append_s8(&buf, s8(" mm"));
    819 			v2 txt_s = measure_text(ui->small_font, stream_to_s8(&buf));
    820 			buf.widx = 0;
    821 
    822 			rr.pos.x  += 0.02 * rr.size.w;
    823 			rr.pos.y  += 0.02 * rr.size.h;
    824 			rr.size.w *= 0.96;
    825 			rr.size.h *= 0.96;
    826 
    827 			f32 tick_len = 20;
    828 			f32 pad      = 1.2 * txt_s.x + tick_len;
    829 
    830 			vr         = rr;
    831 			vr.pos.x  += 0.5 * txt_s.y;
    832 			vr.pos.y  += 0.5 * txt_s.y;
    833 			vr.size.h  = rr.size.h - pad;
    834 			vr.size.w  = rr.size.w - pad;
    835 
    836 			f32 aspect = output_dim.h / output_dim.w;
    837 			if (rr.size.h < (vr.size.w * aspect) + pad) {
    838 				vr.size.w = vr.size.h / aspect;
    839 			} else {
    840 				vr.size.h = vr.size.w * aspect;
    841 			}
    842 			vr.pos.x += (rr.size.w - (vr.size.w + pad)) / 2;
    843 			vr.pos.y += (rr.size.h - (vr.size.h + pad)) / 2;
    844 
    845 			Rectangle tex_r   = { 0.0f, 0.0f, (f32)output->width, -(f32)output->height };
    846 			NPatchInfo tex_np = { tex_r, 0, 0, 0, 0, NPATCH_NINE_PATCH };
    847 			DrawTextureNPatch(*output, tex_np, vr.rl, (Vector2){0}, 0, WHITE);
    848 
    849 			static f32 txt_colour_t[2];
    850 			for (u32 i = 0; i < 2; i++) {
    851 				u32 line_count   = vr.size.E[i] / (1.5 * txt_s.h);
    852 				f32 inc          = vr.size.E[i] / line_count;
    853 				v2 start_pos     = vr.pos;
    854 				start_pos.E[!i] += vr.size.E[!i];
    855 
    856 				v2 end_pos       = start_pos;
    857 				end_pos.E[!i]   += tick_len;
    858 
    859 				/* NOTE: Center the Text with the Tick center */
    860 				f32 txt_pos_scale[2] = {1, -1};
    861 				v2 txt_pos  = end_pos;
    862 				txt_pos.E[i]  += txt_pos_scale[i] * txt_s.y/2;
    863 				txt_pos.E[!i] += 10;
    864 
    865 				Rect tick_rect       = {.pos = start_pos, .size = vr.size};
    866 				tick_rect.size.E[!i] = 10 + tick_len + txt_s.x;
    867 
    868 				/* TODO: don't do this nonsense; this code will need to get
    869 				 * split into a seperate function */
    870 				/* TODO: pass this through the interaction system */
    871 				u32 coord_idx = i == 0? 0 : 2;
    872 				if (hover_text(mouse, tick_rect, txt_colour_t + i, 1)) {
    873 					f32 scale[2]   = {0.5e-3, 1e-3};
    874 					f32 size_delta = GetMouseWheelMove() * scale[i];
    875 					if (coord_idx == 0)
    876 						bp->output_min_coordinate.E[coord_idx] -= size_delta;
    877 					bp->output_max_coordinate.E[coord_idx] += size_delta;
    878 					if (size_delta)
    879 						ui_start_compute(ctx);
    880 				}
    881 
    882 				f32 mm     = bp->output_min_coordinate.E[coord_idx] * 1e3;
    883 				f32 mm_inc = inc * output_dim.E[i] * 1e3 / vr.size.E[i];
    884 
    885 				Color txt_colour = colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR,
    886 				                                                  txt_colour_t[i]));
    887 
    888 				f32 rot[2] = {90, 0};
    889 				for (u32 j = 0; j <= line_count; j++) {
    890 					DrawLineEx(start_pos.rl, end_pos.rl, 3, colour_from_normalized(FG_COLOUR));
    891 					buf.widx = 0;
    892 					if (i == 0 && mm > 0) stream_append_byte(&buf, '+');
    893 					stream_append_f64(&buf, mm, 10);
    894 					stream_append_s8(&buf, s8(" mm"));
    895 					draw_text(ui->small_font, stream_to_s8(&buf), txt_pos,
    896 					          rot[i], txt_colour);
    897 					start_pos.E[i] += inc;
    898 					end_pos.E[i]   += inc;
    899 					txt_pos.E[i]   += inc;
    900 					mm             += mm_inc;
    901 				}
    902 			}
    903 		}
    904 
    905 		draw_settings_ui(ctx, lr, mouse);
    906 		draw_debug_overlay(ctx, ui->arena_for_frame, lr);
    907 
    908 		if (CheckCollisionPointRec(mouse.rl, vr.rl)) {
    909 			InteractionState *is  = &ui->interaction;
    910 			is->hot_state         = IS_DISPLAY;
    911 			is->hot.store         = &ctx->fsctx.threshold;
    912 			is->hot.type          = VT_F32;
    913 			is->hot.f32_limits    = (v2){.y = 240};
    914 			is->hot.flags         = V_GEN_MIPMAPS;
    915 			is->hot.display_scale = 1;
    916 			is->hot.scroll_scale  = 1;
    917 
    918 			/* NOTE: check and draw Ruler */
    919 			if (CheckCollisionPointRec(is->last_mouse_click_p.rl, vr.rl)) {
    920 				Stream buf = arena_stream(&ui->arena_for_frame);
    921 
    922 				Color colour = colour_from_normalized(RULER_COLOUR);
    923 				DrawCircleV(is->last_mouse_click_p.rl, 3, colour);
    924 				DrawLineEx(mouse.rl, is->last_mouse_click_p.rl, 2, colour);
    925 				v2 pixels_to_mm = output_dim;
    926 				pixels_to_mm.x /= vr.size.x * 1e-3;
    927 				pixels_to_mm.y /= vr.size.y * 1e-3;
    928 
    929 				v2 pixel_delta = sub_v2(is->last_mouse_click_p, mouse);
    930 				v2 mm_delta    = mul_v2(pixels_to_mm, pixel_delta);
    931 
    932 				stream_append_f64(&buf, magnitude_v2(mm_delta), 100);
    933 				stream_append_s8(&buf, s8(" mm"));
    934 				v2 txt_p = is->last_mouse_click_p;
    935 				v2 txt_s = measure_text(ui->small_font, stream_to_s8(&buf));
    936 				if (pixel_delta.y < 0) txt_p.y -= txt_s.y;
    937 				if (pixel_delta.x < 0) txt_p.x -= txt_s.x;
    938 				draw_text(ui->small_font, stream_to_s8(&buf), txt_p, 0, colour);
    939 			}
    940 		}
    941 	EndDrawing();
    942 }