ogl_beamforming

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

ui.c (93160B)


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