ogl_beamforming

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

ui.c (86770B)


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