ogl_beamforming

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

ui.c (75685B)


      1 /* See LICENSE for license details. */
      2 /* TODO(rnp):
      3  * [ ]: scroll bar for views that don't have enough space
      4  * [ ]: compute times through same path as parameter list ?
      5  * [ ]: global menu
      6  * [ ]: allow views to collapse to just their title bar
      7  * [ ]: enforce a minimum region size or allow regions themselves to scroll
      8  * [ ]: allow scale bars to collapse
      9  * [ ]: move remaining fragment shader stuff into ui
     10  * [ ]: refactor: draw_left_aligned_list()
     11  * [ ]: refactor: draw_variable_table()
     12  * [ ]: refactor: add_variable_no_link()
     13  * [ ]: refactor: draw_text_limited should clamp to rect and measure text itself
     14  * [ ]: refactor: draw_active_menu should just use draw_variable_list
     15  * [ ]: refactor: draw_beamformer_variable -> draw_variable; draw_variable -> draw_layout_variable
     16  * [ ]: refactor: scale bars should just be variables
     17  * [ ]: refactor: remove scale bar limits (limits should only prevent invalid program state)
     18  * [ ]: ui leaks split beamform views on hot-reload
     19  * [ ]: add tag based selection to frame views
     20  * [ ]: draw the ui with a post-order traversal instead of pre-order traversal
     21  */
     22 
     23 #define BG_COLOUR              (v4){.r = 0.15, .g = 0.12, .b = 0.13, .a = 1.0}
     24 #define FG_COLOUR              (v4){.r = 0.92, .g = 0.88, .b = 0.78, .a = 1.0}
     25 #define FOCUSED_COLOUR         (v4){.r = 0.86, .g = 0.28, .b = 0.21, .a = 1.0}
     26 #define HOVERED_COLOUR         (v4){.r = 0.11, .g = 0.50, .b = 0.59, .a = 1.0}
     27 #define RULER_COLOUR           (v4){.r = 1.00, .g = 0.70, .b = 0.00, .a = 1.0}
     28 
     29 #define MENU_PLUS_COLOUR       (v4){.r = 0.33, .g = 0.42, .b = 1.00, .a = 1.0}
     30 #define MENU_CLOSE_COLOUR      FOCUSED_COLOUR
     31 
     32 #define NORMALIZED_FG_COLOUR   colour_from_normalized(FG_COLOUR)
     33 
     34 #define HOVER_SPEED            5.0f
     35 
     36 #define RULER_TEXT_PAD         10.0f
     37 #define RULER_TICK_LENGTH      20.0f
     38 
     39 #define UI_SPLIT_HANDLE_THICK  8.0f
     40 #define UI_REGION_PAD          32.0f
     41 
     42 /* TODO(rnp) smooth scroll */
     43 #define UI_SCROLL_SPEED 12.0f
     44 
     45 #define LISTING_ITEM_PAD   12.0f
     46 #define LISTING_INDENT     20.0f
     47 #define LISTING_LINE_PAD    6.0f
     48 #define TITLE_BAR_PAD       6.0f
     49 
     50 typedef struct {
     51 	u8   buf[64];
     52 	i32  buf_len;
     53 	i32  cursor;
     54 	f32  cursor_blink_t;
     55 	f32  cursor_blink_scale;
     56 	Rect rect, hot_rect;
     57 	Font *font, *hot_font;
     58 } InputState;
     59 
     60 typedef struct {
     61 	v2    at;
     62 	Font *font, *hot_font;
     63 } MenuState;
     64 
     65 typedef enum {
     66 	IT_NONE,
     67 	IT_NOP,
     68 	IT_DISPLAY,
     69 	IT_DRAG,
     70 	IT_MENU,
     71 	IT_SCALE_BAR,
     72 	IT_SCROLL,
     73 	IT_SET,
     74 	IT_TEXT,
     75 } InteractionType;
     76 
     77 enum ruler_state {
     78 	RS_NONE,
     79 	RS_START,
     80 	RS_HOLD,
     81 };
     82 
     83 typedef struct v2_sll {
     84 	struct v2_sll *next;
     85 	v2             v;
     86 } v2_sll;
     87 
     88 typedef struct BeamformerUI BeamformerUI;
     89 typedef struct Variable Variable;
     90 
     91 typedef enum {
     92 	RSD_VERTICAL,
     93 	RSD_HORIZONTAL,
     94 } RegionSplitDirection;
     95 
     96 typedef struct {
     97 	Variable *left;
     98 	Variable *right;
     99 	f32       fraction;
    100 	RegionSplitDirection direction;
    101 } RegionSplit;
    102 
    103 /* TODO(rnp): this should be refactored to not need a BeamformerCtx */
    104 typedef struct {
    105 	BeamformerCtx *ctx;
    106 	void          *stats;
    107 } ComputeStatsView;
    108 
    109 typedef struct {
    110 	b32 *processing;
    111 	f32 *progress;
    112 	f32 display_t;
    113 	f32 display_t_velocity;
    114 } ComputeProgressBar;
    115 
    116 typedef enum {
    117 	VT_NULL,
    118 	VT_B32,
    119 	VT_F32,
    120 	VT_I32,
    121 	VT_GROUP,
    122 	VT_BEAMFORMER_VARIABLE,
    123 	VT_BEAMFORMER_FRAME_VIEW,
    124 	VT_COMPUTE_STATS_VIEW,
    125 	VT_COMPUTE_LATEST_STATS_VIEW,
    126 	VT_COMPUTE_PROGRESS_BAR,
    127 	VT_SCALE_BAR,
    128 	VT_UI_BUTTON,
    129 	VT_UI_VIEW,
    130 	VT_UI_REGION_SPLIT,
    131 } VariableType;
    132 
    133 typedef enum {
    134 	VG_LIST,
    135 	/* NOTE(rnp): special groups for vectors with components
    136 	 * stored in separate memory locations */
    137 	VG_V2,
    138 	VG_V4,
    139 } VariableGroupType;
    140 
    141 typedef struct {
    142 	Variable *first;
    143 	Variable *last;
    144 	b32       expanded;
    145 	f32       max_name_width;
    146 	VariableGroupType type;
    147 } VariableGroup;
    148 
    149 typedef enum {
    150 	UI_VIEW_CUSTOM_TEXT = 1 << 0,
    151 } UIViewFlags;
    152 
    153 typedef struct {
    154 	/* NOTE(rnp): superset of group, group must come first */
    155 	VariableGroup  group;
    156 	Variable      *close;
    157 	Variable      *menu;
    158 	f32            needed_height;
    159 	f32            offset;
    160 	UIViewFlags    flags;
    161 } UIView;
    162 
    163 /* X(id, text) */
    164 #define FRAME_VIEW_BUTTONS \
    165 	X(FV_COPY_HORIZONTAL, "Copy Horizontal") \
    166 	X(FV_COPY_VERTICAL,   "Copy Vertical")
    167 
    168 #define X(id, text) UI_BID_ ##id,
    169 typedef enum {
    170 	UI_BID_CLOSE_VIEW,
    171 	FRAME_VIEW_BUTTONS
    172 } UIButtonID;
    173 #undef X
    174 
    175 typedef struct {
    176 	s8       suffix;
    177 	/* TODO(rnp): think of something better than this */
    178 	union {
    179 		struct {s8 *names; u32 count;} name_table;
    180 		struct {
    181 			f32 display_scale;
    182 			f32 scroll_scale;
    183 			v2  limits;
    184 		} params;
    185 	};
    186 	void         *store;
    187 	VariableType  store_type;
    188 } BeamformerVariable;
    189 
    190 typedef enum {
    191 	V_INPUT          = 1 << 0,
    192 	V_TEXT           = 1 << 1,
    193 	V_BUTTON         = 1 << 2,
    194 	V_MENU           = 1 << 3,
    195 	V_CAUSES_COMPUTE = 1 << 29,
    196 	V_UPDATE_VIEW    = 1 << 30,
    197 } VariableFlags;
    198 
    199 struct Variable {
    200 	s8 name;
    201 	union {
    202 		void               *generic;
    203 		BeamformerVariable  beamformer_variable;
    204 		ComputeProgressBar  compute_progress_bar;
    205 		ComputeStatsView    compute_stats_view;
    206 		RegionSplit         region_split;
    207 		UIButtonID          button;
    208 		UIView              view;
    209 		VariableGroup       group;
    210 		b32                 b32;
    211 		i32                 i32;
    212 		f32                 f32;
    213 	} u;
    214 	Variable *next;
    215 	Variable *parent;
    216 	VariableFlags flags;
    217 	VariableType  type;
    218 
    219 	f32 hover_t;
    220 	f32 name_width;
    221 };
    222 
    223 typedef enum {
    224 	SB_LATERAL,
    225 	SB_AXIAL,
    226 } ScaleBarDirection;
    227 
    228 typedef struct {
    229 	f32    *min_value, *max_value;
    230 	v2_sll *savepoint_stack;
    231 	v2      zoom_starting_point;
    232 	v2      screen_offset;
    233 	v2      screen_space_to_value;
    234 	v2      limits;
    235 	v2      scroll_scale;
    236 	f32     hover_t;
    237 	b32     causes_compute;
    238 } ScaleBar;
    239 
    240 typedef enum {
    241 	FVT_INDEXED,
    242 	FVT_LATEST,
    243 	FVT_COPY,
    244 } BeamformerFrameViewType;
    245 
    246 typedef struct BeamformerFrameView {
    247 	ScaleBar lateral_scale_bar;
    248 	ScaleBar axial_scale_bar;
    249 
    250 	v4 min_coordinate;
    251 	v4 max_coordinate;
    252 
    253 	Variable threshold;
    254 	Variable dynamic_range;
    255 
    256 	FragmentShaderCtx *ctx;
    257 	BeamformFrame     *frame;
    258 	struct BeamformerFrameView *prev, *next;
    259 
    260 	RenderTexture2D rendered_view;
    261 	BeamformerFrameViewType type;
    262 	b32 needs_update;
    263 } BeamformerFrameView;
    264 
    265 typedef struct {
    266 	Variable *hot;
    267 	Variable *active;
    268 	InteractionType hot_type;
    269 	InteractionType type;
    270 } InteractionState;
    271 
    272 struct BeamformerUI {
    273 	Arena arena;
    274 
    275 	Font font;
    276 	Font small_font;
    277 
    278 	Variable *regions;
    279 	Variable *variable_freelist;
    280 	Variable *scratch_variable;
    281 	Variable  scratch_variables[2];
    282 
    283 	BeamformerFrameView *views;
    284 	BeamformerFrameView *view_freelist;
    285 	BeamformFrame       *frame_freelist;
    286 
    287 	InteractionState interaction;
    288 	/* TODO(rnp): these can be combined */
    289 	MenuState        menu_state;
    290 	InputState       text_input_state;
    291 
    292 	v2_sll *scale_bar_savepoint_freelist;
    293 
    294 	v2  ruler_start_p;
    295 	v2  ruler_stop_p;
    296 	u32 ruler_state;
    297 
    298 	b32                 latest_frame_changed;
    299 	BeamformFrame      *latest_frame;
    300 	ComputeShaderStats *latest_compute_stats;
    301 
    302 	BeamformerUIParameters params;
    303 	b32                    flush_params;
    304 
    305 	iptr                   last_displayed_frame;
    306 
    307 	OS *os;
    308 };
    309 
    310 typedef enum {
    311 	TF_NONE     = 0,
    312 	TF_ROTATED  = 1 << 0,
    313 	TF_LIMITED  = 1 << 1,
    314 	TF_OUTLINED = 1 << 2,
    315 } TextFlags;
    316 
    317 typedef struct {
    318 	Font  *font;
    319 	Rect  limits;
    320 	Color colour;
    321 	Color outline_colour;
    322 	f32   outline_thick;
    323 	f32   rotation;
    324 	TextFlags flags;
    325 } TextSpec;
    326 
    327 static v2
    328 measure_text(Font font, s8 text)
    329 {
    330 	v2 result = {.y = font.baseSize};
    331 	for (iz i = 0; i < text.len; i++) {
    332 		/* NOTE: assumes font glyphs are ordered ASCII */
    333 		i32 idx   = (i32)text.data[i] - 0x20;
    334 		result.x += font.glyphs[idx].advanceX;
    335 		if (font.glyphs[idx].advanceX == 0)
    336 			result.x += (font.recs[idx].width + font.glyphs[idx].offsetX);
    337 	}
    338 	return result;
    339 }
    340 
    341 static void
    342 stream_append_variable_base(Stream *s, VariableType type, void *var, void *scale)
    343 {
    344 	switch (type) {
    345 	case VT_B32: {
    346 		s8 *text = var;
    347 		stream_append_s8(s, text[*(b32 *)scale != 0]);
    348 	} break;
    349 	case VT_F32: {
    350 		f32 val = *(f32 *)var * *(f32 *)scale;
    351 		stream_append_f64(s, val, 100);
    352 	} break;
    353 	default: INVALID_CODE_PATH;
    354 	}
    355 }
    356 
    357 static void
    358 stream_append_variable(Stream *s, Variable *var)
    359 {
    360 	switch (var->type) {
    361 	case VT_UI_BUTTON:
    362 	case VT_GROUP: stream_append_s8(s, var->name); break;
    363 	case VT_BEAMFORMER_VARIABLE: {
    364 		BeamformerVariable *bv = &var->u.beamformer_variable;
    365 		switch (bv->store_type) {
    366 		case VT_B32: {
    367 			stream_append_variable_base(s, VT_B32, bv->name_table.names, bv->store);
    368 		} break;
    369 		case VT_F32: {
    370 			stream_append_variable_base(s, VT_F32, bv->store, &bv->params.display_scale);
    371 		} break;
    372 		default: INVALID_CODE_PATH;
    373 		}
    374 	} break;
    375 	case VT_B32: {
    376 		s8 texts[] = {s8("False"), s8("True")};
    377 		stream_append_variable_base(s, VT_B32, texts, &var->u.b32);
    378 	} break;
    379 	case VT_F32: {
    380 		f32 scale = 1;
    381 		stream_append_variable_base(s, VT_F32, &var->u.f32, &scale);
    382 	} break;
    383 	default: INVALID_CODE_PATH;
    384 	}
    385 }
    386 
    387 static void
    388 resize_frame_view(BeamformerFrameView *view, uv2 dim)
    389 {
    390 	UnloadRenderTexture(view->rendered_view);
    391 	/* TODO(rnp): sometimes when accepting data on w32 something happens
    392 	* and the program will stall in vprintf in TraceLog(...) here.
    393 	* for now do this to avoid the problem */
    394 	//SetTraceLogLevel(LOG_NONE);
    395 	view->rendered_view = LoadRenderTexture(dim.x, dim.y);
    396 	//SetTraceLogLevel(LOG_INFO);
    397 
    398 	/* TODO(rnp): add some ID for the specific view here */
    399 	LABEL_GL_OBJECT(GL_FRAMEBUFFER, view->rendered_view.id,         s8("Frame View"));
    400 	LABEL_GL_OBJECT(GL_TEXTURE,     view->rendered_view.texture.id, s8("Frame View Texture"));
    401 	glGenerateTextureMipmap(view->rendered_view.texture.id);
    402 
    403 	//SetTextureFilter(view->rendered_view.texture, TEXTURE_FILTER_ANISOTROPIC_8X);
    404 	//SetTextureFilter(view->rendered_view.texture, TEXTURE_FILTER_TRILINEAR);
    405 	//SetTextureFilter(view->rendered_view.texture, TEXTURE_FILTER_BILINEAR);
    406 
    407 	/* NOTE(rnp): work around raylib's janky texture sampling */
    408 	i32 id = view->rendered_view.texture.id;
    409 	glTextureParameteri(id, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    410 	glTextureParameteri(id, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    411 	glTextureParameterfv(id, GL_TEXTURE_BORDER_COLOR, (f32 []){0, 0, 0, 1});
    412 }
    413 
    414 static void
    415 ui_variable_free(BeamformerUI *ui, Variable *var)
    416 {
    417 	if (var) {
    418 		var->parent = 0;
    419 		while (var) {
    420 			if (var->type == VT_GROUP || var->type == VT_UI_VIEW) {
    421 				var = var->u.group.first;
    422 			} else {
    423 				if (var->type == VT_BEAMFORMER_FRAME_VIEW) {
    424 					/* TODO(rnp): instead there should be a way of linking these up */
    425 					BeamformerFrameView *bv = var->u.generic;
    426 					glDeleteTextures(1, &bv->frame->texture);
    427 					bv->frame->texture = 0;
    428 					DLLRemove(bv);
    429 					/* TODO(rnp): hack; use a sentinal */
    430 					if (bv == ui->views)
    431 						ui->views = bv->next;
    432 					SLLPush(bv->frame, ui->frame_freelist);
    433 					SLLPush(bv,        ui->view_freelist);
    434 				}
    435 
    436 				Variable *next = SLLPush(var, ui->variable_freelist);
    437 				if (next) {
    438 					var = next;
    439 				} else {
    440 					var = var->parent;
    441 					/* NOTE(rnp): when we assign parent here we have already
    442 					 * released the children. Assign type so we don't loop */
    443 					if (var) var->type = VT_NULL;
    444 				}
    445 			}
    446 		}
    447 	}
    448 }
    449 
    450 static void
    451 ui_view_free(BeamformerUI *ui, Variable *view)
    452 {
    453 	ASSERT(view->type == VT_UI_VIEW);
    454 	ui_variable_free(ui, view->u.view.close);
    455 	ui_variable_free(ui, view->u.view.menu);
    456 	ui_variable_free(ui, view);
    457 }
    458 
    459 static Variable *
    460 fill_variable(Variable *var, Variable *group, s8 name, u32 flags, VariableType type, Font font)
    461 {
    462 	var->flags      = flags;
    463 	var->type       = type;
    464 	var->name       = name;
    465 	var->parent     = group;
    466 	var->name_width = measure_text(font, name).x;
    467 
    468 	if (group && (group->type == VT_GROUP || group->type == VT_UI_VIEW)) {
    469 		if (group->u.group.last) group->u.group.last = group->u.group.last->next = var;
    470 		else                     group->u.group.last = group->u.group.first      = var;
    471 
    472 		group->u.group.max_name_width = MAX(group->u.group.max_name_width, var->name_width);
    473 	}
    474 
    475 	return var;
    476 }
    477 
    478 static Variable *
    479 add_variable(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, u32 flags,
    480              VariableType type, Font font)
    481 {
    482 	Variable *result = SLLPop(ui->variable_freelist);
    483 	if (result) zero_struct(result);
    484 	else        result = push_struct(arena, Variable);
    485 	return fill_variable(result, group, name, flags, type, font);
    486 }
    487 
    488 static Variable *
    489 add_variable_group(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, VariableGroupType type, Font font)
    490 {
    491 	Variable *result     = add_variable(ui, group, arena, name, V_INPUT, VT_GROUP, font);
    492 	result->u.group.type = type;
    493 	return result;
    494 }
    495 
    496 static Variable *
    497 end_variable_group(Variable *group)
    498 {
    499 	ASSERT(group->type == VT_GROUP);
    500 	return group->parent;
    501 }
    502 
    503 static Variable *
    504 add_button(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, UIButtonID id, Font font)
    505 {
    506 	Variable *result = add_variable(ui, group, arena, name, V_INPUT, VT_UI_BUTTON, font);
    507 	result->u.button = id;
    508 	return result;
    509 }
    510 
    511 static Variable *
    512 add_ui_split(BeamformerUI *ui, Variable *parent, Arena *arena, s8 name, f32 fraction,
    513              RegionSplitDirection direction, Font font)
    514 {
    515 	Variable *result = add_variable(ui, parent, arena, name, 0, VT_UI_REGION_SPLIT, font);
    516 	result->u.region_split.direction = direction;
    517 	result->u.region_split.fraction  = fraction;
    518 	return result;
    519 }
    520 
    521 static Variable *
    522 add_ui_view(BeamformerUI *ui, Variable *parent, Arena *arena, s8 name, u32 view_flags, b32 closable)
    523 {
    524 	Variable *result = add_variable(ui, parent, arena, name, 0, VT_UI_VIEW, ui->small_font);
    525 	UIView   *view   = &result->u.view;
    526 	view->group.type = VG_LIST;
    527 	view->flags      = view_flags;
    528 	if (closable) {
    529 		view->close = add_button(ui, 0, arena, s8(""), UI_BID_CLOSE_VIEW, ui->small_font);
    530 		/* NOTE(rnp): we do this explicitly so that close doesn't end up in the view group */
    531 		view->close->parent = result;
    532 	}
    533 	return result;
    534 }
    535 
    536 static void
    537 add_beamformer_variable_f32(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, s8 suffix,
    538                             f32 *store, v2 limits, f32 display_scale, f32 scroll_scale, u32 flags,
    539                             Font font)
    540 {
    541 	Variable *var = add_variable(ui, group, arena, name, flags, VT_BEAMFORMER_VARIABLE, font);
    542 	BeamformerVariable *bv   = &var->u.beamformer_variable;
    543 	bv->suffix               = suffix;
    544 	bv->store                = store;
    545 	bv->store_type           = VT_F32;
    546 	bv->params.display_scale = display_scale;
    547 	bv->params.scroll_scale  = scroll_scale;
    548 	bv->params.limits        = limits;
    549 }
    550 
    551 static void
    552 add_beamformer_variable_b32(BeamformerUI *ui, Variable *group, Arena *arena, s8 name,
    553                             s8 false_text, s8 true_text, b32 *store, u32 flags, Font font)
    554 {
    555 	Variable *var = add_variable(ui, group, arena, name, flags, VT_BEAMFORMER_VARIABLE, font);
    556 	BeamformerVariable *bv  = &var->u.beamformer_variable;
    557 	bv->store               = store;
    558 	bv->store_type          = VT_B32;
    559 	bv->name_table.names    = alloc(arena, s8, 2);
    560 	bv->name_table.count    = 2;
    561 	bv->name_table.names[0] = false_text;
    562 	bv->name_table.names[1] = true_text;
    563 }
    564 
    565 static Variable *
    566 add_beamformer_parameters_view(Variable *parent, BeamformerCtx *ctx)
    567 {
    568 	BeamformerUI *ui           = ctx->ui;
    569 	BeamformerUIParameters *bp = &ui->params;
    570 
    571 	v2 v2_inf = {.x = -F32_INFINITY, .y = F32_INFINITY};
    572 
    573 	/* TODO(rnp): this can be closable once we have a way of opening new views */
    574 	Variable *result = add_ui_view(ui, parent, &ui->arena, s8("Parameters"), 0, 0);
    575 
    576 	add_beamformer_variable_f32(ui, result, &ui->arena, s8("Sampling Frequency:"), s8("[MHz]"),
    577 	                            &bp->sampling_frequency, (v2){0}, 1e-6, 0, 0, ui->font);
    578 
    579 	add_beamformer_variable_f32(ui, result, &ui->arena, s8("Center Frequency:"), s8("[MHz]"),
    580 	                            &bp->center_frequency, (v2){.y = 100e-6}, 1e-6, 1e5,
    581 	                            V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
    582 
    583 	add_beamformer_variable_f32(ui, result, &ui->arena, s8("Speed of Sound:"), s8("[m/s]"),
    584 	                            &bp->speed_of_sound, (v2){.y = 1e6}, 1, 10,
    585 	                            V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
    586 
    587 	result = add_variable_group(ui, result, &ui->arena, s8("Lateral Extent:"), VG_V2, ui->font);
    588 	{
    589 		add_beamformer_variable_f32(ui, result, &ui->arena, s8("Min:"), s8("[mm]"),
    590 		                            &bp->output_min_coordinate.x, v2_inf, 1e3, 0.5e-3,
    591 		                            V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
    592 
    593 		add_beamformer_variable_f32(ui, result, &ui->arena, s8("Max:"), s8("[mm]"),
    594 		                            &bp->output_max_coordinate.x, v2_inf, 1e3, 0.5e-3,
    595 		                            V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
    596 	}
    597 	result = end_variable_group(result);
    598 
    599 	result = add_variable_group(ui, result, &ui->arena, s8("Axial Extent:"), VG_V2, ui->font);
    600 	{
    601 		add_beamformer_variable_f32(ui, result, &ui->arena, s8("Min:"), s8("[mm]"),
    602 		                            &bp->output_min_coordinate.z, v2_inf, 1e3, 0.5e-3,
    603 		                            V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
    604 
    605 		add_beamformer_variable_f32(ui, result, &ui->arena, s8("Max:"), s8("[mm]"),
    606 		                            &bp->output_max_coordinate.z, v2_inf, 1e3, 0.5e-3,
    607 		                            V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
    608 	}
    609 	result = end_variable_group(result);
    610 
    611 	add_beamformer_variable_f32(ui, result, &ui->arena, s8("Off Axis Position:"), s8("[mm]"),
    612 	                            &bp->off_axis_pos, (v2){.x = -1e3, .y = 1e3}, 1e3,
    613 	                            0.5e-3, V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
    614 
    615 	add_beamformer_variable_b32(ui, result, &ui->arena, s8("Beamform Plane:"), s8("XZ"), s8("YZ"),
    616 	                            (b32 *)&bp->beamform_plane, V_INPUT|V_CAUSES_COMPUTE, ui->font);
    617 
    618 	add_beamformer_variable_f32(ui, result, &ui->arena, s8("F#:"), s8(""), &bp->f_number,
    619 	                            (v2){.y = 1e3}, 1, 0.1, V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
    620 
    621 	add_beamformer_variable_b32(ui, result, &ui->arena, s8("Interpolate:"), s8("False"), s8("True"),
    622 	                            &bp->interpolate, V_INPUT|V_CAUSES_COMPUTE, ui->font);
    623 
    624 	return result;
    625 }
    626 
    627 static Variable *
    628 add_beamformer_frame_view(BeamformerUI *ui, Variable *parent, Arena *arena,
    629                           BeamformerFrameViewType type, b32 closable)
    630 {
    631 	/* TODO(rnp): this can be always closable once we have a way of opening new views */
    632 	Variable *result = add_ui_view(ui, parent, arena, s8(""), UI_VIEW_CUSTOM_TEXT, closable);
    633 	Variable *menu   = result->u.view.menu = add_variable_group(ui, 0, &ui->arena, s8(""),
    634 	                                                            VG_LIST, ui->small_font);
    635 	menu->flags  = V_MENU;
    636 	menu->parent = result;
    637 	#define X(id, text) add_button(ui, menu, &ui->arena, s8(text), UI_BID_ ##id, ui->small_font);
    638 	FRAME_VIEW_BUTTONS
    639 	#undef X
    640 
    641 	Variable *var = add_variable(ui, result, &ui->arena, s8(""), 0, VT_BEAMFORMER_FRAME_VIEW,
    642 	                             ui->small_font);
    643 
    644 	BeamformerFrameView *bv = SLLPop(ui->view_freelist);
    645 	if (bv) zero_struct(bv);
    646 	else    bv = push_struct(arena, typeof(*bv));
    647 	DLLPushDown(bv, ui->views);
    648 
    649 	var->u.generic = bv;
    650 
    651 	fill_variable(&bv->dynamic_range, var, s8("Dynamic Range:"), V_INPUT|V_TEXT|V_UPDATE_VIEW,
    652 	              VT_F32, ui->small_font);
    653 	fill_variable(&bv->threshold, var, s8("Threshold:"), V_INPUT|V_TEXT|V_UPDATE_VIEW,
    654 	              VT_F32, ui->small_font);
    655 
    656 	bv->type                = type;
    657 	bv->dynamic_range.u.f32 = -50.0f;
    658 	bv->threshold.u.f32     =  40.0f;
    659 
    660 	bv->lateral_scale_bar.limits              = (v2){.x = -1, .y = 1};
    661 	bv->axial_scale_bar.limits                = (v2){.x =  0, .y = 1};
    662 	bv->lateral_scale_bar.scroll_scale        = (v2){.x = -0.5e-3, .y = 0.5e-3};
    663 	bv->axial_scale_bar.scroll_scale          = (v2){.x =  0,      .y = 1e-3};
    664 	bv->lateral_scale_bar.zoom_starting_point = (v2){.x = F32_INFINITY, .y = F32_INFINITY};
    665 	bv->axial_scale_bar.zoom_starting_point   = (v2){.x = F32_INFINITY, .y = F32_INFINITY};
    666 
    667 	return result;
    668 }
    669 
    670 static Variable *
    671 add_compute_progress_bar(Variable *parent, BeamformerCtx *ctx)
    672 {
    673 	BeamformerUI *ui = ctx->ui;
    674 	/* TODO(rnp): this can be closable once we have a way of opening new views */
    675 	Variable *result = add_ui_view(ui, parent, &ui->arena, s8(""), UI_VIEW_CUSTOM_TEXT, 0);
    676 	add_variable(ui, result, &ui->arena, s8(""), 0, VT_COMPUTE_PROGRESS_BAR, ui->small_font);
    677 	ComputeProgressBar *bar = &result->u.group.first->u.compute_progress_bar;
    678 	bar->progress   = &ctx->csctx.processing_progress;
    679 	bar->processing = &ctx->csctx.processing_compute;
    680 
    681 	return result;
    682 }
    683 
    684 static Variable *
    685 add_compute_stats_view(BeamformerUI *ui, Variable *parent, Arena *arena, VariableType type)
    686 {
    687 	/* TODO(rnp): this can be closable once we have a way of opening new views */
    688 	Variable *result = add_ui_view(ui, parent, arena, s8(""), UI_VIEW_CUSTOM_TEXT, 0);
    689 	add_variable(ui, result, &ui->arena, s8(""), 0, type, ui->small_font);
    690 	return result;
    691 }
    692 
    693 static void
    694 ui_copy_frame(BeamformerUI *ui, Variable *button, RegionSplitDirection direction)
    695 {
    696 	Variable *menu   = button->parent;
    697 	Variable *view   = menu->parent;
    698 	Variable *region = view->parent;
    699 	ASSERT(region->type == VT_UI_REGION_SPLIT);
    700 	ASSERT(view->type   == VT_UI_VIEW);
    701 
    702 	BeamformerFrameView *old = view->u.group.first->u.generic;
    703 	/* TODO(rnp): hack; it would be better if this was unreachable with a 0 old->frame */
    704 	if (!old->frame)
    705 		return;
    706 
    707 	Variable *new_region = add_ui_split(ui, region, &ui->arena, s8(""), 0.5, direction, ui->small_font);
    708 
    709 	if (view == region->u.region_split.left) {
    710 		region->u.region_split.left  = new_region;
    711 	} else {
    712 		region->u.region_split.right = new_region;
    713 	}
    714 	view->parent = new_region;
    715 	new_region->u.region_split.left  = view;
    716 	new_region->u.region_split.right = add_beamformer_frame_view(ui, new_region, &ui->arena, FVT_COPY, 1);
    717 
    718 	BeamformerFrameView *bv = new_region->u.region_split.right->u.group.first->u.generic;
    719 	bv->lateral_scale_bar.min_value = &bv->min_coordinate.x;
    720 	bv->lateral_scale_bar.max_value = &bv->max_coordinate.x;
    721 	bv->axial_scale_bar.min_value   = &bv->min_coordinate.z;
    722 	bv->axial_scale_bar.max_value   = &bv->max_coordinate.z;
    723 
    724 	bv->ctx                 = old->ctx;
    725 	bv->needs_update        = 1;
    726 	bv->threshold.u.f32     = old->threshold.u.f32;
    727 	bv->dynamic_range.u.f32 = old->dynamic_range.u.f32;
    728 	bv->min_coordinate      = old->frame->min_coordinate;
    729 	bv->max_coordinate      = old->frame->max_coordinate;
    730 
    731 	bv->frame = SLLPop(ui->frame_freelist);
    732 	if (!bv->frame) bv->frame = push_struct(&ui->arena, typeof(*bv->frame));
    733 
    734 	ASSERT(old->frame->in_flight == 0);
    735 	mem_copy(bv->frame, old->frame, sizeof(*bv->frame));
    736 	bv->frame->texture = 0;
    737 	bv->frame->next    = 0;
    738 	alloc_beamform_frame(0, bv->frame, 0, old->frame->dim, s8("Frame Copy: "), ui->arena);
    739 	bv->frame->ready_to_present = 1;
    740 
    741 	glCopyImageSubData(old->frame->texture, GL_TEXTURE_3D, 0, 0, 0, 0,
    742 	                   bv->frame->texture,  GL_TEXTURE_3D, 0, 0, 0, 0,
    743 	                   bv->frame->dim.x, bv->frame->dim.y, bv->frame->dim.z);
    744 	glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT);
    745 	/* TODO(rnp): x vs y here */
    746 	resize_frame_view(bv, (uv2){.x = bv->frame->dim.x, .y = bv->frame->dim.z});
    747 }
    748 
    749 static b32
    750 view_update(BeamformerUI *ui, BeamformerFrameView *view)
    751 {
    752 	b32 needs_resize = 0;
    753 	uv2 current = {.w = view->rendered_view.texture.width, .h = view->rendered_view.texture.height};
    754 	uv2 target;
    755 
    756 	switch (view->type) {
    757 	case FVT_LATEST: {
    758 		/* TODO(rnp): x-z or y-z */
    759 		target = (uv2){.w = ui->params.output_points.x, .h = ui->params.output_points.z};
    760 		needs_resize = !uv2_equal(current, target) && !uv2_equal(target, (uv2){0});
    761 		view->needs_update |= view->frame != ui->latest_frame;
    762 		view->frame         = ui->latest_frame;
    763 	} break;
    764 	default: {
    765 		/* TODO(rnp): add method of setting a target size in frame view */
    766 		target = (uv2){.w = ui->params.output_points.x, .h = ui->params.output_points.z};
    767 		needs_resize = !uv2_equal(current, target) && !uv2_equal(target, (uv2){0});
    768 	} break;
    769 	}
    770 
    771 	if (needs_resize && view->frame) {
    772 		resize_frame_view(view, target);
    773 		view->needs_update = 1;
    774 	}
    775 
    776 	return (view->ctx->updated || view->needs_update) && view->frame;
    777 }
    778 
    779 static void
    780 update_frame_views(BeamformerUI *ui)
    781 {
    782 	for (BeamformerFrameView *view = ui->views; view; view = view->next) {
    783 		if (view_update(ui, view)) {
    784 			BeamformFrame *frame = view->frame;
    785 			BeginTextureMode(view->rendered_view);
    786 				ClearBackground(PINK);
    787 				BeginShaderMode(view->ctx->shader);
    788 					glUseProgram(view->ctx->shader.id);
    789 					glBindTextureUnit(0, frame->texture);
    790 					glUniform1f(view->ctx->db_cutoff_id, view->dynamic_range.u.f32);
    791 					glUniform1f(view->ctx->threshold_id, view->threshold.u.f32);
    792 					DrawTexture(view->rendered_view.texture, 0, 0, WHITE);
    793 				EndShaderMode();
    794 			EndTextureMode();
    795 			glGenerateTextureMipmap(view->rendered_view.texture.id);
    796 			view->needs_update = 0;
    797 		}
    798 	}
    799 }
    800 
    801 static b32
    802 frame_view_ready_to_present(BeamformerFrameView *view)
    803 {
    804 	uv2 render_size = {.w = view->rendered_view.texture.width,
    805 	                   .h = view->rendered_view.texture.height};
    806 	return !uv2_equal((uv2){0}, render_size);
    807 }
    808 
    809 static Color
    810 colour_from_normalized(v4 rgba)
    811 {
    812 	return (Color){.r = rgba.r * 255.0f, .g = rgba.g * 255.0f,
    813 	               .b = rgba.b * 255.0f, .a = rgba.a * 255.0f};
    814 }
    815 
    816 static Color
    817 fade(Color a, f32 visibility)
    818 {
    819 	a.a = (u8)((f32)a.a * visibility);
    820 	return a;
    821 }
    822 
    823 static v4
    824 lerp_v4(v4 a, v4 b, f32 t)
    825 {
    826 	return (v4){
    827 		.x = a.x + t * (b.x - a.x),
    828 		.y = a.y + t * (b.y - a.y),
    829 		.z = a.z + t * (b.z - a.z),
    830 		.w = a.w + t * (b.w - a.w),
    831 	};
    832 }
    833 
    834 static s8
    835 push_das_shader_id(Stream *s, DASShaderID shader, u32 transmit_count)
    836 {
    837 	#define X(type, id, pretty, fixed_tx) s8(pretty),
    838 	static s8 pretty_names[] = { DAS_TYPES };
    839 	#undef X
    840 	#define X(type, id, pretty, fixed_tx) fixed_tx,
    841 	static u8 fixed_transmits[] = { DAS_TYPES };
    842 	#undef X
    843 
    844 	if ((u32)shader < (u32)DAS_LAST) {
    845 		stream_append_s8(s, pretty_names[shader]);
    846 		if (!fixed_transmits[shader]) {
    847 			stream_append_byte(s, '-');
    848 			stream_append_u64(s, transmit_count);
    849 		}
    850 	}
    851 
    852 	return stream_to_s8(s);
    853 }
    854 
    855 static s8
    856 push_custom_view_title(Stream *s, Variable *var)
    857 {
    858 	switch (var->type) {
    859 	case VT_COMPUTE_STATS_VIEW:
    860 	case VT_COMPUTE_LATEST_STATS_VIEW: {
    861 		stream_append_s8(s, s8("Compute Stats"));
    862 		if (var->type == VT_COMPUTE_LATEST_STATS_VIEW)
    863 			stream_append_s8(s, s8(": Live"));
    864 	} break;
    865 	case VT_COMPUTE_PROGRESS_BAR: {
    866 		stream_append_s8(s, s8("Compute Progress: "));
    867 		stream_append_f64(s, 100 * *var->u.compute_progress_bar.progress, 100);
    868 		stream_append_byte(s, '%');
    869 	} break;
    870 	case VT_BEAMFORMER_FRAME_VIEW: {
    871 		BeamformerFrameView *bv = var->u.generic;
    872 		stream_append_s8(s, s8("Frame View"));
    873 		switch (bv->type) {
    874 		case FVT_LATEST:  stream_append_s8(s, s8(": Live [")); break;
    875 		case FVT_COPY:    stream_append_s8(s, s8(": Copy [")); break;
    876 		case FVT_INDEXED: stream_append_s8(s, s8(": ["));      break;
    877 		}
    878 		stream_append_hex_u64(s, bv->frame? bv->frame->id : 0);
    879 		stream_append_byte(s, ']');
    880 	} break;
    881 	default: INVALID_CODE_PATH;
    882 	}
    883 	return stream_to_s8(s);
    884 }
    885 
    886 static v2
    887 draw_text_base(Font font, s8 text, v2 pos, Color colour)
    888 {
    889 	v2 off = pos;
    890 	for (iz i = 0; i < text.len; i++) {
    891 		/* NOTE: assumes font glyphs are ordered ASCII */
    892 		i32 idx = text.data[i] - 0x20;
    893 		Rectangle dst = {
    894 			off.x + font.glyphs[idx].offsetX - font.glyphPadding,
    895 			off.y + font.glyphs[idx].offsetY - font.glyphPadding,
    896 			font.recs[idx].width  + 2.0f * font.glyphPadding,
    897 			font.recs[idx].height + 2.0f * font.glyphPadding
    898 		};
    899 		Rectangle src = {
    900 			font.recs[idx].x - font.glyphPadding,
    901 			font.recs[idx].y - font.glyphPadding,
    902 			font.recs[idx].width  + 2.0f * font.glyphPadding,
    903 			font.recs[idx].height + 2.0f * font.glyphPadding
    904 		};
    905 		DrawTexturePro(font.texture, src, dst, (Vector2){0}, 0, colour);
    906 
    907 		off.x += font.glyphs[idx].advanceX;
    908 		if (font.glyphs[idx].advanceX == 0)
    909 			off.x += font.recs[idx].width;
    910 	}
    911 	v2 result = {.x = off.x - pos.x, .y = font.baseSize};
    912 	return result;
    913 }
    914 
    915 /* NOTE(rnp): expensive but of the available options in raylib this gives the best results */
    916 static v2
    917 draw_outlined_text(s8 text, v2 pos, TextSpec *ts)
    918 {
    919 	f32 ow = ts->outline_thick;
    920 	draw_text_base(*ts->font, text, sub_v2(pos, (v2){.x =  ow, .y =  ow}), ts->outline_colour);
    921 	draw_text_base(*ts->font, text, sub_v2(pos, (v2){.x =  ow, .y = -ow}), ts->outline_colour);
    922 	draw_text_base(*ts->font, text, sub_v2(pos, (v2){.x = -ow, .y =  ow}), ts->outline_colour);
    923 	draw_text_base(*ts->font, text, sub_v2(pos, (v2){.x = -ow, .y = -ow}), ts->outline_colour);
    924 
    925 	v2 result = draw_text_base(*ts->font, text, pos, ts->colour);
    926 
    927 	return result;
    928 }
    929 
    930 static v2
    931 draw_text(s8 text, v2 pos, TextSpec *ts)
    932 {
    933 	if (ts->flags & TF_ROTATED) {
    934 		rlPushMatrix();
    935 		rlTranslatef(pos.x, pos.y, 0);
    936 		rlRotatef(ts->rotation, 0, 0, 1);
    937 		pos = (v2){0};
    938 	}
    939 
    940 	v2 result   = measure_text(*ts->font, text);
    941 	s8 ellipsis = s8("...");
    942 	b32 clamped = ts->flags & TF_LIMITED && result.w > ts->limits.size.w;
    943 	if (clamped) {
    944 		f32 elipsis_width = measure_text(*ts->font, ellipsis).x;
    945 		if (elipsis_width < ts->limits.size.w) {
    946 			/* TODO(rnp): there must be a better way */
    947 			while (text.len) {
    948 				text.len--;
    949 				f32 width = measure_text(*ts->font, text).x;
    950 				if (width + elipsis_width < ts->limits.size.w)
    951 					break;
    952 			}
    953 		} else {
    954 			text.len     = 0;
    955 			ellipsis.len = 0;
    956 		}
    957 	}
    958 
    959 	if (ts->flags & TF_OUTLINED) result.x = draw_outlined_text(text, pos, ts).x;
    960 	else                         result.x = draw_text_base(*ts->font, text, pos, ts->colour).x;
    961 
    962 	if (clamped) {
    963 		pos.x += result.x;
    964 		if (ts->flags & TF_OUTLINED) result.x += draw_outlined_text(ellipsis, pos, ts).x;
    965 		else                         result.x += draw_text_base(*ts->font, ellipsis, pos,
    966 		                                                        ts->colour).x;
    967 	}
    968 
    969 	if (ts->flags & TF_ROTATED) rlPopMatrix();
    970 
    971 	return result;
    972 }
    973 
    974 static Rect
    975 extend_rect_centered(Rect r, v2 delta)
    976 {
    977 	r.size.w += delta.x;
    978 	r.size.h += delta.y;
    979 	r.pos.x  -= delta.x / 2;
    980 	r.pos.y  -= delta.y / 2;
    981 	return r;
    982 }
    983 
    984 static Rect
    985 shrink_rect_centered(Rect r, v2 delta)
    986 {
    987 	delta.x   = MIN(delta.x, r.size.w);
    988 	delta.y   = MIN(delta.y, r.size.h);
    989 	r.size.w -= delta.x;
    990 	r.size.h -= delta.y;
    991 	r.pos.x  += delta.x / 2;
    992 	r.pos.y  += delta.y / 2;
    993 	return r;
    994 }
    995 
    996 static Rect
    997 scale_rect_centered(Rect r, v2 scale)
    998 {
    999 	Rect or   = r;
   1000 	r.size.w *= scale.x;
   1001 	r.size.h *= scale.y;
   1002 	r.pos.x  += (or.size.w - r.size.w) / 2;
   1003 	r.pos.y  += (or.size.h - r.size.h) / 2;
   1004 	return r;
   1005 }
   1006 
   1007 static b32
   1008 hover_rect(v2 mouse, Rect rect, f32 *hover_t)
   1009 {
   1010 	b32 hovering = CheckCollisionPointRec(mouse.rl, rect.rl);
   1011 	if (hovering) *hover_t += HOVER_SPEED * dt_for_frame;
   1012 	else          *hover_t -= HOVER_SPEED * dt_for_frame;
   1013 	*hover_t = CLAMP01(*hover_t);
   1014 	return hovering;
   1015 }
   1016 
   1017 static b32
   1018 hover_var(BeamformerUI *ui, v2 mouse, Rect rect, Variable *var)
   1019 {
   1020 	b32 result = hover_rect(mouse, rect, &var->hover_t);
   1021 	if (result) {
   1022 		ui->interaction.hot_type = IT_NONE;
   1023 		ui->interaction.hot      = var;
   1024 	}
   1025 	return result;
   1026 }
   1027 
   1028 static Rect
   1029 draw_title_bar(BeamformerUI *ui, Arena arena, Variable *ui_view, Rect r, v2 mouse)
   1030 {
   1031 	ASSERT(ui_view->type == VT_UI_VIEW);
   1032 	UIView *view = &ui_view->u.view;
   1033 
   1034 	s8 title = ui_view->name;
   1035 	if (view->flags & UI_VIEW_CUSTOM_TEXT) {
   1036 		Stream buf = arena_stream(&arena);
   1037 		title      = push_custom_view_title(&buf, ui_view->u.group.first);
   1038 	}
   1039 
   1040 	Rect result, title_rect;
   1041 	cut_rect_vertical(r, ui->small_font.baseSize + TITLE_BAR_PAD, &title_rect, &result);
   1042 	cut_rect_vertical(result, LISTING_LINE_PAD, 0, &result);
   1043 
   1044 	DrawRectangleRec(title_rect.rl, BLACK);
   1045 
   1046 	title_rect = shrink_rect_centered(title_rect, (v2){.x = 1.5 * TITLE_BAR_PAD});
   1047 	DrawRectangleRounded(title_rect.rl, 0.5, 0, fade(colour_from_normalized(BG_COLOUR), 0.55));
   1048 	title_rect = shrink_rect_centered(title_rect, (v2){.x = 3 * TITLE_BAR_PAD});
   1049 
   1050 	if (view->close) {
   1051 		Rect close;
   1052 		cut_rect_horizontal(title_rect, title_rect.size.w - title_rect.size.h, &title_rect, &close);
   1053 		hover_var(ui, mouse, close, view->close);
   1054 
   1055 		Color colour = colour_from_normalized(lerp_v4(MENU_CLOSE_COLOUR, FG_COLOUR, view->close->hover_t));
   1056 		close = shrink_rect_centered(close, (v2){.x = 16, .y = 16});
   1057 		DrawLineEx(close.pos.rl, add_v2(close.pos, close.size).rl, 4, colour);
   1058 		DrawLineEx(add_v2(close.pos, (v2){.x = close.size.w}).rl,
   1059 		           add_v2(close.pos, (v2){.y = close.size.h}).rl,  4, colour);
   1060 	}
   1061 
   1062 	if (view->menu) {
   1063 		Rect menu;
   1064 		cut_rect_horizontal(title_rect, title_rect.size.w - title_rect.size.h, &title_rect, &menu);
   1065 		if (hover_var(ui, mouse, menu, view->menu))
   1066 			ui->menu_state.hot_font = &ui->small_font;
   1067 
   1068 		Color colour = colour_from_normalized(lerp_v4(MENU_PLUS_COLOUR, FG_COLOUR, view->menu->hover_t));
   1069 		menu = shrink_rect_centered(menu, (v2){.x = 14, .y = 14});
   1070 		DrawLineEx(add_v2(menu.pos, (v2){.x = menu.size.w / 2}).rl,
   1071 		           add_v2(menu.pos, (v2){.x = menu.size.w / 2, .y = menu.size.h}).rl, 4, colour);
   1072 		DrawLineEx(add_v2(menu.pos, (v2){.y = menu.size.h / 2}).rl,
   1073 		           add_v2(menu.pos, (v2){.x = menu.size.w, .y = menu.size.h / 2}).rl, 4, colour);
   1074 	}
   1075 
   1076 	/* TODO(rnp): hover the title text rect and use it to access the global menu */
   1077 	v2 title_pos = title_rect.pos;
   1078 	title_pos.y += 0.5 * TITLE_BAR_PAD;
   1079 	TextSpec text_spec = {.font = &ui->small_font, .flags = TF_LIMITED,
   1080 	                      .colour = NORMALIZED_FG_COLOUR, .limits.size = title_rect.size};
   1081 	draw_text(title, title_pos, &text_spec);
   1082 
   1083 	return result;
   1084 }
   1085 
   1086 /* TODO(rnp): once this has more callers decide if it would be better for this to take
   1087  * an orientation rather than force CCW/right-handed */
   1088 static void
   1089 draw_ruler(BeamformerUI *ui, Stream *buf, v2 start_point, v2 end_point,
   1090            f32 start_value, f32 end_value, f32 *markers, u32 marker_count,
   1091            u32 segments, s8 suffix, Color ruler_colour, Color txt_colour)
   1092 {
   1093 	b32 draw_plus = SIGN(start_value) != SIGN(end_value);
   1094 
   1095 	end_point    = sub_v2(end_point, start_point);
   1096 	f32 rotation = atan2_f32(end_point.y, end_point.x) * 180 / PI;
   1097 
   1098 	rlPushMatrix();
   1099 	rlTranslatef(start_point.x, start_point.y, 0);
   1100 	rlRotatef(rotation, 0, 0, 1);
   1101 
   1102 	f32 inc       = magnitude_v2(end_point) / segments;
   1103 	f32 value_inc = (end_value - start_value) / segments;
   1104 	f32 value     = start_value;
   1105 
   1106 	v2 sp = {0}, ep = {.y = RULER_TICK_LENGTH};
   1107 	v2 tp = {.x = ui->small_font.baseSize / 2, .y = ep.y + RULER_TEXT_PAD};
   1108 	TextSpec text_spec = {.font = &ui->small_font, .rotation = 90, .colour = txt_colour, .flags = TF_ROTATED};
   1109 	for (u32 j = 0; j <= segments; j++) {
   1110 		DrawLineEx(sp.rl, ep.rl, 3, ruler_colour);
   1111 
   1112 		stream_reset(buf, 0);
   1113 		if (draw_plus && value > 0) stream_append_byte(buf, '+');
   1114 		stream_append_f64(buf, value, 10);
   1115 		stream_append_s8(buf, suffix);
   1116 		draw_text(stream_to_s8(buf), tp, &text_spec);
   1117 
   1118 		value += value_inc;
   1119 		sp.x  += inc;
   1120 		ep.x  += inc;
   1121 		tp.x  += inc;
   1122 	}
   1123 
   1124 	ep.y += RULER_TICK_LENGTH;
   1125 	for (u32 i = 0; i < marker_count; i++) {
   1126 		if (markers[i] < F32_INFINITY) {
   1127 			ep.x  = sp.x = markers[i];
   1128 			DrawLineEx(sp.rl, ep.rl, 3, colour_from_normalized(RULER_COLOUR));
   1129 			DrawCircleV(ep.rl, 3, colour_from_normalized(RULER_COLOUR));
   1130 		}
   1131 	}
   1132 
   1133 	rlPopMatrix();
   1134 }
   1135 
   1136 static void
   1137 do_scale_bar(BeamformerUI *ui, Stream *buf, ScaleBar *sb, ScaleBarDirection direction,
   1138              v2 mouse, Rect draw_rect, f32 start_value, f32 end_value, s8 suffix)
   1139 {
   1140 	InteractionState *is = &ui->interaction;
   1141 
   1142 	v2 txt_s = measure_text(ui->small_font, s8("-288.8 mm"));
   1143 
   1144 	Rect tick_rect = draw_rect;
   1145 	v2   start_pos = tick_rect.pos;
   1146 	v2   end_pos   = tick_rect.pos;
   1147 	v2   relative_mouse = sub_v2(mouse, tick_rect.pos);
   1148 
   1149 	f32  markers[2];
   1150 	u32  marker_count = 1;
   1151 
   1152 	u32  tick_count;
   1153 	if (direction == SB_AXIAL) {
   1154 		tick_rect.size.x  = RULER_TEXT_PAD + RULER_TICK_LENGTH + txt_s.x;
   1155 		tick_count        = tick_rect.size.y / (1.5 * ui->small_font.baseSize);
   1156 		start_pos.y      += tick_rect.size.y;
   1157 		markers[0]        = tick_rect.size.y - sb->zoom_starting_point.y;
   1158 		markers[1]        = tick_rect.size.y - relative_mouse.y;
   1159 		sb->screen_offset = (v2){.y = tick_rect.pos.y};
   1160 		sb->screen_space_to_value = (v2){.y = (*sb->max_value - *sb->min_value) / tick_rect.size.y};
   1161 	} else {
   1162 		tick_rect.size.y  = RULER_TEXT_PAD + RULER_TICK_LENGTH + txt_s.x;
   1163 		tick_count        = tick_rect.size.x / (1.5 * ui->small_font.baseSize);
   1164 		end_pos.x        += tick_rect.size.x;
   1165 		markers[0]        = sb->zoom_starting_point.x;
   1166 		markers[1]        = relative_mouse.x;
   1167 		/* TODO(rnp): screen space to value space transformation helper */
   1168 		sb->screen_offset = (v2){.x = tick_rect.pos.x};
   1169 		sb->screen_space_to_value = (v2){.x = (*sb->max_value - *sb->min_value) / tick_rect.size.x};
   1170 	}
   1171 
   1172 	if (hover_rect(mouse, tick_rect, &sb->hover_t)) {
   1173 		Variable *var  = zero_struct(ui->scratch_variable);
   1174 		var->u.generic = sb;
   1175 		var->type      = VT_SCALE_BAR;
   1176 		is->hot_type   = IT_SCALE_BAR;
   1177 		is->hot        = var;
   1178 
   1179 		marker_count  = 2;
   1180 	}
   1181 
   1182 	draw_ruler(ui, buf, start_pos, end_pos, start_value, end_value, markers, marker_count,
   1183 	           tick_count, suffix, colour_from_normalized(FG_COLOUR),
   1184 	           colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, sb->hover_t)));
   1185 }
   1186 
   1187 static v2
   1188 draw_beamformer_variable(BeamformerUI *ui, Arena arena, Variable *var, v2 at, v2 mouse,
   1189                          v4 base_colour, TextSpec text_spec)
   1190 {
   1191 	Stream buf = arena_stream(&arena);
   1192 	stream_append_variable(&buf, var);
   1193 
   1194 	v2 text_size = measure_text(*text_spec.font, stream_to_s8(&buf));
   1195 	if (var->flags & V_INPUT) {
   1196 		Rect text_rect = {.pos = at, .size = text_size};
   1197 		text_rect = extend_rect_centered(text_rect, (v2){.x = 8});
   1198 
   1199 		if (hover_var(ui, mouse, text_rect, var) && (var->flags & V_TEXT)) {
   1200 			InputState *is = &ui->text_input_state;
   1201 			is->hot_rect   = text_rect;
   1202 			is->hot_font   = text_spec.font;
   1203 		}
   1204 
   1205 		text_spec.colour = colour_from_normalized(lerp_v4(base_colour, HOVERED_COLOUR, var->hover_t));
   1206 	}
   1207 	return draw_text(stream_to_s8(&buf), at, &text_spec);
   1208 }
   1209 
   1210 static void
   1211 draw_beamformer_frame_view(BeamformerUI *ui, Arena a, Variable *var, Rect display_rect, v2 mouse)
   1212 {
   1213 	ASSERT(var->type == VT_BEAMFORMER_FRAME_VIEW);
   1214 	BeamformerUIParameters *bp = &ui->params;
   1215 	InteractionState *is       = &ui->interaction;
   1216 	BeamformerFrameView *view  = var->u.generic;
   1217 	BeamformFrame *frame       = view->frame;
   1218 
   1219 	v2 txt_s = measure_text(ui->small_font, s8("-288.8 mm"));
   1220 
   1221 	f32 pad    = 1.2 * txt_s.x + RULER_TICK_LENGTH;
   1222 	Rect vr    = display_rect;
   1223 	vr.pos.x  += 0.5 * ui->small_font.baseSize;
   1224 	vr.pos.y  += 0.5 * ui->small_font.baseSize;
   1225 	vr.size.h  = MAX(0, display_rect.size.h - pad);
   1226 	vr.size.w  = MAX(0, display_rect.size.w - pad);
   1227 
   1228 	/* TODO(rnp): ideally we hook up both versions to view->min/max */
   1229 	v4 min = (view->type == FVT_LATEST)? bp->output_min_coordinate : view->min_coordinate;
   1230 	v4 max = (view->type == FVT_LATEST)? bp->output_max_coordinate : view->max_coordinate;
   1231 
   1232 	/* TODO(rnp): make this depend on the requested draw orientation (x-z or y-z or x-y) */
   1233 	v2 output_dim = {
   1234 		.x = frame->max_coordinate.x - frame->min_coordinate.x,
   1235 		.y = frame->max_coordinate.z - frame->min_coordinate.z,
   1236 	};
   1237 	v2 requested_dim = {.x = max.x - min.x, .y = max.z - min.z};
   1238 
   1239 	f32 aspect = requested_dim.h / requested_dim.w;
   1240 	if (display_rect.size.h < (vr.size.w * aspect) + pad) {
   1241 		vr.size.w = vr.size.h / aspect;
   1242 	} else {
   1243 		vr.size.h = vr.size.w * aspect;
   1244 	}
   1245 	vr.pos.x += (display_rect.size.w - (vr.size.w + pad)) / 2;
   1246 	vr.pos.y += (display_rect.size.h - (vr.size.h + pad)) / 2;
   1247 
   1248 	Texture *output = &view->rendered_view.texture;
   1249 	v2 pixels_per_meter = {
   1250 		.w = (f32)output->width  / output_dim.w,
   1251 		.h = (f32)output->height / output_dim.h,
   1252 	};
   1253 
   1254 	v2 texture_points  = mul_v2(pixels_per_meter, requested_dim);
   1255 	/* TODO(rnp): this also depends on x-y, y-z, x-z */
   1256 	v2 texture_start   = {
   1257 		.x = pixels_per_meter.x * 0.5 * (output_dim.x - requested_dim.x),
   1258 		.y = pixels_per_meter.y * (frame->max_coordinate.z - max.z),
   1259 	};
   1260 
   1261 	Rectangle  tex_r  = {texture_start.x, texture_start.y, texture_points.x, -texture_points.y};
   1262 	NPatchInfo tex_np = { tex_r, 0, 0, 0, 0, NPATCH_NINE_PATCH };
   1263 	DrawTextureNPatch(*output, tex_np, vr.rl, (Vector2){0}, 0, WHITE);
   1264 
   1265 	v2 start_pos  = vr.pos;
   1266 	start_pos.y  += vr.size.y;
   1267 
   1268 	if (vr.size.w > 0) {
   1269 		Arena  tmp = a;
   1270 		Stream buf = arena_stream(&tmp);
   1271 		do_scale_bar(ui, &buf, &view->lateral_scale_bar, SB_LATERAL, mouse,
   1272 		             (Rect){.pos = start_pos, .size = vr.size},
   1273 		             *view->lateral_scale_bar.min_value * 1e3,
   1274 		             *view->lateral_scale_bar.max_value * 1e3, s8(" mm"));
   1275 	}
   1276 
   1277 	start_pos    = vr.pos;
   1278 	start_pos.x += vr.size.x;
   1279 
   1280 	if (vr.size.h > 0) {
   1281 		Arena  tmp = a;
   1282 		Stream buf = arena_stream(&tmp);
   1283 		do_scale_bar(ui, &buf, &view->axial_scale_bar, SB_AXIAL, mouse,
   1284 		             (Rect){.pos = start_pos, .size = vr.size},
   1285 		             *view->axial_scale_bar.max_value * 1e3,
   1286 		             *view->axial_scale_bar.min_value * 1e3, s8(" mm"));
   1287 	}
   1288 
   1289 	v2 pixels_to_mm = output_dim;
   1290 	pixels_to_mm.x /= vr.size.x * 1e-3;
   1291 	pixels_to_mm.y /= vr.size.y * 1e-3;
   1292 
   1293 	TextSpec text_spec = {.font = &ui->small_font, .flags = TF_LIMITED|TF_OUTLINED,
   1294 	                      .colour = colour_from_normalized(RULER_COLOUR),
   1295 	                      .outline_thick = 1, .outline_colour = BLACK,
   1296 	                      .limits.size.x = vr.size.w};
   1297 
   1298 	b32 drew_coordinates = 0;
   1299 	f32 remaining_width  = vr.size.w;
   1300 	if (CheckCollisionPointRec(mouse.rl, vr.rl)) {
   1301 		is->hot_type  = IT_DISPLAY;
   1302 		is->hot       = var;
   1303 
   1304 		v2 relative_mouse = sub_v2(mouse, vr.pos);
   1305 		v2 mm = mul_v2(relative_mouse, pixels_to_mm);
   1306 		mm.x += 1e3 * min.x;
   1307 		mm.y += 1e3 * min.z;
   1308 
   1309 		Arena  tmp = a;
   1310 		Stream buf = arena_stream(&tmp);
   1311 		stream_append_v2(&buf, mm);
   1312 
   1313 		text_spec.limits.size.w -= 4;
   1314 		v2 txt_s = measure_text(*text_spec.font, stream_to_s8(&buf));
   1315 		v2 txt_p = {
   1316 			.x = vr.pos.x + vr.size.w - txt_s.w - 4,
   1317 			.y = vr.pos.y + vr.size.h - txt_s.h - 4,
   1318 		};
   1319 		txt_p.x = MAX(vr.pos.x, txt_p.x);
   1320 		remaining_width -= draw_text(stream_to_s8(&buf), txt_p, &text_spec).w;
   1321 		text_spec.limits.size.w += 4;
   1322 		drew_coordinates = 1;
   1323 	}
   1324 
   1325 	{
   1326 		Arena  tmp = a;
   1327 		Stream buf = arena_stream(&tmp);
   1328 		s8 shader  = push_das_shader_id(&buf, frame->das_shader_id, frame->compound_count);
   1329 		text_spec.font = &ui->font;
   1330 		text_spec.limits.size.w -= 16;
   1331 		v2 txt_s   = measure_text(*text_spec.font, shader);
   1332 		v2 txt_p  = {
   1333 			.x = vr.pos.x + vr.size.w - txt_s.w - 16,
   1334 			.y = vr.pos.y + 4,
   1335 		};
   1336 		txt_p.x = MAX(vr.pos.x, txt_p.x);
   1337 		draw_text(stream_to_s8(&buf), txt_p, &text_spec);
   1338 		text_spec.font = &ui->small_font;
   1339 		text_spec.limits.size.w += 16;
   1340 	}
   1341 
   1342 	/* TODO(rnp): store converted ruler points instead of screen points */
   1343 	if (ui->ruler_state != RS_NONE && CheckCollisionPointRec(ui->ruler_start_p.rl, vr.rl)) {
   1344 		v2 end_p;
   1345 		if (ui->ruler_state == RS_START) end_p = mouse;
   1346 		else                             end_p = ui->ruler_stop_p;
   1347 
   1348 		end_p          = clamp_v2_rect(end_p, vr);
   1349 		v2 pixel_delta = sub_v2(ui->ruler_start_p, end_p);
   1350 		v2 mm_delta    = mul_v2(pixels_to_mm, pixel_delta);
   1351 
   1352 		DrawCircleV(ui->ruler_start_p.rl, 3, text_spec.colour);
   1353 		DrawLineEx(end_p.rl, ui->ruler_start_p.rl, 2, text_spec.colour);
   1354 		DrawCircleV(end_p.rl, 3, text_spec.colour);
   1355 
   1356 		Arena  tmp = a;
   1357 		Stream buf = arena_stream(&tmp);
   1358 		stream_append_f64(&buf, magnitude_v2(mm_delta), 100);
   1359 		stream_append_s8(&buf, s8(" mm"));
   1360 
   1361 		v2 txt_p = ui->ruler_start_p;
   1362 		v2 txt_s = measure_text(*text_spec.font, stream_to_s8(&buf));
   1363 		if (pixel_delta.y < 0) txt_p.y -= txt_s.y;
   1364 		if (pixel_delta.x < 0) txt_p.x -= txt_s.x;
   1365 		draw_text(stream_to_s8(&buf), txt_p, &text_spec);
   1366 	}
   1367 
   1368 	if (remaining_width > view->dynamic_range.name_width || !drew_coordinates) {
   1369 		f32 max_prefix_width = MAX(view->threshold.name_width, view->dynamic_range.name_width);
   1370 
   1371 		v2  end     = add_v2(vr.pos, vr.size);
   1372 		f32 start_y = MAX(end.y - 4 - 2 * text_spec.font->baseSize, vr.pos.y);
   1373 		end.y -= text_spec.font->baseSize;
   1374 		v2 at = {.x = vr.pos.x + 4, .y = start_y};
   1375 
   1376 		at.y += draw_text(view->dynamic_range.name, at, &text_spec).y;
   1377 		if (at.y < end.y) at.y += draw_text(view->threshold.name, at, &text_spec).y;
   1378 
   1379 		at.y  = start_y;
   1380 		at.x += max_prefix_width + 8;
   1381 		text_spec.limits.size.x = end.x - at.x;
   1382 
   1383 		v2 size = draw_beamformer_variable(ui, a, &view->dynamic_range, at, mouse,
   1384 		                                   RULER_COLOUR, text_spec);
   1385 
   1386 		f32 max_center_width = size.w;
   1387 		at.y += size.h;
   1388 
   1389 		if (at.y < end.y) {
   1390 			size = draw_beamformer_variable(ui, a, &view->threshold, at, mouse,
   1391 			                                RULER_COLOUR, text_spec);
   1392 			max_center_width = MAX(size.w, max_center_width);
   1393 		}
   1394 
   1395 		at.y  = start_y;
   1396 		at.x += max_center_width + 8;
   1397 		text_spec.limits.size.x = end.x - at.x;
   1398 		draw_text(s8(" [dB]"), at, &text_spec);
   1399 	}
   1400 }
   1401 
   1402 static v2
   1403 draw_compute_progress_bar(BeamformerUI *ui, Arena arena, ComputeProgressBar *state, Rect r)
   1404 {
   1405 	if (*state->processing) state->display_t_velocity += 65 * dt_for_frame;
   1406 	else                    state->display_t_velocity -= 45 * dt_for_frame;
   1407 
   1408 	state->display_t_velocity = CLAMP(state->display_t_velocity, -10, 10);
   1409 	state->display_t += state->display_t_velocity * dt_for_frame;
   1410 	state->display_t  = CLAMP01(state->display_t);
   1411 
   1412 	if (state->display_t > (1.0 / 255.0)) {
   1413 		Rect outline = {.pos = r.pos, .size = {.w = r.size.w, .h = ui->font.baseSize}};
   1414 		outline      = scale_rect_centered(outline, (v2){.x = 0.96, .y = 0.7});
   1415 		Rect filled  = outline;
   1416 		filled.size.w *= *state->progress;
   1417 		DrawRectangleRounded(filled.rl, 2, 0, fade(colour_from_normalized(HOVERED_COLOUR),
   1418 		                                           state->display_t));
   1419 		DrawRectangleRoundedLinesEx(outline.rl, 2, 0, 3, fade(BLACK, state->display_t));
   1420 	}
   1421 
   1422 	v2 result = {.x = r.size.w, .y = ui->font.baseSize};
   1423 	return result;
   1424 }
   1425 
   1426 static v2
   1427 draw_compute_stats_view(BeamformerCtx *ctx, Arena arena, ComputeShaderStats *stats, Rect r)
   1428 {
   1429 	static s8 labels[CS_LAST] = {
   1430 		#define X(e, n, s, h, pn) [CS_##e] = s8(pn ":"),
   1431 		COMPUTE_SHADERS
   1432 		#undef X
   1433 	};
   1434 
   1435 	BeamformerUI *ui        = ctx->ui;
   1436 	s8  compute_total       = s8("Compute Total:");
   1437 	f32 compute_total_width = measure_text(ui->font, compute_total).w;
   1438 	f32 max_label_width     = compute_total_width;
   1439 
   1440 	f32 *label_widths = alloc(&arena, f32, ARRAY_COUNT(labels));
   1441 	for (u32 i = 0; i < ARRAY_COUNT(labels); i++) {
   1442 		label_widths[i] = measure_text(ui->font, labels[i]).x;
   1443 		max_label_width = MAX(label_widths[i], max_label_width);
   1444 	}
   1445 
   1446 	v2 at = r.pos;
   1447 	Stream buf = stream_alloc(&arena, 64);
   1448 	f32 compute_time_sum = 0;
   1449 	u32 stages = ctx->params->compute_stages_count;
   1450 	TextSpec text_spec = {.font = &ui->font, .colour = NORMALIZED_FG_COLOUR, .flags = TF_LIMITED};
   1451 	for (u32 i = 0; i < stages; i++) {
   1452 		u32 index  = ctx->params->compute_stages[i];
   1453 		text_spec.limits.size.x = r.size.w;
   1454 		draw_text(labels[index], at, &text_spec);
   1455 		text_spec.limits.size.x -= LISTING_ITEM_PAD + max_label_width;
   1456 
   1457 		stream_reset(&buf, 0);
   1458 		stream_append_f64_e(&buf, stats->times[index]);
   1459 		stream_append_s8(&buf, s8(" [s]"));
   1460 		v2 rpos = {.x = r.pos.x + max_label_width + LISTING_ITEM_PAD, .y = at.y};
   1461 		at.y += draw_text(stream_to_s8(&buf), rpos, &text_spec).h;
   1462 
   1463 		compute_time_sum += stats->times[index];
   1464 	}
   1465 
   1466 	stream_reset(&buf, 0);
   1467 	stream_append_f64_e(&buf, compute_time_sum);
   1468 	stream_append_s8(&buf, s8(" [s]"));
   1469 	v2 rpos = {.x = at.x + max_label_width + LISTING_ITEM_PAD, .y = at.y};
   1470 	text_spec.limits.size.w = r.size.w;
   1471 	draw_text(compute_total, at, &text_spec);
   1472 	text_spec.limits.size.w -= LISTING_ITEM_PAD - max_label_width;
   1473 	at.y -= draw_text(stream_to_s8(&buf), rpos, &text_spec).h;
   1474 
   1475 	v2 result = {.x = r.size.w, .y = at.y - r.pos.y};
   1476 	return result;
   1477 }
   1478 
   1479 static void
   1480 draw_ui_view(BeamformerUI *ui, Variable *ui_view, Rect r, v2 mouse)
   1481 {
   1482 	ASSERT(ui_view->type == VT_UI_VIEW);
   1483 	UIView *view = &ui_view->u.view;
   1484 
   1485 	/* TODO(rnp): this should get moved up to draw_variable */
   1486 	hover_var(ui, mouse, r, ui_view);
   1487 
   1488 	if (view->needed_height - r.size.h < view->offset)
   1489 		view->offset = view->needed_height - r.size.h;
   1490 
   1491 	if (view->needed_height - r.size.h < 0)
   1492 		view->offset = 0;
   1493 
   1494 	r.pos.y -= view->offset;
   1495 
   1496 	f32 start_height = r.size.h;
   1497 	Variable *var    = view->group.first;
   1498 	f32 x_off        = view->group.max_name_width;
   1499 	TextSpec text_spec = {.font = &ui->font, .colour = NORMALIZED_FG_COLOUR, .flags = TF_LIMITED};
   1500 	while (var) {
   1501 		s8 suffix   = {0};
   1502 		v2 at       = r.pos;
   1503 		f32 advance = 0;
   1504 		switch (var->type) {
   1505 		case VT_BEAMFORMER_VARIABLE: {
   1506 			BeamformerVariable *bv = &var->u.beamformer_variable;
   1507 
   1508 			suffix   = bv->suffix;
   1509 	                text_spec.limits.size.w = r.size.w + r.pos.x - at.x;
   1510 			advance  = draw_text(var->name, at, &text_spec).y;
   1511 			at.x    += x_off + LISTING_ITEM_PAD;
   1512 	                text_spec.limits.size.w = r.size.w + r.pos.x - at.x;
   1513 			at.x    += draw_beamformer_variable(ui, ui->arena, var, at, mouse,
   1514 			                                    FG_COLOUR, text_spec).x;
   1515 
   1516 			while (var) {
   1517 				if (var->next) {
   1518 					var = var->next;
   1519 					break;
   1520 				}
   1521 				var       = var->parent;
   1522 				r.pos.x  -= LISTING_INDENT;
   1523 				r.size.x += LISTING_INDENT;
   1524 				x_off    += LISTING_INDENT;
   1525 			}
   1526 		} break;
   1527 		case VT_GROUP: {
   1528 			VariableGroup *g = &var->u.group;
   1529 
   1530 	                text_spec.limits.size.w = r.size.w + r.pos.x - at.x;
   1531 			advance  = draw_beamformer_variable(ui, ui->arena, var, at, mouse,
   1532 			                                    FG_COLOUR, text_spec).y;
   1533 			at.x    += x_off + LISTING_ITEM_PAD;
   1534 			if (g->expanded) {
   1535 				cut_rect_horizontal(r, LISTING_INDENT, 0, &r);
   1536 				x_off -= LISTING_INDENT;
   1537 				var   = g->first;
   1538 			} else {
   1539 				Variable *v = g->first;
   1540 
   1541 				ASSERT(!v || v->type == VT_BEAMFORMER_VARIABLE);
   1542 				/* NOTE(rnp): assume the suffix is the same for all elements */
   1543 				if (v) suffix = v->u.beamformer_variable.suffix;
   1544 
   1545 				switch (g->type) {
   1546 				case VG_LIST: break;
   1547 				case VG_V2:
   1548 				case VG_V4: {
   1549 					text_spec.limits.size.w = r.size.w + r.pos.x - at.x;
   1550 					at.x += draw_text(s8("{"), at, &text_spec).x;
   1551 					while (v) {
   1552 						text_spec.limits.size.w = r.size.w + r.pos.x - at.x;
   1553 						at.x += draw_beamformer_variable(ui, ui->arena, v,
   1554 						             at, mouse, FG_COLOUR, text_spec).x;
   1555 
   1556 						v = v->next;
   1557 						if (v) {
   1558 							text_spec.limits.size.w = r.size.w + r.pos.x - at.x;
   1559 							at.x += draw_text(s8(", "), at, &text_spec).x;
   1560 						}
   1561 					}
   1562 					text_spec.limits.size.w = r.size.w + r.pos.x - at.x;
   1563 					at.x += draw_text(s8("}"), at, &text_spec).x;
   1564 				} break;
   1565 				}
   1566 
   1567 				var = var->next;
   1568 			}
   1569 		} break;
   1570 		case VT_BEAMFORMER_FRAME_VIEW: {
   1571 			BeamformerFrameView *bv = var->u.generic;
   1572 			if (frame_view_ready_to_present(bv))
   1573 				draw_beamformer_frame_view(ui, ui->arena, var, r, mouse);
   1574 			var = var->next;
   1575 		} break;
   1576 		case VT_COMPUTE_PROGRESS_BAR: {
   1577 			ComputeProgressBar *bar = &var->u.compute_progress_bar;
   1578 			advance = draw_compute_progress_bar(ui, ui->arena, bar, r).y;
   1579 			var = var->next;
   1580 		} break;
   1581 		case VT_COMPUTE_LATEST_STATS_VIEW:
   1582 		case VT_COMPUTE_STATS_VIEW: {
   1583 			ComputeShaderStats *stats = var->u.compute_stats_view.stats;
   1584 			if (var->type == VT_COMPUTE_LATEST_STATS_VIEW)
   1585 				stats = *(ComputeShaderStats **)stats;
   1586 			advance = draw_compute_stats_view(var->u.compute_stats_view.ctx, ui->arena, stats, r).y;
   1587 			var = var->next;
   1588 		} break;
   1589 		default: INVALID_CODE_PATH;
   1590 		}
   1591 
   1592 		if (suffix.len) {
   1593 			v2 suffix_s = measure_text(ui->font, suffix);
   1594 			if (r.size.w + r.pos.x - LISTING_ITEM_PAD - suffix_s.x > at.x) {
   1595 				v2 suffix_p = {.x = r.pos.x + r.size.w - suffix_s.w, .y = r.pos.y};
   1596 				draw_text(suffix, suffix_p, &text_spec);
   1597 			}
   1598 		}
   1599 
   1600 		/* NOTE(rnp): we want to let this overflow to the desired size */
   1601 		r.pos.y  += advance + LISTING_LINE_PAD;
   1602 		r.size.y -= advance + LISTING_LINE_PAD;
   1603 	}
   1604 	view->needed_height = start_height - r.size.h;
   1605 }
   1606 
   1607 static void
   1608 draw_active_text_box(BeamformerUI *ui, Variable *var)
   1609 {
   1610 	InputState *is = &ui->text_input_state;
   1611 	Rect box       = is->rect;
   1612 
   1613 	s8 text          = {.len = is->buf_len, .data = is->buf};
   1614 	v2 text_size     = measure_text(*is->font, text);
   1615 	v2 text_position = {.x = box.pos.x, .y = box.pos.y + (box.size.h - text_size.h) / 2};
   1616 
   1617 	f32 cursor_width   = (is->cursor == is->buf_len) ? 16 : 4;
   1618 	f32 cursor_offset  = measure_text(*is->font, (s8){.data = text.data, .len = is->cursor}).w;
   1619 	cursor_offset     += text_position.x;
   1620 
   1621 	box.size.w = MAX(box.size.w, text_size.w + cursor_width);
   1622 	Rect background = extend_rect_centered(box, (v2){.x = 12, .y = 8});
   1623 	box = extend_rect_centered(box, (v2){.x = 8, .y = 4});
   1624 
   1625 	Rect cursor = {
   1626 		.pos  = {.x = cursor_offset, .y = text_position.y},
   1627 		.size = {.w = cursor_width,  .h = text_size.h},
   1628 	};
   1629 
   1630 	v4 cursor_colour = FOCUSED_COLOUR;
   1631 	cursor_colour.a  = CLAMP01(is->cursor_blink_t);
   1632 
   1633 	Color c = colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, var->hover_t));
   1634 	TextSpec text_spec = {.font = is->font, .colour = c};
   1635 
   1636 	DrawRectangleRounded(background.rl, 0.2, 0, fade(BLACK, 0.8));
   1637 	DrawRectangleRounded(box.rl, 0.2, 0, colour_from_normalized(BG_COLOUR));
   1638 	draw_text(text, text_position, &text_spec);
   1639 	DrawRectanglePro(cursor.rl, (Vector2){0}, 0, colour_from_normalized(cursor_colour));
   1640 }
   1641 
   1642 static void
   1643 draw_active_menu(BeamformerUI *ui, Arena arena, Variable *menu, v2 mouse, Rect window)
   1644 {
   1645 	ASSERT(menu->type == VT_GROUP);
   1646 	MenuState *ms = &ui->menu_state;
   1647 
   1648 	f32 max_label_width = 0;
   1649 
   1650 	Variable *item = menu->u.group.first;
   1651 	u32 item_count = 0;
   1652 	while (item) {
   1653 		max_label_width = MAX(max_label_width, item->name_width);
   1654 		item = item->next;
   1655 		item_count++;
   1656 	}
   1657 
   1658 	v2  at          = ms->at;
   1659 	f32 menu_width  = max_label_width + 8;
   1660 	f32 menu_height = item_count * ms->font->baseSize + (item_count) * 2;
   1661 
   1662 	if (at.x + menu_width > window.size.w)
   1663 		at.x = window.size.w - menu_width  - 16;
   1664 	if (at.y + menu_height > window.size.h)
   1665 		at.y = window.size.h - menu_height - 12;
   1666 	/* TODO(rnp): scroll menu if it doesn't fit on screen */
   1667 
   1668 	Rect menu_rect = {.pos = at, .size = {.w = menu_width, .h = menu_height}};
   1669 	Rect bg_rect   = extend_rect_centered(menu_rect, (v2){.x = 12, .y = 8});
   1670 	menu_rect      = extend_rect_centered(menu_rect, (v2){.x = 6,  .y = 4});
   1671 	DrawRectangleRounded(bg_rect.rl,   0.1, 0, fade(BLACK, 0.8));
   1672 	DrawRectangleRounded(menu_rect.rl, 0.1, 0, colour_from_normalized(BG_COLOUR));
   1673 
   1674 	/* TODO(rnp): last element has too much vertical space */
   1675 	item = menu->u.group.first;
   1676 	TextSpec text_spec = {.font = &ui->small_font, .colour = NORMALIZED_FG_COLOUR,
   1677 	                      .limits.size.w = menu_width};
   1678 	while (item) {
   1679 		at.y += draw_beamformer_variable(ui, arena, item, at, mouse, FG_COLOUR, text_spec).y;
   1680 		item = item->next;
   1681 		if (item) {
   1682 			DrawLineEx((v2){.x = at.x - 3, .y = at.y}.rl,
   1683 			           add_v2(at, (v2){.w = menu_width + 3}).rl, 2, fade(BLACK, 0.8));
   1684 			at.y += 2;
   1685 		}
   1686 	}
   1687 }
   1688 
   1689 static void
   1690 draw_variable(BeamformerUI *ui, Variable *var, Rect draw_rect, v2 mouse)
   1691 {
   1692 	if (var->type != VT_UI_REGION_SPLIT) {
   1693 		v2 shrink = {.x = UI_REGION_PAD, .y = UI_REGION_PAD};
   1694 		draw_rect = shrink_rect_centered(draw_rect, shrink);
   1695 		BeginScissorMode(draw_rect.pos.x, draw_rect.pos.y, draw_rect.size.w, draw_rect.size.h);
   1696 		draw_rect = draw_title_bar(ui, ui->arena, var, draw_rect, mouse);
   1697 		EndScissorMode();
   1698 	}
   1699 
   1700 	/* TODO(rnp): post order traversal of the ui tree will remove the need for this */
   1701 	if (!CheckCollisionPointRec(mouse.rl, draw_rect.rl))
   1702 		mouse = (v2){.x = F32_INFINITY, .y = F32_INFINITY};
   1703 
   1704 	BeginScissorMode(draw_rect.pos.x, draw_rect.pos.y, draw_rect.size.w, draw_rect.size.h);
   1705 	switch (var->type) {
   1706 	case VT_UI_VIEW: {
   1707 		draw_ui_view(ui, var, draw_rect, mouse);
   1708 	} break;
   1709 	case VT_UI_REGION_SPLIT: {
   1710 		RegionSplit *rs = &var->u.region_split;
   1711 
   1712 		Rect split, hover;
   1713 		switch (rs->direction) {
   1714 		case RSD_VERTICAL: {
   1715 			split_rect_vertical(draw_rect, rs->fraction, 0, &split);
   1716 			split.pos.x  += UI_REGION_PAD;
   1717 			split.pos.y  -= UI_SPLIT_HANDLE_THICK / 2;
   1718 			split.size.h  = UI_SPLIT_HANDLE_THICK;
   1719 			split.size.w -= 2 * UI_REGION_PAD;
   1720 			hover = extend_rect_centered(split, (v2){.y = 0.75 * UI_REGION_PAD});
   1721 		} break;
   1722 		case RSD_HORIZONTAL: {
   1723 			split_rect_horizontal(draw_rect, rs->fraction, 0, &split);
   1724 			split.pos.x  -= UI_SPLIT_HANDLE_THICK / 2;
   1725 			split.pos.y  += UI_REGION_PAD;
   1726 			split.size.w  = UI_SPLIT_HANDLE_THICK;
   1727 			split.size.h -= 2 * UI_REGION_PAD;
   1728 			hover = extend_rect_centered(split, (v2){.x = 0.75 * UI_REGION_PAD});
   1729 		} break;
   1730 		}
   1731 
   1732 		hover_var(ui, mouse, hover, var);
   1733 
   1734 		v4 colour = HOVERED_COLOUR;
   1735 		colour.a  = var->hover_t;
   1736 		DrawRectangleRounded(split.rl, 0.6, 0, colour_from_normalized(colour));
   1737 	} break;
   1738 	default: break;
   1739 	}
   1740 	EndScissorMode();
   1741 }
   1742 
   1743 static void
   1744 draw_ui_regions(BeamformerUI *ui, Rect window, v2 mouse)
   1745 {
   1746 	struct region_stack_item {
   1747 		Variable *var;
   1748 		Rect      rect;
   1749 	} *region_stack;
   1750 
   1751 	TempArena arena_savepoint = begin_temp_arena(&ui->arena);
   1752 	i32 stack_index = 0;
   1753 
   1754 	region_stack = alloc(&ui->arena, typeof(*region_stack), 256);
   1755 	region_stack[0].var  = ui->regions;
   1756 	region_stack[0].rect = window;
   1757 
   1758 	while (stack_index != -1) {
   1759 		struct region_stack_item *rsi = region_stack + stack_index--;
   1760 		Rect rect = rsi->rect;
   1761 		draw_variable(ui, rsi->var, rect, mouse);
   1762 
   1763 		if (rsi->var->type == VT_UI_REGION_SPLIT) {
   1764 			Rect first, second;
   1765 			RegionSplit *rs = &rsi->var->u.region_split;
   1766 			switch (rs->direction) {
   1767 			case RSD_VERTICAL: {
   1768 				split_rect_vertical(rect, rs->fraction, &first, &second);
   1769 			} break;
   1770 			case RSD_HORIZONTAL: {
   1771 				split_rect_horizontal(rect, rs->fraction, &first, &second);
   1772 			} break;
   1773 			}
   1774 
   1775 			stack_index++;
   1776 			region_stack[stack_index].var  = rs->right;
   1777 			region_stack[stack_index].rect = second;
   1778 			stack_index++;
   1779 			region_stack[stack_index].var  = rs->left;
   1780 			region_stack[stack_index].rect = first;
   1781 		}
   1782 
   1783 		ASSERT(stack_index < 256);
   1784 	}
   1785 
   1786 	end_temp_arena(arena_savepoint);
   1787 }
   1788 
   1789 static void
   1790 ui_store_variable_base(VariableType type, void *store, void *new_value, void *limits)
   1791 {
   1792 	switch (type) {
   1793 	case VT_B32: {
   1794 		*(b32 *)store = *(b32 *)new_value;
   1795 	} break;
   1796 	case VT_F32: {
   1797 		v2 *lim = limits;
   1798 		f32 val = *(f32 *)new_value;
   1799 		*(f32 *)store = CLAMP(val, lim->x, lim->y);
   1800 	} break;
   1801 	default: INVALID_CODE_PATH;
   1802 	}
   1803 }
   1804 
   1805 static void
   1806 ui_store_variable(Variable *var, void *new_value)
   1807 {
   1808 	switch (var->type) {
   1809 	case VT_F32: {
   1810 		v2 limits = {.x = -F32_INFINITY, .y = F32_INFINITY};
   1811 		ui_store_variable_base(VT_F32, &var->u.f32, new_value, &limits);
   1812 	} break;
   1813 	case VT_BEAMFORMER_VARIABLE: {
   1814 		BeamformerVariable *bv = &var->u.beamformer_variable;
   1815 		ui_store_variable_base(bv->store_type, bv->store, new_value, &bv->params.limits);
   1816 	} break;
   1817 	default: INVALID_CODE_PATH;
   1818 	}
   1819 }
   1820 
   1821 static void
   1822 scroll_interaction_base(VariableType type, void *store, f32 delta)
   1823 {
   1824 	switch (type) {
   1825 	case VT_B32: { *(b32 *)store  = !*(b32 *)store; } break;
   1826 	case VT_F32: { *(f32 *)store += delta;          } break;
   1827 	case VT_I32: { *(i32 *)store += delta;          } break;
   1828 	default: INVALID_CODE_PATH;
   1829 	}
   1830 }
   1831 
   1832 static void
   1833 scroll_interaction(Variable *var, f32 delta)
   1834 {
   1835 	switch (var->type) {
   1836 	case VT_BEAMFORMER_VARIABLE: {
   1837 		BeamformerVariable *bv = &var->u.beamformer_variable;
   1838 		scroll_interaction_base(bv->store_type, bv->store, delta * bv->params.scroll_scale);
   1839 		ui_store_variable(var, bv->store);
   1840 	} break;
   1841 	case VT_UI_VIEW: {
   1842 		scroll_interaction_base(VT_F32, &var->u.view.offset, UI_SCROLL_SPEED * delta);
   1843 		var->u.view.offset = MAX(0, var->u.view.offset);
   1844 	} break;
   1845 	default: scroll_interaction_base(var->type, &var->u, delta); break;
   1846 	}
   1847 }
   1848 
   1849 static void
   1850 begin_menu_input(MenuState *ms, v2 mouse)
   1851 {
   1852 	ms->at   = mouse;
   1853 	ms->font = ms->hot_font;
   1854 }
   1855 
   1856 static void
   1857 begin_text_input(InputState *is, Variable *var, v2 mouse)
   1858 {
   1859 	Stream s = {.cap = ARRAY_COUNT(is->buf), .data = is->buf};
   1860 	stream_append_variable(&s, var);
   1861 	is->buf_len = s.widx;
   1862 	is->rect    = is->hot_rect;
   1863 	is->font    = is->hot_font;
   1864 
   1865 	/* NOTE: extra offset to help with putting a cursor at idx 0 */
   1866 	#define TEXT_HALF_CHAR_WIDTH 10
   1867 	f32 hover_p = CLAMP01((mouse.x - is->rect.pos.x) / is->rect.size.w);
   1868 	f32 x_off = TEXT_HALF_CHAR_WIDTH, x_bounds = is->rect.size.w * hover_p;
   1869 	i32 i;
   1870 	for (i = 0; i < is->buf_len && x_off < x_bounds; i++) {
   1871 		/* NOTE: assumes font glyphs are ordered ASCII */
   1872 		i32 idx  = is->buf[i] - 0x20;
   1873 		x_off   += is->font->glyphs[idx].advanceX;
   1874 		if (is->font->glyphs[idx].advanceX == 0)
   1875 			x_off += is->font->recs[idx].width;
   1876 	}
   1877 	is->cursor = i;
   1878 }
   1879 
   1880 static void
   1881 end_text_input(InputState *is, Variable *var)
   1882 {
   1883 	f32 scale = 1;
   1884 	if (var->type == VT_BEAMFORMER_VARIABLE) {
   1885 		BeamformerVariable *bv = &var->u.beamformer_variable;
   1886 		ASSERT(bv->store_type == VT_F32);
   1887 		scale = bv->params.display_scale;
   1888 		var->hover_t = 0;
   1889 	}
   1890 	f32 value = parse_f64((s8){.len = is->buf_len, .data = is->buf}) / scale;
   1891 	ui_store_variable(var, &value);
   1892 }
   1893 
   1894 static void
   1895 update_text_input(InputState *is, Variable *var)
   1896 {
   1897 	ASSERT(is->cursor != -1);
   1898 
   1899 	is->cursor_blink_t += is->cursor_blink_scale * dt_for_frame;
   1900 	if (is->cursor_blink_t >= 1) is->cursor_blink_scale = -1.5f;
   1901 	if (is->cursor_blink_t <= 0) is->cursor_blink_scale =  1.5f;
   1902 
   1903 	var->hover_t -= 2 * HOVER_SPEED * dt_for_frame;
   1904 	var->hover_t  = CLAMP01(var->hover_t);
   1905 
   1906 	/* NOTE: handle multiple input keys on a single frame */
   1907 	i32 key = GetCharPressed();
   1908 	while (key > 0) {
   1909 		if ((is->buf_len == ARRAY_COUNT(is->buf)) || (is->cursor == ARRAY_COUNT(is->buf) - 1))
   1910 			break;
   1911 
   1912 		b32 allow_key = ((key >= '0' && key <= '9') || (key == '.') ||
   1913 		                 (key == '-' && is->cursor == 0));
   1914 		if (allow_key) {
   1915 			mem_move(is->buf + is->cursor + 1,
   1916 			         is->buf + is->cursor,
   1917 			         is->buf_len - is->cursor + 1);
   1918 
   1919 			is->buf[is->cursor++] = key;
   1920 			is->buf_len++;
   1921 		}
   1922 		key = GetCharPressed();
   1923 	}
   1924 
   1925 	if ((IsKeyPressed(KEY_LEFT) || IsKeyPressedRepeat(KEY_LEFT)) && is->cursor > 0)
   1926 		is->cursor--;
   1927 
   1928 	if ((IsKeyPressed(KEY_RIGHT) || IsKeyPressedRepeat(KEY_RIGHT)) && is->cursor < is->buf_len)
   1929 		is->cursor++;
   1930 
   1931 	if ((IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE)) && is->cursor > 0) {
   1932 		is->cursor--;
   1933 		if (is->cursor < ARRAY_COUNT(is->buf) - 1) {
   1934 			mem_move(is->buf + is->cursor,
   1935 			         is->buf + is->cursor + 1,
   1936 			         is->buf_len - is->cursor);
   1937 		}
   1938 		is->buf_len--;
   1939 	}
   1940 
   1941 	if ((IsKeyPressed(KEY_DELETE) || IsKeyPressedRepeat(KEY_DELETE)) && is->cursor < is->buf_len) {
   1942 		mem_move(is->buf + is->cursor,
   1943 			 is->buf + is->cursor + 1,
   1944 		         is->buf_len - is->cursor);
   1945 		is->buf_len--;
   1946 	}
   1947 }
   1948 
   1949 static void
   1950 display_interaction_end(BeamformerUI *ui)
   1951 {
   1952 	b32 is_hot    = ui->interaction.hot_type == IT_DISPLAY;
   1953 	b32 is_active = ui->interaction.type     == IT_DISPLAY;
   1954 	if ((is_active && is_hot) || ui->ruler_state == RS_HOLD)
   1955 		return;
   1956 	ui->ruler_state = RS_NONE;
   1957 }
   1958 
   1959 static void
   1960 display_interaction(BeamformerUI *ui, v2 mouse, f32 scroll)
   1961 {
   1962 	b32 mouse_left_pressed  = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
   1963 	b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT);
   1964 	b32 is_hot              = ui->interaction.hot_type == IT_DISPLAY;
   1965 	b32 is_active           = ui->interaction.type     == IT_DISPLAY;
   1966 
   1967 	if (scroll) {
   1968 		ASSERT(ui->interaction.active->type == VT_BEAMFORMER_FRAME_VIEW);
   1969 		BeamformerFrameView *bv = ui->interaction.active->u.generic;
   1970 		bv->threshold.u.f32 += scroll;
   1971 		bv->needs_update = 1;
   1972 	}
   1973 
   1974 	if (mouse_left_pressed && is_active) {
   1975 		ui->ruler_state++;
   1976 		switch (ui->ruler_state) {
   1977 		case RS_START: ui->ruler_start_p = mouse; break;
   1978 		case RS_HOLD:  ui->ruler_stop_p  = mouse; break;
   1979 		default:
   1980 			ui->ruler_state = RS_NONE;
   1981 			break;
   1982 		}
   1983 	} else if (mouse_right_pressed && is_hot) {
   1984 		ui->ruler_state = RS_NONE;
   1985 	}
   1986 }
   1987 
   1988 static void
   1989 scale_bar_interaction(BeamformerCtx *ctx, v2 mouse)
   1990 {
   1991 	BeamformerUI *ui        = ctx->ui;
   1992 	InteractionState *is    = &ui->interaction;
   1993 	ScaleBar *sb            = is->active->u.generic;
   1994 	b32 mouse_left_pressed  = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
   1995 	b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT);
   1996 	f32 mouse_wheel         = GetMouseWheelMoveV().y;
   1997 
   1998 	if (mouse_left_pressed) {
   1999 		if (sb->zoom_starting_point.x == F32_INFINITY) {
   2000 			sb->zoom_starting_point = sub_v2(mouse, sb->screen_offset);
   2001 		} else {
   2002 			v2 relative_mouse = sub_v2(mouse, sb->screen_offset);
   2003 			f32 min = magnitude_v2(mul_v2(sb->zoom_starting_point, sb->screen_space_to_value));
   2004 			f32 max = magnitude_v2(mul_v2(relative_mouse,          sb->screen_space_to_value));
   2005 			if (min > max) { f32 tmp = min; min = max; max = tmp; }
   2006 
   2007 			min += *sb->min_value;
   2008 			max += *sb->min_value;
   2009 
   2010 			v2_sll *savepoint = SLLPop(ui->scale_bar_savepoint_freelist);
   2011 			if (!savepoint) savepoint = push_struct(&ui->arena, v2_sll);
   2012 
   2013 			savepoint->v.x = *sb->min_value;
   2014 			savepoint->v.y = *sb->max_value;
   2015 			SLLPush(savepoint, sb->savepoint_stack);
   2016 
   2017 			*sb->min_value = MAX(min, sb->limits.x);
   2018 			*sb->max_value = MIN(max, sb->limits.y);
   2019 
   2020 			sb->zoom_starting_point = (v2){.x = F32_INFINITY, .y = F32_INFINITY};
   2021 			if (sb->causes_compute)
   2022 				ui->flush_params = 1;
   2023 		}
   2024 	}
   2025 
   2026 	if (mouse_right_pressed) {
   2027 		v2_sll *savepoint = sb->savepoint_stack;
   2028 		if (savepoint) {
   2029 			if (sb->causes_compute)
   2030 				ui->flush_params = 1;
   2031 			*sb->min_value      = savepoint->v.x;
   2032 			*sb->max_value      = savepoint->v.y;
   2033 			sb->savepoint_stack = SLLPush(savepoint, ui->scale_bar_savepoint_freelist);
   2034 		}
   2035 		sb->zoom_starting_point = (v2){.x = F32_INFINITY, .y = F32_INFINITY};
   2036 	}
   2037 
   2038 	if (mouse_wheel) {
   2039 		*sb->min_value += mouse_wheel * sb->scroll_scale.x;
   2040 		*sb->max_value += mouse_wheel * sb->scroll_scale.y;
   2041 		*sb->min_value  = MAX(sb->limits.x, *sb->min_value);
   2042 		*sb->max_value  = MIN(sb->limits.y, *sb->max_value);
   2043 		if (sb->causes_compute)
   2044 			ui->flush_params = 1;
   2045 	}
   2046 }
   2047 
   2048 static void
   2049 ui_button_interaction(BeamformerUI *ui, Variable *button)
   2050 {
   2051 	ASSERT(button->type == VT_UI_BUTTON);
   2052 	switch (button->u.button) {
   2053 	case UI_BID_FV_COPY_HORIZONTAL: ui_copy_frame(ui, button, RSD_HORIZONTAL);  break;
   2054 	case UI_BID_FV_COPY_VERTICAL:   ui_copy_frame(ui, button, RSD_VERTICAL);    break;
   2055 	case UI_BID_CLOSE_VIEW: {
   2056 		Variable *view   = button->parent;
   2057 		Variable *region = view->parent;
   2058 		ASSERT(view->type == VT_UI_VIEW && region->type == VT_UI_REGION_SPLIT);
   2059 
   2060 		Variable *parent    = region->parent;
   2061 		Variable *remaining = region->u.region_split.left;
   2062 		if (remaining == view) remaining = region->u.region_split.right;
   2063 
   2064 		ui_view_free(ui, view);
   2065 
   2066 		ASSERT(parent->type == VT_UI_REGION_SPLIT);
   2067 		if (parent->u.region_split.left == region) {
   2068 			parent->u.region_split.left  = remaining;
   2069 		} else {
   2070 			parent->u.region_split.right = remaining;
   2071 		}
   2072 		remaining->parent = parent;
   2073 
   2074 		SLLPush(region, ui->variable_freelist);
   2075 	} break;
   2076 	}
   2077 }
   2078 
   2079 static void
   2080 ui_begin_interact(BeamformerUI *ui, BeamformerInput *input, b32 scroll, b32 mouse_left_pressed)
   2081 {
   2082 	InteractionState *is = &ui->interaction;
   2083 	if (is->hot_type != IT_NONE) {
   2084 		is->type = is->hot_type;
   2085 	} else if (is->hot) {
   2086 		switch (is->hot->type) {
   2087 		case VT_NULL: is->type = IT_NOP; break;
   2088 		case VT_B32:  is->type = IT_SET; break;
   2089 		case VT_UI_REGION_SPLIT: { is->type = IT_DRAG; }                 break;
   2090 		case VT_UI_VIEW:         { if (scroll) is->type = IT_SCROLL; }   break;
   2091 		case VT_UI_BUTTON:       { ui_button_interaction(ui, is->hot); } break;
   2092 		case VT_GROUP: {
   2093 			if (mouse_left_pressed && is->hot->flags & V_MENU) {
   2094 				is->type = IT_MENU;
   2095 				begin_menu_input(&ui->menu_state, input->mouse);
   2096 			} else {
   2097 				is->type = IT_SET;
   2098 			}
   2099 		} break;
   2100 		case VT_BEAMFORMER_VARIABLE: {
   2101 			if (is->hot->u.beamformer_variable.store_type == VT_B32) {
   2102 				is->type = IT_SET;
   2103 				break;
   2104 			}
   2105 		} /* FALLTHROUGH */
   2106 		case VT_F32: {
   2107 			if (scroll) {
   2108 				is->type = IT_SCROLL;
   2109 			} else if (mouse_left_pressed && is->hot->flags & V_TEXT) {
   2110 				is->type = IT_TEXT;
   2111 				begin_text_input(&ui->text_input_state, is->hot, input->mouse);
   2112 			}
   2113 		} break;
   2114 		default: INVALID_CODE_PATH;
   2115 		}
   2116 	}
   2117 	if (is->type != IT_NONE) {
   2118 		is->active = is->hot;
   2119 		if ((iptr)is->hot == (iptr)ui->scratch_variables)
   2120 			ui->scratch_variable = ui->scratch_variables + 1;
   2121 		else
   2122 			ui->scratch_variable = ui->scratch_variables + 0;
   2123 	}
   2124 }
   2125 
   2126 static void
   2127 ui_end_interact(BeamformerCtx *ctx, v2 mouse)
   2128 {
   2129 	BeamformerUI *ui = ctx->ui;
   2130 	InteractionState *is = &ui->interaction;
   2131 	switch (is->type) {
   2132 	case IT_NOP:  break;
   2133 	case IT_SET: {
   2134 		switch (is->active->type) {
   2135 		case VT_GROUP: {
   2136 			is->active->u.group.expanded = !is->active->u.group.expanded;
   2137 		} break;
   2138 		case VT_B32: {
   2139 			is->active->u.b32 = !is->active->u.b32;
   2140 		} break;
   2141 		case VT_BEAMFORMER_VARIABLE: {
   2142 			ASSERT(is->active->u.beamformer_variable.store_type == VT_B32);
   2143 			b32 *val = is->active->u.beamformer_variable.store;
   2144 			*val     = !(*val);
   2145 		} break;
   2146 		default: INVALID_CODE_PATH;
   2147 		}
   2148 	} break;
   2149 	case IT_DISPLAY: display_interaction_end(ui); break;
   2150 	case IT_SCROLL:  scroll_interaction(is->active, GetMouseWheelMoveV().y); break;
   2151 	case IT_TEXT:    end_text_input(&ui->text_input_state, is->active);      break;
   2152 	case IT_MENU:      break;
   2153 	case IT_SCALE_BAR: break;
   2154 	case IT_DRAG:      break;
   2155 	default: INVALID_CODE_PATH;
   2156 	}
   2157 
   2158 	if (is->active->flags & V_CAUSES_COMPUTE)
   2159 		ui->flush_params = 1;
   2160 	if (is->active->flags & V_UPDATE_VIEW) {
   2161 		Variable *frame_view = is->active->parent;
   2162 		ASSERT(frame_view && frame_view->type == VT_BEAMFORMER_FRAME_VIEW);
   2163 		((BeamformerFrameView *)frame_view->u.generic)->needs_update = 1;
   2164 	}
   2165 
   2166 	is->type   = IT_NONE;
   2167 	is->active = 0;
   2168 }
   2169 
   2170 static void
   2171 ui_interact(BeamformerCtx *ctx, BeamformerInput *input)
   2172 {
   2173 	BeamformerUI *ui        = ctx->ui;
   2174 	InteractionState *is    = &ui->interaction;
   2175 	b32 mouse_left_pressed  = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
   2176 	b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT);
   2177 	b32 wheel_moved         = GetMouseWheelMoveV().y != 0;
   2178 	if (mouse_right_pressed || mouse_left_pressed || wheel_moved) {
   2179 		if (is->type != IT_NONE)
   2180 			ui_end_interact(ctx, input->mouse);
   2181 		ui_begin_interact(ui, input, wheel_moved, mouse_left_pressed);
   2182 	}
   2183 
   2184 	if (IsKeyPressed(KEY_ENTER) && is->type == IT_TEXT)
   2185 		ui_end_interact(ctx, input->mouse);
   2186 
   2187 	switch (is->type) {
   2188 	case IT_NONE: break;
   2189 	case IT_NOP:  break;
   2190 	case IT_MENU: break;
   2191 	case IT_DISPLAY: display_interaction(ui, input->mouse, GetMouseWheelMoveV().y); break;
   2192 	case IT_SCROLL:  ui_end_interact(ctx, input->mouse);                            break;
   2193 	case IT_SET:     ui_end_interact(ctx, input->mouse);                            break;
   2194 	case IT_TEXT:    update_text_input(&ui->text_input_state, is->active);          break;
   2195 	case IT_DRAG: {
   2196 		if (!IsMouseButtonDown(MOUSE_BUTTON_LEFT) && !IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) {
   2197 			ui_end_interact(ctx, input->mouse);
   2198 		} else {
   2199 			v2 ws     = (v2){.w = ctx->window_size.w, .h = ctx->window_size.h};
   2200 			v2 dMouse = sub_v2(input->mouse, input->last_mouse);
   2201 			dMouse    = mul_v2(dMouse, (v2){.x = 1.0f / ws.w, .y = 1.0f / ws.h});
   2202 
   2203 			switch (is->active->type) {
   2204 			case VT_UI_REGION_SPLIT: {
   2205 				f32 min_fraction;
   2206 				RegionSplit *rs = &is->active->u.region_split;
   2207 				switch (rs->direction) {
   2208 				case RSD_VERTICAL: {
   2209 					min_fraction  = (UI_SPLIT_HANDLE_THICK + 0.5 * UI_REGION_PAD) / ws.h;
   2210 					rs->fraction += dMouse.y;
   2211 				} break;
   2212 				case RSD_HORIZONTAL: {
   2213 					min_fraction  = (UI_SPLIT_HANDLE_THICK + 0.5 * UI_REGION_PAD) / ws.w;
   2214 					rs->fraction += dMouse.x;
   2215 				} break;
   2216 				}
   2217 				rs->fraction = CLAMP(rs->fraction, min_fraction, 1 - min_fraction);
   2218 			} break;
   2219 			default: break;
   2220 			}
   2221 
   2222 			if (is->active != is->hot) {
   2223 				is->active->hover_t += HOVER_SPEED * dt_for_frame;
   2224 				is->active->hover_t  = CLAMP01(is->active->hover_t);
   2225 			}
   2226 		}
   2227 	} break;
   2228 	case IT_SCALE_BAR: scale_bar_interaction(ctx, input->mouse); break;
   2229 	}
   2230 
   2231 	is->hot_type = IT_NONE;
   2232 	is->hot      = 0;
   2233 }
   2234 
   2235 static void
   2236 ui_init(BeamformerCtx *ctx, Arena store)
   2237 {
   2238 	/* NOTE(rnp): store the ui at the base of the passed in arena and use the rest for
   2239 	 * temporary allocations within the ui. If needed we can recall this function to
   2240 	 * completely clear the ui state. The is that if we store pointers to static data
   2241 	 * such as embedded font data we will need to reset them when the executable reloads.
   2242 	 * We could also build some sort of ui structure here and store it then iterate over
   2243 	 * it to actually draw the ui. If we reload we may have changed it so we should
   2244 	 * rebuild it */
   2245 
   2246 	BeamformerUI *ui = ctx->ui;
   2247 
   2248 	/* NOTE(rnp): unload old data from GPU */
   2249 	if (ui) {
   2250 		UnloadFont(ui->font);
   2251 		UnloadFont(ui->small_font);
   2252 
   2253 		for (BeamformerFrameView *view = ui->views; view; view = view->next)
   2254 			if (view->rendered_view.id)
   2255 				UnloadRenderTexture(view->rendered_view);
   2256 	}
   2257 
   2258 	ui = ctx->ui = push_struct(&store, typeof(*ui));
   2259 	ui->os    = &ctx->os;
   2260 	ui->arena = store;
   2261 
   2262 	/* TODO: build these into the binary */
   2263 	/* TODO(rnp): better font, this one is jank at small sizes */
   2264 	ui->font       = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 28, 0, 0);
   2265 	ui->small_font = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 20, 0, 0);
   2266 
   2267 	ui->scratch_variable = ui->scratch_variables + 0;
   2268 	Variable *split = ui->regions   = add_ui_split(ui, 0, &ui->arena, s8("UI Root"), 0.4,
   2269 	                                               RSD_HORIZONTAL, ui->font);
   2270 	split->u.region_split.left      = add_ui_split(ui, split, &ui->arena, s8(""), 0.475,
   2271 	                                               RSD_VERTICAL, ui->font);
   2272 	split->u.region_split.right     = add_beamformer_frame_view(ui, split, &ui->arena, FVT_LATEST, 0);
   2273 
   2274 	BeamformerFrameView *bv         = split->u.region_split.right->u.group.first->u.generic;
   2275 	bv->lateral_scale_bar.min_value = &ui->params.output_min_coordinate.x;
   2276 	bv->lateral_scale_bar.max_value = &ui->params.output_max_coordinate.x;
   2277 	bv->axial_scale_bar.min_value   = &ui->params.output_min_coordinate.z;
   2278 	bv->axial_scale_bar.max_value   = &ui->params.output_max_coordinate.z;
   2279 	bv->axial_scale_bar.causes_compute   = 1;
   2280 	bv->lateral_scale_bar.causes_compute = 1;
   2281 	bv->ctx = &ctx->fsctx;
   2282 
   2283 	split = split->u.region_split.left;
   2284 	split->u.region_split.left  = add_beamformer_parameters_view(split, ctx);
   2285 	split->u.region_split.right = add_ui_split(ui, split, &ui->arena, s8(""), 0.22,
   2286 	                                           RSD_VERTICAL, ui->font);
   2287 	split = split->u.region_split.right;
   2288 
   2289 	split->u.region_split.left  = add_compute_progress_bar(split, ctx);
   2290 	split->u.region_split.right = add_compute_stats_view(ui, split, &ui->arena,
   2291 	                                                     VT_COMPUTE_LATEST_STATS_VIEW);
   2292 
   2293 	ComputeStatsView *compute_stats = &split->u.region_split.right->u.group.first->u.compute_stats_view;
   2294 	compute_stats->ctx   = ctx;
   2295 	compute_stats->stats = &ui->latest_compute_stats;
   2296 
   2297 	ctx->ui_read_params = 1;
   2298 }
   2299 
   2300 static void
   2301 validate_ui_parameters(BeamformerUIParameters *p)
   2302 {
   2303 	if (p->output_min_coordinate.x > p->output_max_coordinate.x)
   2304 		SWAP(p->output_min_coordinate.x, p->output_max_coordinate.x)
   2305 	if (p->output_min_coordinate.z > p->output_max_coordinate.z)
   2306 		SWAP(p->output_min_coordinate.z, p->output_max_coordinate.z)
   2307 }
   2308 
   2309 static void
   2310 draw_ui(BeamformerCtx *ctx, BeamformerInput *input, BeamformFrame *frame_to_draw,
   2311         ComputeShaderStats *latest_compute_stats)
   2312 {
   2313 	BeamformerUI *ui = ctx->ui;
   2314 
   2315 	if (frame_to_draw->ready_to_present) ui->latest_frame = frame_to_draw;
   2316 	ui->latest_compute_stats = latest_compute_stats;
   2317 
   2318 	/* TODO(rnp): there should be a better way of detecting this */
   2319 	if (ctx->ui_read_params) {
   2320 		mem_copy(&ui->params, &ctx->params->raw.output_min_coordinate, sizeof(ui->params));
   2321 		ui->flush_params    = 0;
   2322 		ctx->ui_read_params = 0;
   2323 	}
   2324 
   2325 	/* NOTE: process interactions first because the user interacted with
   2326 	 * the ui that was presented last frame */
   2327 	ui_interact(ctx, input);
   2328 
   2329 	if (ui->flush_params) {
   2330 		validate_ui_parameters(&ui->params);
   2331 		if (!ctx->csctx.processing_compute) {
   2332 			mem_copy(&ctx->params->raw.output_min_coordinate, &ui->params, sizeof(ui->params));
   2333 			ui->flush_params    = 0;
   2334 			ctx->params->upload = 1;
   2335 			ctx->start_compute  = 1;
   2336 		}
   2337 	}
   2338 
   2339 	/* NOTE(rnp): can't render to a different framebuffer in the middle of BeginDrawing()... */
   2340 	update_frame_views(ui);
   2341 
   2342 	BeginDrawing();
   2343 		ClearBackground(colour_from_normalized(BG_COLOUR));
   2344 
   2345 		v2 mouse         = input->mouse;
   2346 		Rect window_rect = {.size = {.w = ctx->window_size.w, .h = ctx->window_size.h}};
   2347 
   2348 		draw_ui_regions(ui, window_rect, mouse);
   2349 		if (ui->interaction.type == IT_TEXT)
   2350 			draw_active_text_box(ui, ui->interaction.active);
   2351 		if (ui->interaction.type == IT_MENU)
   2352 			draw_active_menu(ui, ui->arena, ui->interaction.active, mouse, window_rect);
   2353 	EndDrawing();
   2354 }