ogl_beamforming

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

ui.c (38379B)


      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 /* TODO(rnp): once this has more callers decide if it would be better for this to take
    115  * an orientation rather than force CCW/right-handed */
    116 static void
    117 draw_ruler(BeamformerUI *ui, Stream *buf, v2 start_point, v2 end_point,
    118            f32 start_value, f32 end_value, f32 *markers, u32 marker_count,
    119            u32 segments, s8 suffix, Color ruler_colour, Color txt_colour)
    120 {
    121 	b32 draw_plus = SIGN(start_value) != SIGN(end_value);
    122 
    123 	end_point    = sub_v2(end_point, start_point);
    124 	f32 rotation = atan2_f32(end_point.y, end_point.x) * 180 / PI;
    125 
    126 	rlPushMatrix();
    127 	rlTranslatef(start_point.x, start_point.y, 0);
    128 	rlRotatef(rotation, 0, 0, 1);
    129 
    130 	f32 inc       = magnitude_v2(end_point) / segments;
    131 	f32 value_inc = (end_value - start_value) / segments;
    132 	f32 value     = start_value;
    133 
    134 	v2 sp = {0}, ep = {.y = RULER_TICK_LENGTH};
    135 	v2 tp = {.x = ui->small_font_height / 2, .y = ep.y + RULER_TEXT_PAD};
    136 	for (u32 j = 0; j <= segments; j++) {
    137 		DrawLineEx(sp.rl, ep.rl, 3, ruler_colour);
    138 
    139 		buf->widx = 0;
    140 		if (draw_plus && value > 0) stream_append_byte(buf, '+');
    141 		stream_append_f64(buf, value, 10);
    142 		stream_append_s8(buf, suffix);
    143 		draw_text(ui->small_font, stream_to_s8(buf), tp, 90, txt_colour);
    144 
    145 		value += value_inc;
    146 		sp.x  += inc;
    147 		ep.x  += inc;
    148 		tp.x  += inc;
    149 	}
    150 
    151 	ep.y += RULER_TICK_LENGTH;
    152 	for (u32 i = 0; i < marker_count; i++) {
    153 		if (markers[i] < F32_INFINITY) {
    154 			ep.x  = sp.x = markers[i];
    155 			DrawLineEx(sp.rl, ep.rl, 3, colour_from_normalized(RULER_COLOUR));
    156 			DrawCircleV(ep.rl, 3, colour_from_normalized(RULER_COLOUR));
    157 		}
    158 	}
    159 
    160 	rlPopMatrix();
    161 }
    162 
    163 static void
    164 do_scale_bar(BeamformerUI *ui, Stream *buf, Variable var, v2 mouse, i32 direction, Rect draw_rect,
    165              f32 start_value, f32 end_value, s8 suffix)
    166 {
    167 	InteractionState *is = &ui->interaction;
    168 	ScaleBar *sb         = var.store;
    169 
    170 	v2 txt_s = measure_text(ui->small_font, s8("-288.8 mm"));
    171 
    172 	Rect tick_rect = draw_rect;
    173 	v2   start_pos = tick_rect.pos;
    174 	v2   end_pos   = tick_rect.pos;
    175 	v2   relative_mouse = sub_v2(mouse, tick_rect.pos);
    176 
    177 	f32  markers[2];
    178 	u32  marker_count = 1;
    179 
    180 	u32  tick_count;
    181 	if (direction == SB_AXIAL) {
    182 		tick_rect.size.x  = RULER_TEXT_PAD + RULER_TICK_LENGTH + txt_s.x;
    183 		tick_count        = tick_rect.size.y / (1.5 * ui->small_font_height);
    184 		start_pos.y      += tick_rect.size.y;
    185 		markers[0]        = tick_rect.size.y - sb->zoom_starting_point.y;
    186 		markers[1]        = tick_rect.size.y - relative_mouse.y;
    187 		sb->screen_offset = (v2){.y = tick_rect.pos.y};
    188 		sb->screen_space_to_value = (v2){.y = (*sb->max_value - *sb->min_value) / tick_rect.size.y};
    189 	} else {
    190 		tick_rect.size.y  = RULER_TEXT_PAD + RULER_TICK_LENGTH + txt_s.x;
    191 		tick_count        = tick_rect.size.x / (1.5 * ui->small_font_height);
    192 		end_pos.x        += tick_rect.size.x;
    193 		markers[0]        = sb->zoom_starting_point.x;
    194 		markers[1]        = relative_mouse.x;
    195 		/* TODO(rnp): screen space to value space transformation helper */
    196 		sb->screen_offset = (v2){.x = tick_rect.pos.x};
    197 		sb->screen_space_to_value = (v2){.x = (*sb->max_value - *sb->min_value) / tick_rect.size.x};
    198 	}
    199 
    200 	if (hover_text(mouse, tick_rect, &sb->hover_t, 1)) {
    201 		is->hot_state = IS_SCALE_BAR;
    202 		is->hot       = var;
    203 		marker_count  = 2;
    204 	}
    205 
    206 	draw_ruler(ui, buf, start_pos, end_pos, start_value, end_value, markers, marker_count,
    207 	           tick_count, suffix, colour_from_normalized(FG_COLOUR),
    208 	           colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, sb->hover_t)));
    209 }
    210 
    211 static void
    212 draw_display_overlay(BeamformerCtx *ctx, Arena a, v2 mouse, Rect display_rect, BeamformFrame *frame)
    213 {
    214 	BeamformerUI *ui         = ctx->ui;
    215 	BeamformerParameters *bp = &ctx->params->raw;
    216 	InteractionState *is     = &ui->interaction;
    217 
    218 	Stream buf      = arena_stream(&a);
    219 	Texture *output = &ctx->fsctx.output.texture;
    220 
    221 	v2 txt_s = measure_text(ui->small_font, s8("-288.8 mm"));
    222 
    223 	display_rect.pos.x  += 0.02 * display_rect.size.w;
    224 	display_rect.pos.y  += 0.02 * display_rect.size.h;
    225 	display_rect.size.w *= 0.96;
    226 	display_rect.size.h *= 0.96;
    227 
    228 	f32 pad    = 1.2 * txt_s.x + RULER_TICK_LENGTH;
    229 	Rect vr    = display_rect;
    230 	vr.pos.x  += 0.5 * ui->small_font_height;
    231 	vr.pos.y  += 0.5 * ui->small_font_height;
    232 	vr.size.h  = display_rect.size.h - pad;
    233 	vr.size.w  = display_rect.size.w - pad;
    234 
    235 	/* TODO(rnp): make this depend on the requested draw orientation (x-z or y-z or x-y) */
    236 	v2 output_dim = {
    237 		.x = frame->max_coordinate.x - frame->min_coordinate.x,
    238 		.y = frame->max_coordinate.z - frame->min_coordinate.z,
    239 	};
    240 	v2 requested_dim = {
    241 		.x = bp->output_max_coordinate.x - bp->output_min_coordinate.x,
    242 		.y = bp->output_max_coordinate.z - bp->output_min_coordinate.z,
    243 	};
    244 
    245 	f32 aspect = requested_dim.h / requested_dim.w;
    246 	if (display_rect.size.h < (vr.size.w * aspect) + pad) {
    247 		vr.size.w = vr.size.h / aspect;
    248 	} else {
    249 		vr.size.h = vr.size.w * aspect;
    250 	}
    251 	vr.pos.x += (display_rect.size.w - (vr.size.w + pad)) / 2;
    252 	vr.pos.y += (display_rect.size.h - (vr.size.h + pad)) / 2;
    253 
    254 	v2 pixels_per_meter = {
    255 		.w = (f32)output->width  / output_dim.w,
    256 		.h = (f32)output->height / output_dim.h,
    257 	};
    258 
    259 	v2 texture_points  = mul_v2(pixels_per_meter, requested_dim);
    260 	/* TODO(rnp): this also depends on x-y, y-z, x-z */
    261 	v2 texture_start   = {
    262 		.x = pixels_per_meter.x * 0.5 * (output_dim.x - requested_dim.x),
    263 		.y = pixels_per_meter.y * (frame->max_coordinate.z - bp->output_max_coordinate.z),
    264 	};
    265 
    266 	Rectangle  tex_r  = {texture_start.x, texture_start.y, texture_points.x, -texture_points.y};
    267 	NPatchInfo tex_np = { tex_r, 0, 0, 0, 0, NPATCH_NINE_PATCH };
    268 	DrawTextureNPatch(*output, tex_np, vr.rl, (Vector2){0}, 0, WHITE);
    269 
    270 	Variable var     = {.display_scale = 1e3};
    271 	var.store        = ui->scale_bars[0] + SB_LATERAL;
    272 	var.f32_limits   = (v2){.x = -1, .y = 1};
    273 	var.scroll_scale = 0.5e-3;
    274 
    275 	v2 start_pos  = vr.pos;
    276 	start_pos.y  += vr.size.y;
    277 
    278 	do_scale_bar(ui, &buf, var, mouse, SB_LATERAL, (Rect){.pos = start_pos, .size = vr.size},
    279 	             bp->output_min_coordinate.x * 1e3, bp->output_max_coordinate.x * 1e3, s8(" mm"));
    280 
    281 	var.store        = ui->scale_bars[0] + SB_AXIAL;
    282 	var.f32_limits   = (v2){.x = 0, .y = 1};
    283 	var.scroll_scale = 1e-3;
    284 
    285 	start_pos    = vr.pos;
    286 	start_pos.x += vr.size.x;
    287 
    288 	do_scale_bar(ui, &buf, var, mouse, SB_AXIAL, (Rect){.pos = start_pos, .size = vr.size},
    289 	             bp->output_max_coordinate.z * 1e3, bp->output_min_coordinate.z * 1e3, s8(" mm"));
    290 
    291 	v2 pixels_to_mm = output_dim;
    292 	pixels_to_mm.x /= vr.size.x * 1e-3;
    293 	pixels_to_mm.y /= vr.size.y * 1e-3;
    294 
    295 	if (CheckCollisionPointRec(mouse.rl, vr.rl)) {
    296 		is->hot_state         = IS_DISPLAY;
    297 		is->hot.store         = &ctx->fsctx.threshold;
    298 		is->hot.type          = VT_F32;
    299 		is->hot.f32_limits    = (v2){.y = 240};
    300 		is->hot.flags         = V_GEN_MIPMAPS;
    301 		is->hot.display_scale = 1;
    302 		is->hot.scroll_scale  = 1;
    303 
    304 		v2 relative_mouse = sub_v2(mouse, vr.pos);
    305 		v2 mm = mul_v2(relative_mouse, pixels_to_mm);
    306 		mm.x += 1e3 * bp->output_min_coordinate.x;
    307 		mm.y += 1e3 * bp->output_min_coordinate.z;
    308 
    309 		buf.widx = 0;
    310 		stream_append_v2(&buf, mm);
    311 		v2 txt_s = measure_text(ui->small_font, stream_to_s8(&buf));
    312 		v2 txt_p = {
    313 			.x = vr.pos.x + vr.size.w - txt_s.w - 4,
    314 			.y = vr.pos.y + vr.size.h - txt_s.h - 4,
    315 		};
    316 		draw_text(ui->small_font, stream_to_s8(&buf), txt_p, 0,
    317 		          colour_from_normalized(RULER_COLOUR));
    318 	}
    319 
    320 	/* TODO(rnp): store converted ruler points instead of screen points */
    321 	if (ui->ruler_state != RS_NONE && CheckCollisionPointRec(ui->ruler_start_p.rl, vr.rl)) {
    322 		v2 end_p;
    323 		if (ui->ruler_state == RS_START) end_p = mouse;
    324 		else                             end_p = ui->ruler_stop_p;
    325 
    326 		Color colour = colour_from_normalized(RULER_COLOUR);
    327 
    328 		end_p          = clamp_v2_rect(end_p, vr);
    329 		v2 pixel_delta = sub_v2(ui->ruler_start_p, end_p);
    330 		v2 mm_delta    = mul_v2(pixels_to_mm, pixel_delta);
    331 
    332 		DrawCircleV(ui->ruler_start_p.rl, 3, colour);
    333 		DrawLineEx(end_p.rl, ui->ruler_start_p.rl, 2, colour);
    334 		DrawCircleV(end_p.rl, 3, colour);
    335 
    336 		buf.widx = 0;
    337 		stream_append_f64(&buf, magnitude_v2(mm_delta), 100);
    338 		stream_append_s8(&buf, s8(" mm"));
    339 
    340 		v2 txt_p = ui->ruler_start_p;
    341 		v2 txt_s = measure_text(ui->small_font, stream_to_s8(&buf));
    342 		if (pixel_delta.y < 0) txt_p.y -= txt_s.y;
    343 		if (pixel_delta.x < 0) txt_p.x -= txt_s.x;
    344 		draw_text(ui->small_font, stream_to_s8(&buf), txt_p, 0, colour);
    345 	}
    346 }
    347 
    348 /* TODO(rnp): this is known after the first frame, we could unbind
    349  * the texture for the first draw pass or just accept a slight glitch
    350  * at start up (make a good default guess) */
    351 /* NOTE: This is kinda sucks no matter how you spin it. If we want the values to be
    352  * left aligned in the center column we need to know the longest prefix length but
    353  * without either hardcoding one of the prefixes as the longest one or measuring all
    354  * of them we can't know this ahead of time. For now we hardcode this and manually
    355  * adjust when needed */
    356 #define LISTING_LEFT_COLUMN_WIDTH 270.0f
    357 #define LISTING_LINE_PAD           6.0f
    358 
    359 static Rect
    360 do_value_listing(s8 prefix, s8 suffix, Arena a, f32 value, Font font, Rect r)
    361 {
    362 	v2 suffix_s = measure_text(font, suffix);
    363 	v2 suffix_p = {.x = r.pos.x + r.size.w - suffix_s.w, .y = r.pos.y};
    364 
    365 	Stream buf = arena_stream(&a);
    366 	stream_append_f64(&buf, value, 100);
    367 	v2 txt_p = {.x = r.pos.x + LISTING_LEFT_COLUMN_WIDTH, .y = r.pos.y};
    368 
    369 	draw_text(font, prefix,             r.pos,    0, colour_from_normalized(FG_COLOUR));
    370 	draw_text(font, stream_to_s8(&buf), txt_p,    0, colour_from_normalized(FG_COLOUR));
    371 	draw_text(font, suffix,             suffix_p, 0, colour_from_normalized(FG_COLOUR));
    372 	r.pos.y  += suffix_s.h + LISTING_LINE_PAD;
    373 	r.size.y -= suffix_s.h + LISTING_LINE_PAD;
    374 
    375 	return r;
    376 }
    377 
    378 static Rect
    379 do_text_input_listing(s8 prefix, s8 suffix, Variable var, BeamformerUI *ui, Rect r,
    380                       v2 mouse, f32 *hover_t)
    381 {
    382 	InputState   *is = &ui->text_input_state;
    383 	b32 text_input_active = (ui->interaction.state == IS_TEXT) &&
    384 	                        (var.store == ui->interaction.active.store);
    385 
    386 	Arena  arena = ui->arena_for_frame;
    387 	Stream buf   = arena_stream(&arena);
    388 	v2 txt_s;
    389 
    390 	if (text_input_active) {
    391 		txt_s = measure_text(ui->font, (s8){.len = is->buf_len, .data = is->buf});
    392 	} else {
    393 		stream_append_variable(&buf, &var);
    394 		txt_s = measure_text(ui->font, stream_to_s8(&buf));
    395 	}
    396 
    397 	Rect edit_rect = {
    398 		.pos  = {.x = r.pos.x + LISTING_LEFT_COLUMN_WIDTH, .y = r.pos.y},
    399 		.size = {.x = txt_s.w + TEXT_BOX_EXTRA_X, .y = txt_s.h}
    400 	};
    401 
    402 	b32 hovering = hover_text(mouse, edit_rect, hover_t, !text_input_active);
    403 	if (hovering)
    404 		ui->interaction.hot = var;
    405 
    406 	/* TODO: where should this go? */
    407 	if (text_input_active && is->cursor == -1) {
    408 		/* NOTE: extra offset to help with putting a cursor at idx 0 */
    409 		#define TEXT_HALF_CHAR_WIDTH 10
    410 		f32 hover_p = CLAMP01((mouse.x - edit_rect.pos.x) / edit_rect.size.w);
    411 		f32 x_off = TEXT_HALF_CHAR_WIDTH, x_bounds = edit_rect.size.w * hover_p;
    412 		i32 i;
    413 		for (i = 0; i < is->buf_len && x_off < x_bounds; i++) {
    414 			/* NOTE: assumes font glyphs are ordered ASCII */
    415 			i32 idx  = is->buf[i] - 0x20;
    416 			x_off   += ui->font.glyphs[idx].advanceX;
    417 			if (ui->font.glyphs[idx].advanceX == 0)
    418 				x_off += ui->font.recs[idx].width;
    419 		}
    420 		is->cursor = i;
    421 	}
    422 
    423 	Color colour = colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, *hover_t));
    424 
    425 	if (text_input_active) {
    426 		s8 buf = {.len = is->buf_len, .data = is->buf};
    427 		v2 ts  = measure_text(ui->font, buf);
    428 		v2 pos = {.x = edit_rect.pos.x, .y = edit_rect.pos.y + (edit_rect.size.y - ts.y) / 2};
    429 
    430 		#define MAX_DISP_CHARS 7
    431 		i32 buf_delta = is->buf_len - MAX_DISP_CHARS;
    432 		if (buf_delta < 0) buf_delta = 0;
    433 		buf.len  -= buf_delta;
    434 		buf.data += buf_delta;
    435 		{
    436 			/* NOTE: drop a char if the subtext still doesn't fit */
    437 			v2 nts = measure_text(ui->font, buf);
    438 			if (nts.w > 0.96 * edit_rect.size.w) {
    439 				buf.data++;
    440 				buf.len--;
    441 			}
    442 		}
    443 		draw_text(ui->font, buf, pos, 0, colour);
    444 
    445 		v4 bg = FOCUSED_COLOUR;
    446 		bg.a  = 0;
    447 		Color cursor_colour = colour_from_normalized(lerp_v4(bg, FOCUSED_COLOUR,
    448 		                                                     CLAMP01(is->cursor_blink_t)));
    449 		buf.len = is->cursor - buf_delta;
    450 		v2 sts = measure_text(ui->font, buf);
    451 		f32 cursor_x = pos.x + sts.x;
    452 		f32 cursor_width;
    453 		if (is->cursor == is->buf_len) cursor_width = 20;
    454 		else                           cursor_width = 4;
    455 		Rect cursor_r = {
    456 			.pos  = {.x = cursor_x,     .y = pos.y},
    457 			.size = {.w = cursor_width, .h = ts.h},
    458 		};
    459 
    460 		DrawRectanglePro(cursor_r.rl, (Vector2){0}, 0, cursor_colour);
    461 	} else {
    462 		draw_text(ui->font, stream_to_s8(&buf), edit_rect.pos, 0, colour);
    463 	}
    464 
    465 	v2 suffix_s = measure_text(ui->font, suffix);
    466 	v2 suffix_p = {.x = r.pos.x + r.size.w - suffix_s.w, .y = r.pos.y};
    467 	draw_text(ui->font, prefix, r.pos,    0, colour_from_normalized(FG_COLOUR));
    468 	draw_text(ui->font, suffix, suffix_p, 0, colour_from_normalized(FG_COLOUR));
    469 
    470 	r.pos.y  += suffix_s.h + LISTING_LINE_PAD;
    471 	r.size.y -= suffix_s.h + LISTING_LINE_PAD;
    472 
    473 	return r;
    474 }
    475 
    476 static Rect
    477 do_text_toggle_listing(s8 prefix, s8 text0, s8 text1, Variable var,
    478                        BeamformerUI *ui, Rect r, v2 mouse, f32 *hover_t)
    479 {
    480 	b32 toggle = *(b32 *)var.store;
    481 	v2 txt_s;
    482 	if (toggle) txt_s = measure_text(ui->font, text1);
    483 	else        txt_s = measure_text(ui->font, text0);
    484 
    485 	Rect edit_rect = {
    486 		.pos  = {.x = r.pos.x + LISTING_LEFT_COLUMN_WIDTH, .y = r.pos.y},
    487 		.size = {.x = txt_s.w + TEXT_BOX_EXTRA_X, .y = txt_s.h}
    488 	};
    489 
    490 	if (hover_text(mouse, edit_rect, hover_t, 1))
    491 		ui->interaction.hot = var;
    492 
    493 	Color colour = colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, *hover_t));
    494 	draw_text(ui->font, prefix, r.pos, 0, colour_from_normalized(FG_COLOUR));
    495 	draw_text(ui->font, toggle? text1: text0, edit_rect.pos, 0, colour);
    496 
    497 	r.pos.y  += txt_s.h + LISTING_LINE_PAD;
    498 	r.size.y -= txt_s.h + LISTING_LINE_PAD;
    499 
    500 	return r;
    501 }
    502 
    503 static b32
    504 do_text_button(BeamformerUI *ui, s8 text, Rect r, v2 mouse, f32 *hover_t)
    505 {
    506 	b32 hovering = hover_text(mouse, r, hover_t, 1);
    507 	b32 pressed  = 0;
    508 	pressed     |= (hovering && IsMouseButtonPressed(MOUSE_BUTTON_LEFT));
    509 	pressed     |= (hovering && IsMouseButtonPressed(MOUSE_BUTTON_RIGHT));
    510 
    511 	f32 param  = lerp(1, 1.04, *hover_t);
    512 	v2  bscale = (v2){
    513 		.x = param + RECT_BTN_BORDER_WIDTH / r.size.w,
    514 		.y = param + RECT_BTN_BORDER_WIDTH / r.size.h,
    515 	};
    516 	Rect sr    = scale_rect_centered(r, (v2){.x = param, .y = param});
    517 	Rect sb    = scale_rect_centered(r, bscale);
    518 	DrawRectangleRounded(sb.rl, RECT_BTN_ROUNDNESS, 0, RECT_BTN_BORDER_COLOUR);
    519 	DrawRectangleRounded(sr.rl, RECT_BTN_ROUNDNESS, 0, RECT_BTN_COLOUR);
    520 
    521 	v2 tpos   = center_align_text_in_rect(r, text, ui->font);
    522 	v2 spos   = {.x = tpos.x + 1.75, .y = tpos.y + 2};
    523 	v4 colour = lerp_v4(FG_COLOUR, HOVERED_COLOUR, *hover_t);
    524 
    525 	draw_text(ui->font, text, spos, 0, fade(BLACK, 0.8));
    526 	draw_text(ui->font, text, tpos, 0, colour_from_normalized(colour));
    527 
    528 	return pressed;
    529 }
    530 
    531 static void
    532 draw_settings_ui(BeamformerCtx *ctx, Rect r, v2 mouse)
    533 {
    534 	BeamformerUI *ui         = ctx->ui;
    535 	BeamformerParameters *bp = &ctx->params->raw;
    536 
    537 	f32 minx = bp->output_min_coordinate.x + 1e-6, maxx = bp->output_max_coordinate.x - 1e-6;
    538 	f32 minz = bp->output_min_coordinate.z + 1e-6, maxz = bp->output_max_coordinate.z - 1e-6;
    539 
    540 	Rect draw_r    = r;
    541 	draw_r.pos.y  += 20;
    542 	draw_r.pos.x  += 20;
    543 	draw_r.size.x -= 20;
    544 	draw_r.size.y -= 20;
    545 
    546 	draw_r = do_value_listing(s8("Sampling Frequency:"), s8("[MHz]"), ui->arena_for_frame,
    547 	                          bp->sampling_frequency * 1e-6, ui->font, draw_r);
    548 
    549 	static f32 hover_t[15];
    550 	i32 idx = 0;
    551 
    552 	Variable var;
    553 
    554 	var.store         = &bp->center_frequency;
    555 	var.type          = VT_F32;
    556 	var.f32_limits    = (v2){.y = 100e6};
    557 	var.flags         = V_CAUSES_COMPUTE;
    558 	var.display_scale = 1e-6;
    559 	var.scroll_scale  = 1e5;
    560 	draw_r = do_text_input_listing(s8("Center Frequency:"), s8("[MHz]"), var, ui, draw_r,
    561 	                               mouse, hover_t + idx++);
    562 
    563 	var.store         = &bp->speed_of_sound;
    564 	var.type          = VT_F32;
    565 	var.f32_limits    = (v2){.y = 1e6};
    566 	var.flags         = V_CAUSES_COMPUTE;
    567 	var.display_scale = 1;
    568 	var.scroll_scale  = 10;
    569 	draw_r = do_text_input_listing(s8("Speed of Sound:"), s8("[m/s]"), var, ui, draw_r,
    570 	                               mouse, hover_t + idx++);
    571 
    572 	var.store         = &bp->output_min_coordinate.x;
    573 	var.type          = VT_F32;
    574 	var.f32_limits    = (v2){.x = -1e3, .y = maxx};
    575 	var.flags         = V_CAUSES_COMPUTE;
    576 	var.display_scale = 1e3;
    577 	var.scroll_scale  = 0.5e-3;
    578 	draw_r = do_text_input_listing(s8("Min Lateral Point:"), s8("[mm]"), var, ui, draw_r,
    579 	                               mouse, hover_t + idx++);
    580 
    581 	var.store         = &bp->output_max_coordinate.x;
    582 	var.type          = VT_F32;
    583 	var.f32_limits    = (v2){.x = minx, .y = 1e3};
    584 	var.flags         = V_CAUSES_COMPUTE;
    585 	var.display_scale = 1e3;
    586 	var.scroll_scale  = 0.5e-3;
    587 	draw_r = do_text_input_listing(s8("Max Lateral Point:"), s8("[mm]"), var, ui, draw_r,
    588 	                               mouse, hover_t + idx++);
    589 
    590 	var.store         = &bp->output_min_coordinate.z;
    591 	var.type          = VT_F32;
    592 	var.f32_limits    = (v2){.y = maxz};
    593 	var.flags         = V_CAUSES_COMPUTE;
    594 	var.display_scale = 1e3;
    595 	var.scroll_scale  = 0.5e-3;
    596 	draw_r = do_text_input_listing(s8("Min Axial Point:"), s8("[mm]"), var, ui, draw_r,
    597 	                               mouse, hover_t + idx++);
    598 
    599 	var.store         = &bp->output_max_coordinate.z;
    600 	var.type          = VT_F32;
    601 	var.f32_limits    = (v2){.x = minz, .y = 1e3};
    602 	var.flags         = V_CAUSES_COMPUTE;
    603 	var.display_scale = 1e3;
    604 	var.scroll_scale  = 0.5e-3;
    605 	draw_r = do_text_input_listing(s8("Max Axial Point:"), s8("[mm]"), var, ui, draw_r,
    606 	                               mouse, hover_t + idx++);
    607 
    608 	var.store         = &bp->off_axis_pos;
    609 	var.type          = VT_F32;
    610 	var.f32_limits    = (v2){.x = minx, .y = maxx};
    611 	var.flags         = V_CAUSES_COMPUTE;
    612 	var.display_scale = 1e3;
    613 	var.scroll_scale  = 0.5e-3;
    614 	draw_r = do_text_input_listing(s8("Off Axis Position:"), s8("[mm]"), var, ui, draw_r,
    615 	                               mouse, hover_t + idx++);
    616 
    617 	var       = (Variable){0};
    618 	var.store = &bp->beamform_plane;
    619 	var.type  = VT_B32;
    620 	var.flags = V_CAUSES_COMPUTE;
    621 	draw_r = do_text_toggle_listing(s8("Beamform Plane:"), s8("XZ"), s8("YZ"), var, ui,
    622 	                                draw_r, mouse, hover_t + idx++);
    623 
    624 	var.store         = &bp->f_number;
    625 	var.type          = VT_F32;
    626 	var.f32_limits    = (v2){.y = 1e3};
    627 	var.flags         = V_CAUSES_COMPUTE;
    628 	var.display_scale = 1;
    629 	var.scroll_scale  = 0.1;
    630 	draw_r = do_text_input_listing(s8("F#:"), s8(""), var, ui, draw_r, mouse, hover_t + idx++);
    631 
    632 	var.store         = &ctx->fsctx.db;
    633 	var.type          = VT_F32;
    634 	var.f32_limits    = (v2){.x = -120};
    635 	var.flags         = V_GEN_MIPMAPS;
    636 	var.display_scale = 1;
    637 	var.scroll_scale  = 1;
    638 	draw_r = do_text_input_listing(s8("Dynamic Range:"), s8("[dB]"), var, ui, draw_r,
    639 	                               mouse, hover_t + idx++);
    640 
    641 	var.store         = &ctx->fsctx.threshold;
    642 	var.type          = VT_F32;
    643 	var.f32_limits    = (v2){.y = 240};
    644 	var.flags         = V_GEN_MIPMAPS;
    645 	var.display_scale = 1;
    646 	var.scroll_scale  = 1;
    647 	draw_r = do_text_input_listing(s8("Threshold:"), s8(""), var, ui, draw_r,
    648 	                               mouse, hover_t + idx++);
    649 
    650 	draw_r.pos.y  += 2 * LISTING_LINE_PAD;
    651 	draw_r.size.y -= 2 * LISTING_LINE_PAD;
    652 
    653 	#if 0
    654 	/* TODO: work this into the work queue */
    655 	bmv = (BPModifiableValue){&ctx->partial_compute_ctx.volume_dim.x, bmv_store_power_of_two,
    656 	                          .ilimits = (iv2){.x = 1, .y = ctx->gl.max_3d_texture_dim},
    657 	                          MV_INT, 1, 1};
    658 	draw_r = do_text_input_listing(s8("Export Dimension X:"), s8(""), bmv, ctx, arena,
    659 	                               draw_r, mouse, hover_t + idx++);
    660 
    661 	bmv = (BPModifiableValue){&ctx->partial_compute_ctx.volume_dim.y, bmv_store_power_of_two,
    662 	                          .ilimits = (iv2){.x = 1, .y = ctx->gl.max_3d_texture_dim},
    663 	                          MV_INT, 1, 1};
    664 	draw_r = do_text_input_listing(s8("Export Dimension Y:"), s8(""), bmv, ctx, arena,
    665 	                               draw_r, mouse, hover_t + idx++);
    666 
    667 	bmv = (BPModifiableValue){&ctx->partial_compute_ctx.volume_dim.z, bmv_store_power_of_two,
    668 	                          .ilimits = (iv2){.x = 1, .y = ctx->gl.max_3d_texture_dim},
    669 	                          MV_INT, 1, 1};
    670 	draw_r = do_text_input_listing(s8("Export Dimension Z:"), s8(""), bmv, ctx, arena,
    671 	                               draw_r, mouse, hover_t + idx++);
    672 
    673 	Rect btn_r = draw_r;
    674 	btn_r.size.h  = ctx->font.baseSize * 1.3;
    675 	btn_r.size.w *= 0.6;
    676 	if (do_text_button(ctx, s8("Dump Raw Volume"), btn_r, mouse, hover_t + idx++)) {
    677 		if (!ctx->partial_compute_ctx.state) {
    678 		}
    679 	}
    680 	#endif
    681 
    682 	/* NOTE: if C compilers didn't suck this would be a static assert */
    683 	ASSERT(idx <= ARRAY_COUNT(hover_t));
    684 }
    685 
    686 static void
    687 draw_debug_overlay(BeamformerCtx *ctx, Arena arena, Rect r)
    688 {
    689 	static s8 labels[CS_LAST] = {
    690 		#define X(e, n, s, h, pn) [CS_##e] = s8(pn ":"),
    691 		COMPUTE_SHADERS
    692 		#undef X
    693 	};
    694 
    695 	BeamformerUI *ui     = ctx->ui;
    696 	ComputeShaderCtx *cs = &ctx->csctx;
    697 	uv2 ws = ctx->window_size;
    698 
    699 	Stream buf = stream_alloc(&arena, 64);
    700 	v2 pos     = {.x = 20, .y = ws.h - 10};
    701 
    702 	f32 compute_time_sum = 0;
    703 	u32 stages = ctx->params->compute_stages_count;
    704 	for (u32 i = 0; i < stages; i++) {
    705 		u32 index  = ctx->params->compute_stages[i];
    706 		pos.y     -= measure_text(ui->font, labels[index]).y;
    707 		draw_text(ui->font, labels[index], pos, 0, colour_from_normalized(FG_COLOUR));
    708 
    709 		buf.widx = 0;
    710 		stream_append_f64_e(&buf, cs->last_frame_time[index]);
    711 		stream_append_s8(&buf, s8(" [s]"));
    712 		v2 txt_fs = measure_text(ui->font, stream_to_s8(&buf));
    713 		v2 rpos   = {.x = r.pos.x + r.size.w - txt_fs.w, .y = pos.y};
    714 		draw_text(ui->font, stream_to_s8(&buf), rpos, 0, colour_from_normalized(FG_COLOUR));
    715 
    716 		compute_time_sum += cs->last_frame_time[index];
    717 	}
    718 
    719 	static s8 totals[2] = {s8("Compute Total:"), s8("Volume Total:")};
    720 	f32 times[2]        = {compute_time_sum, ctx->partial_compute_ctx.runtime};
    721 	for (u32 i = 0; i < ARRAY_COUNT(totals); i++) {
    722 		pos.y    -= measure_text(ui->font, totals[i]).y;
    723 		draw_text(ui->font, totals[i], pos, 0, colour_from_normalized(FG_COLOUR));
    724 
    725 		buf.widx = 0;
    726 		stream_append_f64_e(&buf, times[i]);
    727 		stream_append_s8(&buf, s8(" [s]"));
    728 		v2 txt_fs = measure_text(ui->font, stream_to_s8(&buf));
    729 		v2 rpos   = {.x = r.pos.x + r.size.w - txt_fs.w, .y = pos.y};
    730 		draw_text(ui->font, stream_to_s8(&buf), rpos, 0, colour_from_normalized(FG_COLOUR));
    731 	}
    732 
    733 	{
    734 		static v2 pos       = {.x = 32,  .y = 128};
    735 		static v2 scale     = {.x = 1.0, .y = 1.0};
    736 		static u32 txt_idx  = 0;
    737 		static s8 txt[2]    = { s8("-_-"), s8("^_^") };
    738 		static v2 ts[2];
    739 		if (ts[0].x == 0) {
    740 			ts[0] = measure_text(ui->font, txt[0]);
    741 			ts[1] = measure_text(ui->font, txt[1]);
    742 		}
    743 
    744 		pos.x += 130 * dt_for_frame * scale.x;
    745 		pos.y += 120 * dt_for_frame * scale.y;
    746 
    747 		if (pos.x > (ws.w - ts[txt_idx].x) || pos.x < 0) {
    748 			txt_idx  = !txt_idx;
    749 			pos.x    = CLAMP(pos.x, 0, ws.w - ts[txt_idx].x);
    750 			scale.x *= -1.0;
    751 		}
    752 
    753 		if (pos.y > (ws.h - ts[txt_idx].y) || pos.y < 0) {
    754 			txt_idx  = !txt_idx;
    755 			pos.y    = CLAMP(pos.y, 0, ws.h - ts[txt_idx].y);
    756 			scale.y *= -1.0;
    757 		}
    758 
    759 		draw_text(ui->font, txt[txt_idx], pos, 0, RED);
    760 	}
    761 }
    762 
    763 static void
    764 ui_store_variable(Variable *var, void *new_value)
    765 {
    766 	/* TODO: special cases (eg. power of 2) */
    767 	switch (var->type) {
    768 	case VT_F32: {
    769 		f32  f32_val = *(f32 *)new_value;
    770 		f32 *f32_var = var->store;
    771 		*f32_var     = CLAMP(f32_val, var->f32_limits.x, var->f32_limits.y);
    772 	} break;
    773 	case VT_I32: {
    774 		i32  i32_val = *(i32 *)new_value;
    775 		i32 *i32_var = var->store;
    776 		*i32_var     = CLAMP(i32_val, var->i32_limits.x, var->i32_limits.y);
    777 	} break;
    778 	default: INVALID_CODE_PATH;
    779 	}
    780 }
    781 
    782 static void
    783 begin_text_input(InputState *is, Variable *var)
    784 {
    785 	ASSERT(var->store != NULL);
    786 
    787 	Stream s = {.cap = ARRAY_COUNT(is->buf), .data = is->buf};
    788 	stream_append_variable(&s, var);
    789 	ASSERT(!s.errors);
    790 	is->buf_len = s.widx;
    791 	is->cursor  = -1;
    792 }
    793 
    794 static void
    795 end_text_input(InputState *is, Variable *var)
    796 {
    797 	f32 value = parse_f64((s8){.len = is->buf_len, .data = is->buf}) / var->display_scale;
    798 	ui_store_variable(var, &value);
    799 }
    800 
    801 static void
    802 update_text_input(InputState *is)
    803 {
    804 	if (is->cursor == -1)
    805 		return;
    806 
    807 	is->cursor_blink_t += is->cursor_blink_scale * dt_for_frame;
    808 	if (is->cursor_blink_t >= 1) is->cursor_blink_scale = -1.5f;
    809 	if (is->cursor_blink_t <= 0) is->cursor_blink_scale =  1.5f;
    810 
    811 	/* NOTE: handle multiple input keys on a single frame */
    812 	i32 key = GetCharPressed();
    813 	while (key > 0) {
    814 		if (is->buf_len == ARRAY_COUNT(is->buf))
    815 			break;
    816 
    817 		b32 allow_key = ((key >= '0' && key <= '9') || (key == '.') ||
    818 		                 (key == '-' && is->cursor == 0));
    819 		if (allow_key) {
    820 			mem_move(is->buf + is->cursor,
    821 			         is->buf + is->cursor + 1,
    822 			         is->buf_len - is->cursor + 1);
    823 
    824 			is->buf[is->cursor++] = key;
    825 			is->buf_len++;
    826 		}
    827 		key = GetCharPressed();
    828 	}
    829 
    830 	if ((IsKeyPressed(KEY_LEFT) || IsKeyPressedRepeat(KEY_LEFT)) && is->cursor > 0)
    831 		is->cursor--;
    832 
    833 	if ((IsKeyPressed(KEY_RIGHT) || IsKeyPressedRepeat(KEY_RIGHT)) && is->cursor < is->buf_len)
    834 		is->cursor++;
    835 
    836 	if ((IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE)) && is->cursor > 0) {
    837 		is->cursor--;
    838 		mem_move(is->buf + is->cursor + 1,
    839 		         is->buf + is->cursor,
    840 		         is->buf_len - is->cursor);
    841 		is->buf_len--;
    842 	}
    843 
    844 	if ((IsKeyPressed(KEY_DELETE) || IsKeyPressedRepeat(KEY_DELETE)) && is->cursor < is->buf_len) {
    845 		mem_move(is->buf + is->cursor + 1,
    846 		         is->buf + is->cursor,
    847 		         is->buf_len - is->cursor);
    848 		is->buf_len--;
    849 	}
    850 }
    851 
    852 static b32
    853 ui_can_start_compute(BeamformerCtx *ctx)
    854 {
    855 	BeamformFrame *displayed = ctx->beamform_frames + ctx->displayed_frame_index;
    856 	b32 result  = ctx->beamform_work_queue.compute_in_flight == 0;
    857 	result     &= (displayed->dim.x != 0 || displayed->dim.y != 0);
    858 	result     &= displayed->dim.z != 0;
    859 	return result;
    860 }
    861 
    862 static void
    863 ui_start_compute(BeamformerCtx *ctx)
    864 {
    865 	/* NOTE: we do not allow ui to start a work if no work was previously completed */
    866 	Arena a = {0};
    867 	if (ui_can_start_compute(ctx)) {
    868 		beamform_work_queue_push(ctx, &a, BW_RECOMPUTE);
    869 		BeamformFrameIterator bfi = beamform_frame_iterator(ctx);
    870 		for (BeamformFrame *frame = frame_next(&bfi); frame; frame = frame_next(&bfi))
    871 			glClearTexImage(frame->texture, 0, GL_RED, GL_FLOAT, 0);
    872 	}
    873 	ctx->params->upload = 1;
    874 }
    875 
    876 static void
    877 ui_gen_mipmaps(BeamformerCtx *ctx)
    878 {
    879 	if (ctx->fsctx.output.texture.id)
    880 		ctx->fsctx.gen_mipmaps = 1;
    881 }
    882 
    883 static void
    884 display_interaction_end(BeamformerUI *ui)
    885 {
    886 	b32 is_hot    = ui->interaction.hot_state == IS_DISPLAY;
    887 	b32 is_active = ui->interaction.state     == IS_DISPLAY;
    888 	if ((is_active && is_hot) || ui->ruler_state == RS_HOLD)
    889 		return;
    890 	ui->ruler_state = RS_NONE;
    891 }
    892 
    893 static void
    894 display_interaction(BeamformerUI *ui, v2 mouse)
    895 {
    896 	b32 mouse_left_pressed  = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
    897 	b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT);
    898 	b32 is_hot              = ui->interaction.hot_state == IS_DISPLAY;
    899 	b32 is_active           = ui->interaction.state     == IS_DISPLAY;
    900 
    901 	if (mouse_left_pressed && is_active) {
    902 		ui->ruler_state++;
    903 		switch (ui->ruler_state) {
    904 		case RS_START: ui->ruler_start_p = mouse; break;
    905 		case RS_HOLD:  ui->ruler_stop_p  = mouse; break;
    906 		default:
    907 			ui->ruler_state = RS_NONE;
    908 			break;
    909 		}
    910 	} else if ((mouse_left_pressed && !is_hot) || (mouse_right_pressed && is_hot)) {
    911 		ui->ruler_state = RS_NONE;
    912 	}
    913 }
    914 
    915 static void
    916 scale_bar_interaction(BeamformerCtx *ctx, v2 mouse)
    917 {
    918 	BeamformerUI *ui        = ctx->ui;
    919 	InteractionState *is    = &ui->interaction;
    920 	ScaleBar *sb            = is->active.store;
    921 	b32 mouse_left_pressed  = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
    922 	b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT);
    923 	f32 mouse_wheel         = GetMouseWheelMoveV().y * is->active.scroll_scale;
    924 
    925 	if (mouse_left_pressed) {
    926 		if (sb->zoom_starting_point.x == F32_INFINITY) {
    927 			sb->zoom_starting_point = sub_v2(mouse, sb->screen_offset);
    928 		} else {
    929 			v2 relative_mouse = sub_v2(mouse, sb->screen_offset);
    930 			f32 min = magnitude_v2(mul_v2(sb->zoom_starting_point, sb->screen_space_to_value));
    931 			f32 max = magnitude_v2(mul_v2(relative_mouse,          sb->screen_space_to_value));
    932 			if (min > max) { f32 tmp = min; min = max; max = tmp; }
    933 
    934 			min += *sb->min_value;
    935 			max += *sb->min_value;
    936 
    937 			/* TODO(rnp): SLL_* macros */
    938 			v2_sll *savepoint = ui->scale_bar_savepoint_freelist;
    939 			if (!savepoint) savepoint = push_struct(&ui->arena_for_frame, v2_sll);
    940 			ui->scale_bar_savepoint_freelist = savepoint->next;
    941 
    942 			savepoint->v.x      = *sb->min_value;
    943 			savepoint->v.y      = *sb->max_value;
    944 			savepoint->next     = sb->savepoint_stack;
    945 			sb->savepoint_stack = savepoint;
    946 
    947 			*sb->min_value = MAX(min, is->active.f32_limits.x);
    948 			*sb->max_value = MIN(max, is->active.f32_limits.y);
    949 
    950 			sb->zoom_starting_point = (v2){.x = F32_INFINITY, .y = F32_INFINITY};
    951 			ui_start_compute(ctx);
    952 		}
    953 	}
    954 
    955 	if (mouse_right_pressed) {
    956 		v2_sll *savepoint = sb->savepoint_stack;
    957 		if (savepoint) {
    958 			*sb->min_value = savepoint->v.x;
    959 			*sb->max_value = savepoint->v.y;
    960 			ui_start_compute(ctx);
    961 
    962 			sb->savepoint_stack = savepoint->next;
    963 			savepoint->next     = ui->scale_bar_savepoint_freelist;
    964 			ui->scale_bar_savepoint_freelist = savepoint;
    965 		}
    966 		sb->zoom_starting_point = (v2){.x = F32_INFINITY, .y = F32_INFINITY};
    967 	}
    968 
    969 	if (mouse_wheel) {
    970 		v2 limits = is->active.f32_limits;
    971 		*sb->min_value -= mouse_wheel * sb->scroll_both;
    972 		*sb->max_value += mouse_wheel;
    973 		*sb->min_value  = MAX(limits.x, *sb->min_value);
    974 		*sb->max_value  = MIN(limits.y, *sb->max_value);
    975 		ui_start_compute(ctx);
    976 	}
    977 }
    978 
    979 static void
    980 ui_begin_interact(BeamformerUI *ui, BeamformerInput *input, b32 scroll, b32 mouse_left_pressed)
    981 {
    982 	InteractionState *is = &ui->interaction;
    983 	if (is->hot_state != IS_NONE) {
    984 		is->state = is->hot_state;
    985 	} else {
    986 		switch (is->hot.type) {
    987 		case VT_NULL:  is->state = IS_NOP; break;
    988 		case VT_B32:   is->state = IS_SET; break;
    989 		case VT_GROUP: is->state = IS_SET; break;
    990 		case VT_F32: {
    991 			if (scroll) {
    992 				is->state = IS_SCROLL;
    993 			} else if (mouse_left_pressed) {
    994 				is->state = IS_TEXT;
    995 				begin_text_input(&ui->text_input_state, &is->hot);
    996 			}
    997 		} break;
    998 		}
    999 	}
   1000 	if (is->state != IS_NONE) {
   1001 		is->active = is->hot;
   1002 	}
   1003 }
   1004 
   1005 static void
   1006 ui_end_interact(BeamformerCtx *ctx, v2 mouse)
   1007 {
   1008 	BeamformerUI *ui = ctx->ui;
   1009 	InteractionState *is = &ui->interaction;
   1010 	switch (is->state) {
   1011 	case IS_NONE: break;
   1012 	case IS_NOP:  break;
   1013 	case IS_SET: {
   1014 		switch (is->active.type) {
   1015 		case VT_B32: {
   1016 			b32 *val = is->active.store;
   1017 			*val = !(*val);
   1018 		} break;
   1019 		}
   1020 	} break;
   1021 	case IS_DISPLAY: display_interaction_end(ui); /* FALLTHROUGH */
   1022 	case IS_SCROLL: {
   1023 		f32 delta = GetMouseWheelMoveV().y * is->active.scroll_scale;
   1024 		switch (is->active.type) {
   1025 		case VT_B32: {
   1026 			b32 *old_val = is->active.store;
   1027 			b32  new_val = !(*old_val);
   1028 			ui_store_variable(&is->active, &new_val);
   1029 		} break;
   1030 		case VT_F32: {
   1031 			f32 *old_val = is->active.store;
   1032 			f32  new_val = *old_val + delta;
   1033 			ui_store_variable(&is->active, &new_val);
   1034 		} break;
   1035 		case VT_I32: {
   1036 			i32 *old_val = is->active.store;
   1037 			i32  new_val = *old_val + delta;
   1038 			ui_store_variable(&is->active, &new_val);
   1039 		} break;
   1040 		}
   1041 	} break;
   1042 	case IS_SCALE_BAR: break;
   1043 	case IS_TEXT:      end_text_input(&ui->text_input_state, &is->active); break;
   1044 	}
   1045 
   1046 	if (is->active.flags & V_CAUSES_COMPUTE)
   1047 		ui_start_compute(ctx);
   1048 
   1049 	if (is->active.flags & V_GEN_MIPMAPS)
   1050 		ui_gen_mipmaps(ctx);
   1051 
   1052 	is->state  = IS_NONE;
   1053 	is->active = NULL_VARIABLE;
   1054 }
   1055 
   1056 static void
   1057 ui_interact(BeamformerCtx *ctx, BeamformerInput *input)
   1058 {
   1059 	BeamformerUI *ui        = ctx->ui;
   1060 	InteractionState *is    = &ui->interaction;
   1061 	b32 mouse_left_pressed  = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
   1062 	b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT);
   1063 	b32 wheel_moved         = GetMouseWheelMoveV().y != 0;
   1064 	if (mouse_right_pressed || mouse_left_pressed || wheel_moved) {
   1065 		if (is->state != IS_NONE)
   1066 			ui_end_interact(ctx, input->mouse);
   1067 		ui_begin_interact(ui, input, wheel_moved, mouse_left_pressed);
   1068 	}
   1069 
   1070 	if (IsKeyPressed(KEY_ENTER) && is->state == IS_TEXT)
   1071 		ui_end_interact(ctx, input->mouse);
   1072 
   1073 	switch (is->state) {
   1074 	case IS_DISPLAY: display_interaction(ui, input->mouse);    break;
   1075 	case IS_SCROLL:  ui_end_interact(ctx, input->mouse);       break;
   1076 	case IS_SET:     ui_end_interact(ctx, input->mouse);       break;
   1077 	case IS_TEXT:    update_text_input(&ui->text_input_state); break;
   1078 	case IS_DRAG: {
   1079 		if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
   1080 			ui_end_interact(ctx, input->mouse);
   1081 		} else {
   1082 			switch (is->active.type) {
   1083 			}
   1084 		}
   1085 	} break;
   1086 	case IS_SCALE_BAR: scale_bar_interaction(ctx, input->mouse); break;
   1087 	}
   1088 
   1089 	is->hot_state = IS_NONE;
   1090 	is->hot       = NULL_VARIABLE;
   1091 }
   1092 
   1093 static void
   1094 ui_init(BeamformerCtx *ctx, Arena store)
   1095 {
   1096 	/* NOTE(rnp): store the ui at the base of the passed in arena and use the rest for
   1097 	 * temporary allocations within the ui. If needed we can recall this function to
   1098 	 * completely clear the ui state. The is that if we store pointers to static data
   1099 	 * such as embedded font data we will need to reset them when the executable reloads.
   1100 	 * We could also build some sort of ui structure here and store it then iterate over
   1101 	 * it to actually draw the ui. If we reload we may have changed it so we should
   1102 	 * rebuild it */
   1103 
   1104 	/* NOTE: unload old fonts from the GPU */
   1105 	if (ctx->ui) {
   1106 		UnloadFont(ctx->ui->font);
   1107 		UnloadFont(ctx->ui->small_font);
   1108 	}
   1109 
   1110 	BeamformerUI *ui = ctx->ui = alloc(&store, typeof(*ctx->ui), 1);
   1111 	ui->arena_for_frame = store;
   1112 	ui->frame_temporary_arena = begin_temp_arena(&ui->arena_for_frame);
   1113 
   1114 	/* TODO: build these into the binary */
   1115 	ui->font       = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 28, 0, 0);
   1116 	ui->small_font = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 22, 0, 0);
   1117 
   1118 	ui->font_height       = measure_text(ui->font, s8("8\\W")).h;
   1119 	ui->small_font_height = measure_text(ui->small_font, s8("8\\W")).h;
   1120 
   1121 	/* TODO: multiple views */
   1122 	ui->scale_bars[0][SB_LATERAL].min_value = &ctx->params->raw.output_min_coordinate.x;
   1123 	ui->scale_bars[0][SB_LATERAL].max_value = &ctx->params->raw.output_max_coordinate.x;
   1124 	ui->scale_bars[0][SB_AXIAL].min_value   = &ctx->params->raw.output_min_coordinate.z;
   1125 	ui->scale_bars[0][SB_AXIAL].max_value   = &ctx->params->raw.output_max_coordinate.z;
   1126 
   1127 	ui->scale_bars[0][SB_LATERAL].scroll_both = 1;
   1128 	ui->scale_bars[0][SB_AXIAL].scroll_both   = 0;
   1129 
   1130 	ui->scale_bars[0][SB_LATERAL].zoom_starting_point = (v2){.x = F32_INFINITY, .y = F32_INFINITY};
   1131 	ui->scale_bars[0][SB_AXIAL].zoom_starting_point   = (v2){.x = F32_INFINITY, .y = F32_INFINITY};
   1132 
   1133 }
   1134 
   1135 static void
   1136 draw_ui(BeamformerCtx *ctx, BeamformerInput *input, BeamformFrame *frame_to_draw)
   1137 {
   1138 	BeamformerUI *ui = ctx->ui;
   1139 
   1140 	/* TODO(rnp): we need an ALLOC_END flag so that we can have permanent storage
   1141 	 * or we need a sub arena for the save point stack */
   1142 	//end_temp_arena(ui->frame_temporary_arena);
   1143 	//ui->frame_temporary_arena = begin_temp_arena(&ui->arena_for_frame);
   1144 
   1145 	/* NOTE: process interactions first because the user interacted with
   1146 	 * the ui that was presented last frame */
   1147 	ui_interact(ctx, input);
   1148 
   1149 	BeginDrawing();
   1150 		ClearBackground(colour_from_normalized(BG_COLOUR));
   1151 
   1152 		v2 mouse = input->mouse;
   1153 		Rect wr = {.size = {.w = (f32)ctx->window_size.w, .h = (f32)ctx->window_size.h}};
   1154 		Rect lr = wr, rr = wr;
   1155 		lr.size.w = INFO_COLUMN_WIDTH;
   1156 		rr.size.w = wr.size.w - lr.size.w;
   1157 		rr.pos.x  = lr.pos.x  + lr.size.w;
   1158 
   1159 		draw_settings_ui(ctx, lr, mouse);
   1160 		if (frame_to_draw->dim.w)
   1161 			draw_display_overlay(ctx, ui->arena_for_frame, mouse, rr, frame_to_draw);
   1162 		draw_debug_overlay(ctx, ui->arena_for_frame, lr);
   1163 	EndDrawing();
   1164 }