ogl_beamforming

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

ui.c (93568B)


      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 function 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 function 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 function 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 function 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 function 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 function 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 true_false_labels[] = {s8("False"), s8("True")};
    959 	add_variable_cycler(ui, group, &ui->arena, V_CAUSES_COMPUTE, ui->font, s8("Interpolate:"),
    960 	                    &bp->interpolate, true_false_labels, countof(true_false_labels));
    961 
    962 	add_variable_cycler(ui, group, &ui->arena, V_CAUSES_COMPUTE, ui->font, s8("Coherency Weighting:"),
    963 	                    &bp->coherency_weighting, true_false_labels, countof(true_false_labels));
    964 
    965 	return result;
    966 }
    967 
    968 function Variable *
    969 add_beamformer_frame_view(BeamformerUI *ui, Variable *parent, Arena *arena,
    970                           BeamformerFrameViewType type, b32 closable)
    971 {
    972 	/* TODO(rnp): this can be always closable once we have a way of opening new views */
    973 	Variable *result = add_ui_view(ui, parent, arena, s8(""), UI_VIEW_CUSTOM_TEXT, closable);
    974 	Variable *var = result->u.view.child = add_variable(ui, result, arena, s8(""), 0,
    975 	                                                    VT_BEAMFORMER_FRAME_VIEW, ui->small_font);
    976 
    977 	BeamformerFrameView *bv = SLLPop(ui->view_freelist);
    978 	if (bv) zero_struct(bv);
    979 	else    bv = push_struct(arena, typeof(*bv));
    980 	DLLPushDown(bv, ui->views);
    981 
    982 	var->u.generic = bv;
    983 	bv->type       = type;
    984 
    985 	fill_variable(&bv->dynamic_range, var, s8("Dynamic Range:"), V_INPUT|V_TEXT|V_UPDATE_VIEW,
    986 	              VT_F32, ui->small_font);
    987 	fill_variable(&bv->threshold, var, s8("Threshold:"), V_INPUT|V_TEXT|V_UPDATE_VIEW,
    988 	              VT_F32, ui->small_font);
    989 	fill_variable(&bv->gamma, var, s8("Gamma:"), V_INPUT|V_TEXT|V_UPDATE_VIEW,
    990 	              VT_SCALED_F32, ui->small_font);
    991 
    992 	bv->dynamic_range.u.f32      = 50.0f;
    993 	bv->threshold.u.f32          = 55.0f;
    994 	bv->gamma.u.scaled_f32.val   = 1.0f;
    995 	bv->gamma.u.scaled_f32.scale = 0.05f;
    996 
    997 	fill_variable(&bv->lateral_scale_bar, var, s8(""), V_INPUT, VT_SCALE_BAR, ui->small_font);
    998 	fill_variable(&bv->axial_scale_bar,   var, s8(""), V_INPUT, VT_SCALE_BAR, ui->small_font);
    999 	ScaleBar *lateral            = &bv->lateral_scale_bar.u.scale_bar;
   1000 	ScaleBar *axial              = &bv->axial_scale_bar.u.scale_bar;
   1001 	lateral->direction           = SB_LATERAL;
   1002 	axial->direction             = SB_AXIAL;
   1003 	lateral->scroll_scale        = (v2){.x = -0.5e-3, .y = 0.5e-3};
   1004 	axial->scroll_scale          = (v2){.x =  0,      .y = 1e-3};
   1005 	lateral->zoom_starting_coord = F32_INFINITY;
   1006 	axial->zoom_starting_coord   = F32_INFINITY;
   1007 
   1008 	Variable *menu = result->u.view.menu;
   1009 	/* TODO(rnp): push to head of list? */
   1010 	Variable *old_menu_first = menu->u.group.first;
   1011 	Variable *old_menu_last  = menu->u.group.last;
   1012 	menu->u.group.first = menu->u.group.last = 0;
   1013 
   1014 	#define X(id, text) add_button(ui, menu, arena, s8(text), UI_BID_ ##id, V_CLOSES_MENU, ui->small_font);
   1015 	FRAME_VIEW_BUTTONS
   1016 	#undef X
   1017 
   1018 	switch (type) {
   1019 	case FVT_LATEST: {
   1020 		#define X(_type, _id, pretty) s8(pretty),
   1021 		local_persist s8 labels[] = { IMAGE_PLANE_TAGS s8("Any") };
   1022 		#undef X
   1023 		bv->cycler = add_variable_cycler(ui, menu, arena, 0, ui->small_font, s8("Live: "),
   1024 		                                 &bv->cycler_state, labels, countof(labels));
   1025 		bv->cycler_state = IPT_LAST;
   1026 	} break;
   1027 	case FVT_INDEXED: {
   1028 		bv->cycler = add_variable_cycler(ui, menu, arena, 0, ui->small_font, s8("Index: "),
   1029 		                                 &bv->cycler_state, 0, MAX_BEAMFORMED_SAVED_FRAMES);
   1030 	} break;
   1031 	default: break;
   1032 	}
   1033 
   1034 	bv->log_scale                = add_variable(ui, menu, arena, s8("Log Scale"),
   1035 	                                            V_INPUT|V_UPDATE_VIEW|V_RADIO_BUTTON, VT_B32,
   1036 	                                            ui->small_font);
   1037 	bv->axial_scale_bar_active   = add_variable(ui, menu, arena, s8("Axial Scale Bar"),
   1038 	                                            V_INPUT|V_RADIO_BUTTON, VT_B32, ui->small_font);
   1039 	bv->lateral_scale_bar_active = add_variable(ui, menu, arena, s8("Lateral Scale Bar"),
   1040 	                                            V_INPUT|V_RADIO_BUTTON, VT_B32, ui->small_font);
   1041 
   1042 	menu->u.group.last->next = old_menu_first;
   1043 	menu->u.group.last       = old_menu_last;
   1044 
   1045 	return result;
   1046 }
   1047 
   1048 function Variable *
   1049 add_compute_progress_bar(Variable *parent, BeamformerCtx *ctx)
   1050 {
   1051 	BeamformerUI *ui = ctx->ui;
   1052 	/* TODO(rnp): this can be closable once we have a way of opening new views */
   1053 	Variable *result = add_ui_view(ui, parent, &ui->arena, s8(""), UI_VIEW_CUSTOM_TEXT, 0);
   1054 	result->u.view.child = add_variable(ui, result, &ui->arena, s8(""), 0,
   1055 	                                    VT_COMPUTE_PROGRESS_BAR, ui->small_font);
   1056 	ComputeProgressBar *bar = &result->u.view.child->u.compute_progress_bar;
   1057 	bar->progress   = &ctx->csctx.processing_progress;
   1058 	bar->processing = &ctx->csctx.processing_compute;
   1059 
   1060 	return result;
   1061 }
   1062 
   1063 function Variable *
   1064 add_compute_stats_view(BeamformerUI *ui, Variable *parent, Arena *arena, VariableType type)
   1065 {
   1066 	/* TODO(rnp): this can be closable once we have a way of opening new views */
   1067 	Variable *result     = add_ui_view(ui, parent, arena, s8(""), UI_VIEW_CUSTOM_TEXT, 0);
   1068 	result->u.view.child = add_variable(ui, result, &ui->arena, s8(""), 0, type, ui->small_font);
   1069 	return result;
   1070 }
   1071 
   1072 function Variable *
   1073 ui_split_region(BeamformerUI *ui, Variable *region, Variable *split_side, RegionSplitDirection direction)
   1074 {
   1075 	Variable *result = add_ui_split(ui, region, &ui->arena, s8(""), 0.5, direction, ui->small_font);
   1076 	if (split_side == region->u.region_split.left) {
   1077 		region->u.region_split.left  = result;
   1078 	} else {
   1079 		region->u.region_split.right = result;
   1080 	}
   1081 	split_side->parent = result;
   1082 	result->u.region_split.left = split_side;
   1083 	return result;
   1084 }
   1085 
   1086 function void
   1087 ui_fill_live_frame_view(BeamformerUI *ui, BeamformerFrameView *bv)
   1088 {
   1089 	ScaleBar *lateral = &bv->lateral_scale_bar.u.scale_bar;
   1090 	ScaleBar *axial   = &bv->axial_scale_bar.u.scale_bar;
   1091 	lateral->min_value = ui->params.output_min_coordinate + 0;
   1092 	lateral->max_value = ui->params.output_max_coordinate + 0;
   1093 	axial->min_value   = ui->params.output_min_coordinate + 2;
   1094 	axial->max_value   = ui->params.output_max_coordinate + 2;
   1095 	bv->axial_scale_bar_active->u.b32   = 1;
   1096 	bv->lateral_scale_bar_active->u.b32 = 1;
   1097 	bv->ctx = ui->frame_view_render_context;
   1098 	bv->axial_scale_bar.flags   |= V_CAUSES_COMPUTE;
   1099 	bv->lateral_scale_bar.flags |= V_CAUSES_COMPUTE;
   1100 }
   1101 
   1102 function void
   1103 ui_add_live_frame_view(BeamformerUI *ui, Variable *view, RegionSplitDirection direction)
   1104 {
   1105 	Variable *region = view->parent;
   1106 	ASSERT(region->type == VT_UI_REGION_SPLIT);
   1107 	ASSERT(view->type   == VT_UI_VIEW);
   1108 
   1109 	Variable *new_region = ui_split_region(ui, region, view, direction);
   1110 	new_region->u.region_split.right = add_beamformer_frame_view(ui, new_region, &ui->arena, FVT_LATEST, 1);
   1111 
   1112 	ui_fill_live_frame_view(ui, new_region->u.region_split.right->u.group.first->u.generic);
   1113 }
   1114 
   1115 function void
   1116 ui_copy_frame(BeamformerUI *ui, Variable *view, RegionSplitDirection direction)
   1117 {
   1118 	Variable *region = view->parent;
   1119 	ASSERT(region->type == VT_UI_REGION_SPLIT);
   1120 	ASSERT(view->type   == VT_UI_VIEW);
   1121 
   1122 	BeamformerFrameView *old = view->u.group.first->u.generic;
   1123 	/* TODO(rnp): hack; it would be better if this was unreachable with a 0 old->frame */
   1124 	if (!old->frame)
   1125 		return;
   1126 
   1127 	Variable *new_region = ui_split_region(ui, region, view, direction);
   1128 	new_region->u.region_split.right = add_beamformer_frame_view(ui, new_region, &ui->arena, FVT_COPY, 1);
   1129 
   1130 	BeamformerFrameView *bv = new_region->u.region_split.right->u.group.first->u.generic;
   1131 	ScaleBar *lateral  = &bv->lateral_scale_bar.u.scale_bar;
   1132 	ScaleBar *axial    = &bv->axial_scale_bar.u.scale_bar;
   1133 	lateral->min_value = &bv->min_coordinate.x;
   1134 	lateral->max_value = &bv->max_coordinate.x;
   1135 	axial->min_value   = &bv->min_coordinate.z;
   1136 	axial->max_value   = &bv->max_coordinate.z;
   1137 
   1138 	bv->ctx                 = old->ctx;
   1139 	bv->needs_update        = 1;
   1140 	bv->threshold.u.f32     = old->threshold.u.f32;
   1141 	bv->dynamic_range.u.f32 = old->dynamic_range.u.f32;
   1142 	bv->gamma.u.f32         = old->gamma.u.f32;
   1143 	bv->log_scale->u.b32    = old->log_scale->u.b32;
   1144 	bv->min_coordinate      = old->frame->min_coordinate;
   1145 	bv->max_coordinate      = old->frame->max_coordinate;
   1146 
   1147 	bv->frame = SLLPop(ui->frame_freelist);
   1148 	if (!bv->frame) bv->frame = push_struct(&ui->arena, typeof(*bv->frame));
   1149 
   1150 	mem_copy(bv->frame, old->frame, sizeof(*bv->frame));
   1151 	bv->frame->texture = 0;
   1152 	bv->frame->next    = 0;
   1153 	alloc_beamform_frame(0, bv->frame, 0, old->frame->dim, s8("Frame Copy: "), ui->arena);
   1154 
   1155 	glCopyImageSubData(old->frame->texture, GL_TEXTURE_3D, 0, 0, 0, 0,
   1156 	                   bv->frame->texture,  GL_TEXTURE_3D, 0, 0, 0, 0,
   1157 	                   bv->frame->dim.x, bv->frame->dim.y, bv->frame->dim.z);
   1158 	glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT);
   1159 	/* TODO(rnp): x vs y here */
   1160 	resize_frame_view(bv, (uv2){.x = bv->frame->dim.x, .y = bv->frame->dim.z});
   1161 }
   1162 
   1163 function b32
   1164 view_update(BeamformerUI *ui, BeamformerFrameView *view)
   1165 {
   1166 	if (view->type == FVT_LATEST) {
   1167 		u32 index = *view->cycler->u.cycler.state;
   1168 		view->needs_update |= view->frame != ui->latest_plane[index];
   1169 		view->frame         = ui->latest_plane[index];
   1170 		if (view->needs_update) {
   1171 			view->min_coordinate = v4_from_f32_array(ui->params.output_min_coordinate);
   1172 			view->max_coordinate = v4_from_f32_array(ui->params.output_max_coordinate);
   1173 		}
   1174 	}
   1175 
   1176 	/* TODO(rnp): x-z or y-z */
   1177 	/* TODO(rnp): add method of setting a target size in frame view */
   1178 	uv2 current = view->texture_dim;
   1179 	uv2 target  = {.w = ui->params.output_points[0], .h = ui->params.output_points[2]};
   1180 	if (view->type != FVT_COPY && !uv2_equal(current, target) && !uv2_equal(target, (uv2){0})) {
   1181 		resize_frame_view(view, target);
   1182 		view->needs_update = 1;
   1183 	}
   1184 
   1185 	return (view->ctx->updated || view->needs_update) && view->frame;
   1186 }
   1187 
   1188 function void
   1189 update_frame_views(BeamformerUI *ui, Rect window)
   1190 {
   1191 	b32 fbo_bound = 0;
   1192 	for (BeamformerFrameView *view = ui->views; view; view = view->next) {
   1193 		if (view_update(ui, view)) {
   1194 			if (!fbo_bound) {
   1195 				fbo_bound = 1;
   1196 				glBindFramebuffer(GL_FRAMEBUFFER, view->ctx->framebuffer);
   1197 				glUseProgram(view->ctx->shader);
   1198 				glBindVertexArray(view->ctx->vao);
   1199 				glClearColor(0.79, 0.46, 0.77, 1);
   1200 			}
   1201 			glViewport(0, 0, view->texture_dim.w, view->texture_dim.h);
   1202 			glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
   1203 			                       GL_TEXTURE_2D, view->texture, 0);
   1204 			glClear(GL_COLOR_BUFFER_BIT);
   1205 			glBindTextureUnit(0, view->frame->texture);
   1206 			glUniform1f(FRAME_VIEW_RENDER_DYNAMIC_RANGE_LOC, view->dynamic_range.u.f32);
   1207 			glUniform1f(FRAME_VIEW_RENDER_THRESHOLD_LOC,     view->threshold.u.f32);
   1208 			glUniform1f(FRAME_VIEW_RENDER_GAMMA_LOC,         view->gamma.u.scaled_f32.val);
   1209 			glUniform1ui(FRAME_VIEW_RENDER_LOG_SCALE_LOC,    view->log_scale->u.b32);
   1210 
   1211 			glDrawArrays(GL_TRIANGLES, 0, 6);
   1212 			glGenerateTextureMipmap(view->texture);
   1213 			view->needs_update = 0;
   1214 		}
   1215 	}
   1216 	if (fbo_bound) {
   1217 		glBindFramebuffer(GL_FRAMEBUFFER, 0);
   1218 		glViewport(window.pos.x, window.pos.y, window.size.w, window.size.h);
   1219 		/* NOTE(rnp): I don't trust raylib to not mess with us */
   1220 		glBindVertexArray(0);
   1221 	}
   1222 }
   1223 
   1224 function b32
   1225 frame_view_ready_to_present(BeamformerFrameView *view)
   1226 {
   1227 	return !uv2_equal((uv2){0}, view->texture_dim) && view->frame;
   1228 }
   1229 
   1230 function Color
   1231 colour_from_normalized(v4 rgba)
   1232 {
   1233 	return (Color){.r = rgba.r * 255.0f, .g = rgba.g * 255.0f,
   1234 	               .b = rgba.b * 255.0f, .a = rgba.a * 255.0f};
   1235 }
   1236 
   1237 function Color
   1238 fade(Color a, f32 visibility)
   1239 {
   1240 	a.a = (u8)((f32)a.a * visibility);
   1241 	return a;
   1242 }
   1243 
   1244 function v4
   1245 lerp_v4(v4 a, v4 b, f32 t)
   1246 {
   1247 	return (v4){
   1248 		.x = a.x + t * (b.x - a.x),
   1249 		.y = a.y + t * (b.y - a.y),
   1250 		.z = a.z + t * (b.z - a.z),
   1251 		.w = a.w + t * (b.w - a.w),
   1252 	};
   1253 }
   1254 
   1255 function s8
   1256 push_das_shader_kind(Stream *s, DASShaderKind shader, u32 transmit_count)
   1257 {
   1258 	#define X(type, id, pretty, fixed_tx) s8(pretty),
   1259 	read_only local_persist s8 pretty_names[DASShaderKind_Count + 1] = {DAS_TYPES s8("Invalid")};
   1260 	#undef X
   1261 	#define X(type, id, pretty, fixed_tx) fixed_tx,
   1262 	read_only local_persist u8 fixed_transmits[DASShaderKind_Count + 1] = {DAS_TYPES 0};
   1263 	#undef X
   1264 
   1265 	stream_append_s8(s, pretty_names[MIN(shader, DASShaderKind_Count)]);
   1266 	if (!fixed_transmits[MIN(shader, DASShaderKind_Count)]) {
   1267 		stream_append_byte(s, '-');
   1268 		stream_append_u64(s, transmit_count);
   1269 	}
   1270 
   1271 	return stream_to_s8(s);
   1272 }
   1273 
   1274 function s8
   1275 push_custom_view_title(Stream *s, Variable *var)
   1276 {
   1277 	switch (var->type) {
   1278 	case VT_COMPUTE_STATS_VIEW:
   1279 	case VT_COMPUTE_LATEST_STATS_VIEW: {
   1280 		stream_append_s8(s, s8("Compute Stats"));
   1281 		if (var->type == VT_COMPUTE_LATEST_STATS_VIEW)
   1282 			stream_append_s8(s, s8(": Live"));
   1283 	} break;
   1284 	case VT_COMPUTE_PROGRESS_BAR: {
   1285 		stream_append_s8(s, s8("Compute Progress: "));
   1286 		stream_append_f64(s, 100 * *var->u.compute_progress_bar.progress, 100);
   1287 		stream_append_byte(s, '%');
   1288 	} break;
   1289 	case VT_BEAMFORMER_FRAME_VIEW: {
   1290 		BeamformerFrameView *bv = var->u.generic;
   1291 		stream_append_s8(s, s8("Frame View"));
   1292 		switch (bv->type) {
   1293 		case FVT_COPY: stream_append_s8(s, s8(": Copy [")); break;
   1294 		case FVT_LATEST: {
   1295 			#define X(plane, id, pretty) s8(": " pretty " ["),
   1296 			local_persist s8 labels[IPT_LAST + 1] = { IMAGE_PLANE_TAGS s8(": Live [") };
   1297 			#undef X
   1298 			stream_append_s8(s, labels[*bv->cycler->u.cycler.state % (IPT_LAST + 1)]);
   1299 		} break;
   1300 		case FVT_INDEXED: {
   1301 			stream_append_s8(s, s8(": Index {"));
   1302 			stream_append_u64(s, *bv->cycler->u.cycler.state % MAX_BEAMFORMED_SAVED_FRAMES);
   1303 			stream_append_s8(s, s8("} ["));
   1304 		} break;
   1305 		}
   1306 		stream_append_hex_u64(s, bv->frame? bv->frame->id : 0);
   1307 		stream_append_byte(s, ']');
   1308 	} break;
   1309 	default: INVALID_CODE_PATH;
   1310 	}
   1311 	return stream_to_s8(s);
   1312 }
   1313 
   1314 function v2
   1315 draw_text_base(Font font, s8 text, v2 pos, Color colour)
   1316 {
   1317 	v2 off = pos;
   1318 	for (iz i = 0; i < text.len; i++) {
   1319 		/* NOTE: assumes font glyphs are ordered ASCII */
   1320 		i32 idx = text.data[i] - 0x20;
   1321 		Rectangle dst = {
   1322 			off.x + font.glyphs[idx].offsetX - font.glyphPadding,
   1323 			off.y + font.glyphs[idx].offsetY - font.glyphPadding,
   1324 			font.recs[idx].width  + 2.0f * font.glyphPadding,
   1325 			font.recs[idx].height + 2.0f * font.glyphPadding
   1326 		};
   1327 		Rectangle src = {
   1328 			font.recs[idx].x - font.glyphPadding,
   1329 			font.recs[idx].y - font.glyphPadding,
   1330 			font.recs[idx].width  + 2.0f * font.glyphPadding,
   1331 			font.recs[idx].height + 2.0f * font.glyphPadding
   1332 		};
   1333 		DrawTexturePro(font.texture, src, dst, (Vector2){0}, 0, colour);
   1334 
   1335 		off.x += font.glyphs[idx].advanceX;
   1336 		if (font.glyphs[idx].advanceX == 0)
   1337 			off.x += font.recs[idx].width;
   1338 	}
   1339 	v2 result = {.x = off.x - pos.x, .y = font.baseSize};
   1340 	return result;
   1341 }
   1342 
   1343 /* NOTE(rnp): expensive but of the available options in raylib this gives the best results */
   1344 function v2
   1345 draw_outlined_text(s8 text, v2 pos, TextSpec *ts)
   1346 {
   1347 	f32 ow = ts->outline_thick;
   1348 	Color outline = colour_from_normalized(ts->outline_colour);
   1349 	Color colour  = colour_from_normalized(ts->colour);
   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 	draw_text_base(*ts->font, text, sub_v2(pos, (v2){.x = -ow, .y = -ow}), outline);
   1354 
   1355 	v2 result = draw_text_base(*ts->font, text, pos, colour);
   1356 
   1357 	return result;
   1358 }
   1359 
   1360 function v2
   1361 draw_text(s8 text, v2 pos, TextSpec *ts)
   1362 {
   1363 	if (ts->flags & TF_ROTATED) {
   1364 		rlPushMatrix();
   1365 		rlTranslatef(pos.x, pos.y, 0);
   1366 		rlRotatef(ts->rotation, 0, 0, 1);
   1367 		pos = (v2){0};
   1368 	}
   1369 
   1370 	v2 result   = measure_text(*ts->font, text);
   1371 	/* TODO(rnp): the size of this should be stored for each font */
   1372 	s8 ellipsis = s8("...");
   1373 	b32 clamped = ts->flags & TF_LIMITED && result.w > ts->limits.size.w;
   1374 	if (clamped) {
   1375 		f32 ellipsis_width = measure_text(*ts->font, ellipsis).x;
   1376 		if (ellipsis_width < ts->limits.size.w) {
   1377 			text = clamp_text_to_width(*ts->font, text, ts->limits.size.w - ellipsis_width);
   1378 		} else {
   1379 			text.len     = 0;
   1380 			ellipsis.len = 0;
   1381 		}
   1382 	}
   1383 
   1384 	Color colour = colour_from_normalized(ts->colour);
   1385 	if (ts->flags & TF_OUTLINED) result.x = draw_outlined_text(text, pos, ts).x;
   1386 	else                         result.x = draw_text_base(*ts->font, text, pos, colour).x;
   1387 
   1388 	if (clamped) {
   1389 		pos.x += result.x;
   1390 		if (ts->flags & TF_OUTLINED) result.x += draw_outlined_text(ellipsis, pos, ts).x;
   1391 		else                         result.x += draw_text_base(*ts->font, ellipsis, pos,
   1392 		                                                        colour).x;
   1393 	}
   1394 
   1395 	if (ts->flags & TF_ROTATED) rlPopMatrix();
   1396 
   1397 	return result;
   1398 }
   1399 
   1400 function Rect
   1401 extend_rect_centered(Rect r, v2 delta)
   1402 {
   1403 	r.size.w += delta.x;
   1404 	r.size.h += delta.y;
   1405 	r.pos.x  -= delta.x / 2;
   1406 	r.pos.y  -= delta.y / 2;
   1407 	return r;
   1408 }
   1409 
   1410 function Rect
   1411 shrink_rect_centered(Rect r, v2 delta)
   1412 {
   1413 	delta.x   = MIN(delta.x, r.size.w);
   1414 	delta.y   = MIN(delta.y, r.size.h);
   1415 	r.size.w -= delta.x;
   1416 	r.size.h -= delta.y;
   1417 	r.pos.x  += delta.x / 2;
   1418 	r.pos.y  += delta.y / 2;
   1419 	return r;
   1420 }
   1421 
   1422 function Rect
   1423 scale_rect_centered(Rect r, v2 scale)
   1424 {
   1425 	Rect or   = r;
   1426 	r.size.w *= scale.x;
   1427 	r.size.h *= scale.y;
   1428 	r.pos.x  += (or.size.w - r.size.w) / 2;
   1429 	r.pos.y  += (or.size.h - r.size.h) / 2;
   1430 	return r;
   1431 }
   1432 
   1433 function b32
   1434 point_in_rect(v2 p, Rect r)
   1435 {
   1436 	v2  end    = add_v2(r.pos, r.size);
   1437 	b32 result = BETWEEN(p.x, r.pos.x, end.x) & BETWEEN(p.y, r.pos.y, end.y);
   1438 	return result;
   1439 }
   1440 
   1441 function v2
   1442 screen_point_to_world_2d(v2 p, v2 screen_min, v2 screen_max, v2 world_min, v2 world_max)
   1443 {
   1444 	v2 pixels_to_m = div_v2(sub_v2(world_max, world_min), sub_v2(screen_max, screen_min));
   1445 	v2 result      = add_v2(mul_v2(sub_v2(p, screen_min), pixels_to_m), world_min);
   1446 	return result;
   1447 }
   1448 
   1449 function v2
   1450 world_point_to_screen_2d(v2 p, v2 world_min, v2 world_max, v2 screen_min, v2 screen_max)
   1451 {
   1452 	v2 m_to_pixels = div_v2(sub_v2(screen_max, screen_min), sub_v2(world_max, world_min));
   1453 	v2 result      = add_v2(mul_v2(sub_v2(p, world_min), m_to_pixels), screen_min);
   1454 	return result;
   1455 }
   1456 
   1457 function b32
   1458 hover_rect(v2 mouse, Rect rect, f32 *hover_t)
   1459 {
   1460 	b32 hovering = point_in_rect(mouse, rect);
   1461 	if (hovering) *hover_t += HOVER_SPEED * dt_for_frame;
   1462 	else          *hover_t -= HOVER_SPEED * dt_for_frame;
   1463 	*hover_t = CLAMP01(*hover_t);
   1464 	return hovering;
   1465 }
   1466 
   1467 function b32
   1468 hover_var(BeamformerUI *ui, v2 mouse, Rect rect, Variable *var)
   1469 {
   1470 	b32 result = 0;
   1471 	if (ui->interaction.type != IT_DRAG || ui->interaction.active == var) {
   1472 		result = hover_rect(mouse, rect, &var->hover_t);
   1473 		if (result) {
   1474 			ui->interaction.hot_rect = rect;
   1475 			ui->interaction.hot      = var;
   1476 		}
   1477 	}
   1478 	return result;
   1479 }
   1480 
   1481 function Rect
   1482 draw_title_bar(BeamformerUI *ui, Arena arena, Variable *ui_view, Rect r, v2 mouse)
   1483 {
   1484 	ASSERT(ui_view->type == VT_UI_VIEW);
   1485 	UIView *view = &ui_view->u.view;
   1486 
   1487 	s8 title = ui_view->name;
   1488 	if (view->flags & UI_VIEW_CUSTOM_TEXT) {
   1489 		Stream buf = arena_stream(arena);
   1490 		push_custom_view_title(&buf, ui_view->u.group.first);
   1491 		title = arena_stream_commit(&arena, &buf);
   1492 	}
   1493 
   1494 	Rect result, title_rect;
   1495 	cut_rect_vertical(r, ui->small_font.baseSize + TITLE_BAR_PAD, &title_rect, &result);
   1496 	cut_rect_vertical(result, LISTING_LINE_PAD, 0, &result);
   1497 
   1498 	DrawRectangleRec(title_rect.rl, BLACK);
   1499 
   1500 	title_rect = shrink_rect_centered(title_rect, (v2){.x = 1.5 * TITLE_BAR_PAD});
   1501 	DrawRectangleRounded(title_rect.rl, 0.5, 0, fade(colour_from_normalized(BG_COLOUR), 0.55));
   1502 	title_rect = shrink_rect_centered(title_rect, (v2){.x = 3 * TITLE_BAR_PAD});
   1503 
   1504 	if (view->close) {
   1505 		Rect close;
   1506 		cut_rect_horizontal(title_rect, title_rect.size.w - title_rect.size.h, &title_rect, &close);
   1507 		hover_var(ui, mouse, close, view->close);
   1508 
   1509 		Color colour = colour_from_normalized(lerp_v4(MENU_CLOSE_COLOUR, FG_COLOUR, view->close->hover_t));
   1510 		close = shrink_rect_centered(close, (v2){.x = 16, .y = 16});
   1511 		DrawLineEx(close.pos.rl, add_v2(close.pos, close.size).rl, 4, colour);
   1512 		DrawLineEx(add_v2(close.pos, (v2){.x = close.size.w}).rl,
   1513 		           add_v2(close.pos, (v2){.y = close.size.h}).rl,  4, colour);
   1514 	}
   1515 
   1516 	if (view->menu) {
   1517 		Rect menu;
   1518 		cut_rect_horizontal(title_rect, title_rect.size.w - title_rect.size.h, &title_rect, &menu);
   1519 		if (hover_var(ui, mouse, menu, view->menu))
   1520 			ui->interaction.hot_font = &ui->small_font;
   1521 
   1522 		Color colour = colour_from_normalized(lerp_v4(MENU_PLUS_COLOUR, FG_COLOUR, view->menu->hover_t));
   1523 		menu = shrink_rect_centered(menu, (v2){.x = 14, .y = 14});
   1524 		DrawLineEx(add_v2(menu.pos, (v2){.x = menu.size.w / 2}).rl,
   1525 		           add_v2(menu.pos, (v2){.x = menu.size.w / 2, .y = menu.size.h}).rl, 4, colour);
   1526 		DrawLineEx(add_v2(menu.pos, (v2){.y = menu.size.h / 2}).rl,
   1527 		           add_v2(menu.pos, (v2){.x = menu.size.w, .y = menu.size.h / 2}).rl, 4, colour);
   1528 	}
   1529 
   1530 	v2 title_pos = title_rect.pos;
   1531 	title_pos.y += 0.5 * TITLE_BAR_PAD;
   1532 	TextSpec text_spec = {.font = &ui->small_font, .flags = TF_LIMITED, .colour = FG_COLOUR,
   1533 	                      .limits.size = title_rect.size};
   1534 	draw_text(title, title_pos, &text_spec);
   1535 
   1536 	return result;
   1537 }
   1538 
   1539 /* TODO(rnp): once this has more callers decide if it would be better for this to take
   1540  * an orientation rather than force CCW/right-handed */
   1541 function void
   1542 draw_ruler(BeamformerUI *ui, Arena arena, v2 start_point, v2 end_point,
   1543            f32 start_value, f32 end_value, f32 *markers, u32 marker_count,
   1544            u32 segments, s8 suffix, v4 marker_colour, v4 txt_colour)
   1545 {
   1546 	b32 draw_plus = SIGN(start_value) != SIGN(end_value);
   1547 
   1548 	end_point    = sub_v2(end_point, start_point);
   1549 	f32 rotation = atan2_f32(end_point.y, end_point.x) * 180 / PI;
   1550 
   1551 	rlPushMatrix();
   1552 	rlTranslatef(start_point.x, start_point.y, 0);
   1553 	rlRotatef(rotation, 0, 0, 1);
   1554 
   1555 	f32 inc       = magnitude_v2(end_point) / segments;
   1556 	f32 value_inc = (end_value - start_value) / segments;
   1557 	f32 value     = start_value;
   1558 
   1559 	Stream buf = arena_stream(arena);
   1560 	v2 sp = {0}, ep = {.y = RULER_TICK_LENGTH};
   1561 	v2 tp = {.x = ui->small_font.baseSize / 2, .y = ep.y + RULER_TEXT_PAD};
   1562 	TextSpec text_spec = {.font = &ui->small_font, .rotation = 90, .colour = txt_colour, .flags = TF_ROTATED};
   1563 	Color rl_txt_colour = colour_from_normalized(txt_colour);
   1564 	for (u32 j = 0; j <= segments; j++) {
   1565 		DrawLineEx(sp.rl, ep.rl, 3, rl_txt_colour);
   1566 
   1567 		stream_reset(&buf, 0);
   1568 		if (draw_plus && value > 0) stream_append_byte(&buf, '+');
   1569 		stream_append_f64(&buf, value, 10);
   1570 		stream_append_s8(&buf, suffix);
   1571 		draw_text(stream_to_s8(&buf), tp, &text_spec);
   1572 
   1573 		value += value_inc;
   1574 		sp.x  += inc;
   1575 		ep.x  += inc;
   1576 		tp.x  += inc;
   1577 	}
   1578 
   1579 	Color rl_marker_colour = colour_from_normalized(marker_colour);
   1580 	ep.y += RULER_TICK_LENGTH;
   1581 	for (u32 i = 0; i < marker_count; i++) {
   1582 		if (markers[i] < F32_INFINITY) {
   1583 			ep.x  = sp.x = markers[i];
   1584 			DrawLineEx(sp.rl, ep.rl, 3, rl_marker_colour);
   1585 			DrawCircleV(ep.rl, 3, rl_marker_colour);
   1586 		}
   1587 	}
   1588 
   1589 	rlPopMatrix();
   1590 }
   1591 
   1592 function void
   1593 do_scale_bar(BeamformerUI *ui, Arena arena, Variable *scale_bar, v2 mouse, Rect draw_rect,
   1594              f32 start_value, f32 end_value, s8 suffix)
   1595 {
   1596 	ASSERT(scale_bar->type == VT_SCALE_BAR);
   1597 	ScaleBar *sb = &scale_bar->u.scale_bar;
   1598 
   1599 	v2 txt_s = measure_text(ui->small_font, s8("-288.8 mm"));
   1600 
   1601 	Rect tick_rect = draw_rect;
   1602 	v2   start_pos = tick_rect.pos;
   1603 	v2   end_pos   = tick_rect.pos;
   1604 	v2   relative_mouse = sub_v2(mouse, tick_rect.pos);
   1605 
   1606 	f32  markers[2];
   1607 	u32  marker_count = 1;
   1608 
   1609 	v2 world_zoom_point  = {{sb->zoom_starting_coord, sb->zoom_starting_coord}};
   1610 	v2 screen_zoom_point = world_point_to_screen_2d(world_zoom_point,
   1611 	                                                (v2){{*sb->min_value, *sb->min_value}},
   1612 	                                                (v2){{*sb->max_value, *sb->max_value}},
   1613 	                                                (v2){0}, tick_rect.size);
   1614 	u32  tick_count;
   1615 	if (sb->direction == SB_AXIAL) {
   1616 		tick_rect.size.x  = RULER_TEXT_PAD + RULER_TICK_LENGTH + txt_s.x;
   1617 		tick_count        = tick_rect.size.y / (1.5 * ui->small_font.baseSize);
   1618 		start_pos.y      += tick_rect.size.y;
   1619 		markers[0]        = tick_rect.size.y - screen_zoom_point.y;
   1620 		markers[1]        = tick_rect.size.y - relative_mouse.y;
   1621 	} else {
   1622 		tick_rect.size.y  = RULER_TEXT_PAD + RULER_TICK_LENGTH + txt_s.x;
   1623 		tick_count        = tick_rect.size.x / (1.5 * ui->small_font.baseSize);
   1624 		end_pos.x        += tick_rect.size.x;
   1625 		markers[0]        = screen_zoom_point.x;
   1626 		markers[1]        = relative_mouse.x;
   1627 	}
   1628 
   1629 	if (hover_var(ui, mouse, tick_rect, scale_bar))
   1630 		marker_count = 2;
   1631 
   1632 	draw_ruler(ui, arena, start_pos, end_pos, start_value, end_value, markers, marker_count,
   1633 	           tick_count, suffix, RULER_COLOUR, lerp_v4(FG_COLOUR, HOVERED_COLOUR, scale_bar->hover_t));
   1634 }
   1635 
   1636 function v2
   1637 draw_radio_button(BeamformerUI *ui, Variable *var, v2 at, v2 mouse, v4 base_colour, f32 size)
   1638 {
   1639 	ASSERT(var->type == VT_B32 || var->type == VT_BEAMFORMER_VARIABLE);
   1640 	b32 value;
   1641 	if (var->type == VT_B32) {
   1642 		value = var->u.b32;
   1643 	} else {
   1644 		ASSERT(var->u.beamformer_variable.store_type == VT_B32);
   1645 		value = *(b32 *)var->u.beamformer_variable.store;
   1646 	}
   1647 
   1648 	v2 result = (v2){.x = size, .y = size};
   1649 	Rect hover_rect   = {.pos = at, .size = result};
   1650 	hover_rect.pos.y += 1;
   1651 	hover_var(ui, mouse, hover_rect, var);
   1652 
   1653 	hover_rect = shrink_rect_centered(hover_rect, (v2){.x = 8, .y = 8});
   1654 	Rect inner = shrink_rect_centered(hover_rect, (v2){.x = 4, .y = 4});
   1655 	v4 fill = lerp_v4(value? base_colour : (v4){0}, HOVERED_COLOUR, var->hover_t);
   1656 	DrawRectangleRoundedLinesEx(hover_rect.rl, 0.2, 0, 2, colour_from_normalized(base_colour));
   1657 	DrawRectangleRec(inner.rl, colour_from_normalized(fill));
   1658 
   1659 	return result;
   1660 }
   1661 
   1662 function v2
   1663 draw_variable(BeamformerUI *ui, Arena arena, Variable *var, v2 at, v2 mouse, v4 base_colour, TextSpec text_spec)
   1664 {
   1665 	v2 result;
   1666 	if (var->flags & V_RADIO_BUTTON) {
   1667 		result = draw_radio_button(ui, var, at, mouse, base_colour, text_spec.font->baseSize);
   1668 	} else {
   1669 		Stream buf = arena_stream(arena);
   1670 		stream_append_variable(&buf, var);
   1671 		s8 text = arena_stream_commit(&arena, &buf);
   1672 		result = measure_text(*text_spec.font, text);
   1673 
   1674 		if (var->flags & V_INPUT) {
   1675 			Rect text_rect = {.pos = at, .size = result};
   1676 			text_rect = extend_rect_centered(text_rect, (v2){.x = 8});
   1677 			if (hover_var(ui, mouse, text_rect, var) && (var->flags & V_TEXT))
   1678 				ui->interaction.hot_font = text_spec.font;
   1679 			text_spec.colour = lerp_v4(base_colour, HOVERED_COLOUR, var->hover_t);
   1680 		}
   1681 
   1682 		draw_text(text, at, &text_spec);
   1683 	}
   1684 	return result;
   1685 }
   1686 
   1687 function v2
   1688 draw_table_cell(BeamformerUI *ui, TableCell *cell, Rect cell_rect, TextAlignment alignment,
   1689                 TextSpec ts, v2 mouse)
   1690 {
   1691 	/* NOTE(rnp): use desired width for alignment and clamped width for drawing */
   1692 	f32 start_x = cell_rect.pos.x;
   1693 	v2 cell_at  = table_cell_align(cell, alignment, cell_rect);
   1694 	ts.limits.size.w -= (cell_at.x - start_x);
   1695 	cell_rect.size.w  = MIN(ts.limits.size.w, cell_rect.size.w);
   1696 
   1697 	v4 base_colour = ts.colour;
   1698 	if (cell->kind == TCK_VARIABLE && cell->var->flags & V_INPUT) {
   1699 		Rect hover = {.pos = cell_at, .size = {.w = cell->width, .h = cell_rect.size.h}};
   1700 		if (hover_var(ui, mouse, hover, cell->var) && (cell->var->flags & V_TEXT))
   1701 			ui->interaction.hot_font = ts.font;
   1702 		ts.colour = lerp_v4(ts.colour, HOVERED_COLOUR, cell->var->hover_t);
   1703 	}
   1704 
   1705 	/* TODO(rnp): push truncated text for hovering */
   1706 	if (cell->kind == TCK_VARIABLE && cell->var->flags & V_RADIO_BUTTON)
   1707 		draw_radio_button(ui, cell->var, cell_at, mouse, base_colour, ts.font->baseSize);
   1708 	else if (cell->text.len)
   1709 		draw_text(cell->text, cell_at, &ts);
   1710 	/* TODO(rnp): draw column border */
   1711 
   1712 	return cell_rect.size;
   1713 }
   1714 
   1715 function v2
   1716 draw_table_row(BeamformerUI *ui, Arena arena, TableCell *cells, TextAlignment *cell_alignments,
   1717                f32 *widths, i32 cell_count, Rect draw_rect, TextSpec ts, v2 mouse)
   1718 {
   1719 	Rect cell_rect = {.pos = draw_rect.pos, .size.h = draw_rect.size.h};
   1720 	for (i32 i = 0; i < cell_count; i++) {
   1721 		TableCell *cell  = cells + i;
   1722 		cell_rect.size.w = widths[i];
   1723 
   1724 		f32 dw = draw_table_cell(ui, cell, cell_rect, cell_alignments[i], ts, mouse).w;
   1725 		cell_rect.pos.x  += dw;
   1726 		ts.limits.size.w -= dw;
   1727 	}
   1728 	return (v2){.x = draw_rect.pos.x - cell_rect.pos.x, .y = draw_rect.size.h};
   1729 }
   1730 
   1731 function v2
   1732 draw_table(BeamformerUI *ui, Arena arena, Table *table, Rect draw_rect, TextSpec ts, v2 mouse)
   1733 {
   1734 	ts.flags |= TF_LIMITED;
   1735 	ts.limits.size.w = draw_rect.size.w;
   1736 
   1737 	f32 start_height  = draw_rect.size.h;
   1738 	i32 row_index     = table_skip_rows(table, draw_rect.size.h, ts.font->baseSize);
   1739 	TableIterator *it = table_iterator_new(table, TIK_ROWS, &arena, row_index, (v2){0}, ts.font);
   1740 	for (TableRow *row = table_iterator_next(it, &arena);
   1741 	     row;
   1742 	     row = table_iterator_next(it, &arena))
   1743 	{
   1744 		Table *table    = it->frame.table;
   1745 		Rect row_rect   = draw_rect;
   1746 		row_rect.size.h = ts.font->baseSize + TABLE_CELL_PAD_HEIGHT;
   1747 		f32 h = draw_table_row(ui, arena, row->data, table->alignment, table->widths,
   1748 		                       table->columns, row_rect, ts, mouse).y;
   1749 		draw_rect.pos.y  += h;
   1750 		draw_rect.size.y -= h;
   1751 		/* TODO(rnp): draw row border */
   1752 	}
   1753 	v2 result = {.x = table_width(table), .y = start_height - draw_rect.size.h};
   1754 	return result;
   1755 }
   1756 
   1757 function void
   1758 draw_beamformer_frame_view(BeamformerUI *ui, Arena a, Variable *var, Rect display_rect, v2 mouse)
   1759 {
   1760 	ASSERT(var->type == VT_BEAMFORMER_FRAME_VIEW);
   1761 	InteractionState *is      = &ui->interaction;
   1762 	BeamformerFrameView *view = var->u.generic;
   1763 	BeamformFrame *frame      = view->frame;
   1764 
   1765 	v2 txt_s = measure_text(ui->small_font, s8("-288.8 mm"));
   1766 	f32 scale_bar_size = 1.2 * txt_s.x + RULER_TICK_LENGTH;
   1767 
   1768 	v4 min = view->min_coordinate;
   1769 	v4 max = view->max_coordinate;
   1770 	v2 requested_dim = sub_v2(XZ(max), XZ(min));
   1771 	f32 aspect = requested_dim.w / requested_dim.h;
   1772 
   1773 	Rect vr = display_rect;
   1774 	v2 scale_bar_area = {0};
   1775 	if (view->axial_scale_bar_active->u.b32) {
   1776 		vr.pos.y         += 0.5 * ui->small_font.baseSize;
   1777 		scale_bar_area.x += scale_bar_size;
   1778 		scale_bar_area.y += ui->small_font.baseSize;
   1779 	}
   1780 
   1781 	if (view->lateral_scale_bar_active->u.b32) {
   1782 		vr.pos.x         += 0.5 * ui->small_font.baseSize;
   1783 		scale_bar_area.x += ui->small_font.baseSize;
   1784 		scale_bar_area.y += scale_bar_size;
   1785 	}
   1786 
   1787 	vr.size = sub_v2(vr.size, scale_bar_area);
   1788 	if (aspect > 1) vr.size.h = vr.size.w / aspect;
   1789 	else            vr.size.w = vr.size.h * aspect;
   1790 
   1791 	v2 occupied = add_v2(vr.size, scale_bar_area);
   1792 	if (occupied.w > display_rect.size.w) {
   1793 		vr.size.w -= (occupied.w - display_rect.size.w);
   1794 		vr.size.h  = vr.size.w / aspect;
   1795 	} else if (occupied.h > display_rect.size.h) {
   1796 		vr.size.h -= (occupied.h - display_rect.size.h);
   1797 		vr.size.w  = vr.size.h * aspect;
   1798 	}
   1799 	occupied = add_v2(vr.size, scale_bar_area);
   1800 	vr.pos   = add_v2(vr.pos, scale_v2(sub_v2(display_rect.size, occupied), 0.5));
   1801 
   1802 	/* TODO(rnp): make this depend on the requested draw orientation (x-z or y-z or x-y) */
   1803 	v2 output_dim = {
   1804 		.x = frame->max_coordinate.x - frame->min_coordinate.x,
   1805 		.y = frame->max_coordinate.z - frame->min_coordinate.z,
   1806 	};
   1807 
   1808 	v2 pixels_per_meter = {
   1809 		.w = (f32)view->texture_dim.w / output_dim.w,
   1810 		.h = (f32)view->texture_dim.h / output_dim.h,
   1811 	};
   1812 
   1813 	v2 texture_points  = mul_v2(pixels_per_meter, requested_dim);
   1814 	/* TODO(rnp): this also depends on x-y, y-z, x-z */
   1815 	v2 texture_start   = {
   1816 		.x = pixels_per_meter.x * 0.5 * (output_dim.x - requested_dim.x),
   1817 		.y = pixels_per_meter.y * (frame->max_coordinate.z - max.z),
   1818 	};
   1819 
   1820 	Rectangle  tex_r  = {texture_start.x, texture_start.y, texture_points.x, -texture_points.y};
   1821 	NPatchInfo tex_np = { tex_r, 0, 0, 0, 0, NPATCH_NINE_PATCH };
   1822 	DrawTextureNPatch(make_raylib_texture(view), tex_np, vr.rl, (Vector2){0}, 0, WHITE);
   1823 
   1824 	v2 start_pos  = vr.pos;
   1825 	start_pos.y  += vr.size.y;
   1826 
   1827 	if (vr.size.w > 0 && view->lateral_scale_bar_active->u.b32) {
   1828 		do_scale_bar(ui, a, &view->lateral_scale_bar, mouse,
   1829 		             (Rect){.pos = start_pos, .size = vr.size},
   1830 		             *view->lateral_scale_bar.u.scale_bar.min_value * 1e3,
   1831 		             *view->lateral_scale_bar.u.scale_bar.max_value * 1e3, s8(" mm"));
   1832 	}
   1833 
   1834 	start_pos    = vr.pos;
   1835 	start_pos.x += vr.size.x;
   1836 
   1837 	if (vr.size.h > 0 && view->axial_scale_bar_active->u.b32) {
   1838 		do_scale_bar(ui, a, &view->axial_scale_bar, mouse,
   1839 		             (Rect){.pos = start_pos, .size = vr.size},
   1840 		             *view->axial_scale_bar.u.scale_bar.max_value * 1e3,
   1841 		             *view->axial_scale_bar.u.scale_bar.min_value * 1e3, s8(" mm"));
   1842 	}
   1843 
   1844 	TextSpec text_spec = {.font = &ui->small_font, .flags = TF_LIMITED|TF_OUTLINED,
   1845 	                      .colour = RULER_COLOUR, .outline_thick = 1, .outline_colour.a = 1,
   1846 	                      .limits.size.x = vr.size.w};
   1847 
   1848 	f32 draw_table_width = vr.size.w;
   1849 	if (point_in_rect(mouse, vr)) {
   1850 		is->hot      = var;
   1851 		is->hot_rect = vr;
   1852 
   1853 		v2 world = screen_point_to_world_2d(mouse, vr.pos, add_v2(vr.pos, vr.size),
   1854 		                                    XZ(view->min_coordinate),
   1855 		                                    XZ(view->max_coordinate));
   1856 		Stream buf = arena_stream(a);
   1857 		stream_append_v2(&buf, scale_v2(world, 1e3));
   1858 
   1859 		text_spec.limits.size.w -= 4;
   1860 		v2 txt_s = measure_text(*text_spec.font, stream_to_s8(&buf));
   1861 		v2 txt_p = {
   1862 			.x = vr.pos.x + vr.size.w - txt_s.w - 4,
   1863 			.y = vr.pos.y + vr.size.h - txt_s.h - 4,
   1864 		};
   1865 		txt_p.x = MAX(vr.pos.x, txt_p.x);
   1866 		draw_table_width -= draw_text(stream_to_s8(&buf), txt_p, &text_spec).w;
   1867 		text_spec.limits.size.w += 4;
   1868 	}
   1869 
   1870 	{
   1871 		Stream buf = arena_stream(a);
   1872 		s8 shader  = push_das_shader_kind(&buf, frame->das_shader_kind, frame->compound_count);
   1873 		text_spec.font = &ui->font;
   1874 		text_spec.limits.size.w -= 16;
   1875 		v2 txt_s   = measure_text(*text_spec.font, shader);
   1876 		v2 txt_p  = {
   1877 			.x = vr.pos.x + vr.size.w - txt_s.w - 16,
   1878 			.y = vr.pos.y + 4,
   1879 		};
   1880 		txt_p.x = MAX(vr.pos.x, txt_p.x);
   1881 		draw_text(stream_to_s8(&buf), txt_p, &text_spec);
   1882 		text_spec.font = &ui->small_font;
   1883 		text_spec.limits.size.w += 16;
   1884 	}
   1885 
   1886 	if (view->ruler.state != RS_NONE) {
   1887 		v2 vr_max_p = add_v2(vr.pos, vr.size);
   1888 		v2 start_p = world_point_to_screen_2d(view->ruler.start, XZ(view->min_coordinate),
   1889 		                                      XZ(view->max_coordinate), vr.pos, vr_max_p);
   1890 		v2 end_p   = clamp_v2_rect(mouse, vr);
   1891 
   1892 		if (view->ruler.state == RS_HOLD) {
   1893 			end_p = world_point_to_screen_2d(view->ruler.end, XZ(view->min_coordinate),
   1894 			                                 XZ(view->max_coordinate), vr.pos, vr_max_p);
   1895 		}
   1896 
   1897 		v2 start_p_world = view->ruler.start;
   1898 		v2 end_p_world   = screen_point_to_world_2d(end_p, vr.pos, vr_max_p,
   1899 		                                            XZ(view->min_coordinate),
   1900 		                                            XZ(view->max_coordinate));
   1901 		v2 pixel_delta = sub_v2(start_p, end_p);
   1902 		v2 m_delta     = sub_v2(end_p_world, start_p_world);
   1903 
   1904 		Color rl_colour = colour_from_normalized(text_spec.colour);
   1905 		DrawCircleV(start_p.rl, 3, rl_colour);
   1906 		DrawLineEx(end_p.rl, start_p.rl, 2, rl_colour);
   1907 		DrawCircleV(end_p.rl, 3, rl_colour);
   1908 
   1909 		Stream buf = arena_stream(a);
   1910 		stream_append_f64(&buf, 1e3 * magnitude_v2(m_delta), 100);
   1911 		stream_append_s8(&buf, s8(" mm"));
   1912 
   1913 		v2 txt_p = start_p;
   1914 		v2 txt_s = measure_text(*text_spec.font, stream_to_s8(&buf));
   1915 		if (pixel_delta.y < 0) txt_p.y -= txt_s.y;
   1916 		if (pixel_delta.x < 0) txt_p.x -= txt_s.x;
   1917 		draw_text(stream_to_s8(&buf), txt_p, &text_spec);
   1918 	}
   1919 
   1920 	Table *table = table_new(&a, 3, 3, (TextAlignment []){TA_LEFT, TA_LEFT, TA_LEFT});
   1921 	table_push_parameter_row(table, &a, view->gamma.name,     &view->gamma,     s8(""));
   1922 	table_push_parameter_row(table, &a, view->threshold.name, &view->threshold, s8(""));
   1923 	if (view->log_scale->u.b32)
   1924 		table_push_parameter_row(table, &a, view->dynamic_range.name, &view->dynamic_range, s8("[dB]"));
   1925 
   1926 	Rect table_rect = vr;
   1927 	f32 height      = table_extent(table, a, text_spec.font).y;
   1928 	height          = MIN(height, vr.size.h);
   1929 	table_rect.pos.w  += 8;
   1930 	table_rect.pos.y  += vr.size.h - height - 8;
   1931 	table_rect.size.h  = height;
   1932 	table_rect.size.w  = draw_table_width - 16;
   1933 
   1934 	draw_table(ui, a, table, table_rect, text_spec, mouse);
   1935 }
   1936 
   1937 function v2
   1938 draw_compute_progress_bar(BeamformerUI *ui, Arena arena, ComputeProgressBar *state, Rect r)
   1939 {
   1940 	if (*state->processing) state->display_t_velocity += 65 * dt_for_frame;
   1941 	else                    state->display_t_velocity -= 45 * dt_for_frame;
   1942 
   1943 	state->display_t_velocity = CLAMP(state->display_t_velocity, -10, 10);
   1944 	state->display_t += state->display_t_velocity * dt_for_frame;
   1945 	state->display_t  = CLAMP01(state->display_t);
   1946 
   1947 	if (state->display_t > (1.0 / 255.0)) {
   1948 		Rect outline = {.pos = r.pos, .size = {.w = r.size.w, .h = ui->font.baseSize}};
   1949 		outline      = scale_rect_centered(outline, (v2){.x = 0.96, .y = 0.7});
   1950 		Rect filled  = outline;
   1951 		filled.size.w *= *state->progress;
   1952 		DrawRectangleRounded(filled.rl, 2, 0, fade(colour_from_normalized(HOVERED_COLOUR),
   1953 		                                           state->display_t));
   1954 		DrawRectangleRoundedLinesEx(outline.rl, 2, 0, 3, fade(BLACK, state->display_t));
   1955 	}
   1956 
   1957 	v2 result = {.x = r.size.w, .y = ui->font.baseSize};
   1958 	return result;
   1959 }
   1960 
   1961 function v2
   1962 draw_compute_stats_view(BeamformerCtx *ctx, Arena arena, ComputeShaderStats *stats, Rect r)
   1963 {
   1964 	#define X(e, n, s, h, pn) [ComputeShaderKind_##e] = s8(pn ":"),
   1965 	read_only local_persist s8 labels[ComputeShaderKind_Count] = { COMPUTE_SHADERS };
   1966 	#undef X
   1967 
   1968 	BeamformerUI *ui     = ctx->ui;
   1969 	f32 compute_time_sum = 0;
   1970 	u32 stages           = ctx->shared_memory->compute_stages_count;
   1971 	TextSpec text_spec   = {.font = &ui->font, .colour = FG_COLOUR, .flags = TF_LIMITED};
   1972 
   1973 	Table *table = table_new(&arena, stages + 1, 3, (TextAlignment []){TA_LEFT, TA_LEFT, TA_LEFT});
   1974 	for (u32 i = 0; i < stages; i++) {
   1975 		TableCell *cells = table_push_row(table, &arena, TRK_CELLS)->data;
   1976 
   1977 
   1978 		Stream sb = arena_stream(arena);
   1979 		u32 index = ctx->shared_memory->compute_stages[i];
   1980 		compute_time_sum += stats->times[index];
   1981 		stream_append_f64_e(&sb, stats->times[index]);
   1982 
   1983 		cells[0].text = labels[index];
   1984 		cells[1].text = arena_stream_commit(&arena, &sb);
   1985 		cells[2].text = s8("[s]");
   1986 	}
   1987 
   1988 	TableCell *cells = table_push_row(table, &arena, TRK_CELLS)->data;
   1989 	Stream sb = arena_stream(arena);
   1990 	stream_append_f64_e(&sb, compute_time_sum);
   1991 	cells[0].text = s8("Compute Total:");
   1992 	cells[1].text = arena_stream_commit(&arena, &sb);
   1993 	cells[2].text = s8("[s]");
   1994 
   1995 	table_extent(table, arena, text_spec.font);
   1996 	return draw_table(ui, arena, table, r, text_spec, (v2){0});
   1997 }
   1998 
   1999 function v2
   2000 draw_ui_view_listing(BeamformerUI *ui, Variable *group, Arena arena, Rect r, v2 mouse, TextSpec text_spec)
   2001 {
   2002 	ASSERT(group->type == VT_GROUP);
   2003 	Table *table  = table_new(&arena, 0, 3, (TextAlignment []){TA_LEFT, TA_LEFT, TA_RIGHT});
   2004 	/* NOTE(rnp): minimum width for middle column */
   2005 	table->widths[1] = 150;
   2006 
   2007 	Variable *var = group->u.group.first;
   2008 	while (var) {
   2009 		switch (var->type) {
   2010 		case VT_CYCLER:
   2011 		case VT_BEAMFORMER_VARIABLE: {
   2012 			s8 suffix = s8("");
   2013 			if (var->type == VT_BEAMFORMER_VARIABLE)
   2014 				suffix = var->u.beamformer_variable.suffix;
   2015 			table_push_parameter_row(table, &arena, var->name, var, suffix);
   2016 			while (var) {
   2017 				if (var->next) {
   2018 					var = var->next;
   2019 					break;
   2020 				}
   2021 				var   = var->parent;
   2022 				table = table_end_subtable(table);
   2023 			}
   2024 		} break;
   2025 		case VT_GROUP: {
   2026 			VariableGroup *g = &var->u.group;
   2027 
   2028 			TableCell *cells = table_push_row(table, &arena, TRK_CELLS)->data;
   2029 			cells[0] = (TableCell){.text = var->name, .kind = TCK_VARIABLE, .var = var};
   2030 
   2031 			if (g->expanded) {
   2032 				var = g->first;
   2033 				table = table_begin_subtable(table, &arena, table->columns,
   2034 				                             (TextAlignment []){TA_LEFT, TA_CENTER, TA_RIGHT});
   2035 				table->widths[1] = 100;
   2036 			} else {
   2037 				Variable *v = g->first;
   2038 
   2039 				ASSERT(!v || v->type == VT_BEAMFORMER_VARIABLE);
   2040 				/* NOTE(rnp): assume the suffix is the same for all elements */
   2041 				if (v) cells[2].text = v->u.beamformer_variable.suffix;
   2042 
   2043 				Stream sb = arena_stream(arena);
   2044 				switch (g->type) {
   2045 				case VG_LIST: break;
   2046 				case VG_V2:
   2047 				case VG_V4: {
   2048 					stream_append_s8(&sb, s8("{"));
   2049 					while (v) {
   2050 						stream_append_variable(&sb, v);
   2051 						v = v->next;
   2052 						if (v) stream_append_s8(&sb, s8(", "));
   2053 					}
   2054 					stream_append_s8(&sb, s8("}"));
   2055 				} break;
   2056 				}
   2057 				cells[1].kind = TCK_VARIABLE_GROUP;
   2058 				cells[1].text = arena_stream_commit(&arena, &sb);
   2059 				cells[1].var  = var;
   2060 
   2061 				var = var->next;
   2062 			}
   2063 		} break;
   2064 		INVALID_DEFAULT_CASE;
   2065 		}
   2066 	}
   2067 
   2068 	text_spec.flags |= TF_LIMITED;
   2069 	v2 result = table_extent(table, arena, text_spec.font);
   2070 	TableIterator *it = table_iterator_new(table, TIK_CELLS, &arena, 0, r.pos, text_spec.font);
   2071 	for (TableCell *cell = table_iterator_next(it, &arena);
   2072 	     cell;
   2073 	     cell = table_iterator_next(it, &arena))
   2074 	{
   2075 		text_spec.limits.size.w = r.size.w - (it->cell_rect.pos.x - it->start_x);
   2076 		/* TODO(rnp): ensure this doesn't exceed r.size */
   2077 		Rect rect;
   2078 		rect.pos  = add_v2(it->cell_rect.pos, scale_v2((v2){.x = text_spec.font->baseSize}, it->sub_table_depth));
   2079 		rect.size = it->cell_rect.size;
   2080 		if (cell->kind == TCK_VARIABLE_GROUP) {
   2081 			Variable *v = cell->var->u.group.first;
   2082 			v2 at = table_cell_align(cell, it->alignment, rect);
   2083 			text_spec.limits.size.w = r.size.w - (at.x - it->start_x);
   2084 			f32 dw = draw_text(s8("{"), at, &text_spec).x;
   2085 			while (v) {
   2086 				at.x += dw;
   2087 				text_spec.limits.size.w -= dw;
   2088 				dw = draw_variable(ui, arena, v, at, mouse, text_spec.colour, text_spec).x;
   2089 
   2090 				v = v->next;
   2091 				if (v) {
   2092 					at.x += dw;
   2093 					text_spec.limits.size.w -= dw;
   2094 					dw = draw_text(s8(", "), at, &text_spec).x;
   2095 				}
   2096 			}
   2097 			at.x += dw;
   2098 			text_spec.limits.size.w -= dw;
   2099 			draw_text(s8("}"), at, &text_spec);
   2100 		} else {
   2101 			draw_table_cell(ui, cell, rect, it->alignment, text_spec, mouse);
   2102 		}
   2103 	}
   2104 
   2105 	return result;
   2106 }
   2107 
   2108 function void
   2109 draw_ui_view(BeamformerUI *ui, Variable *ui_view, Rect r, v2 mouse, TextSpec text_spec)
   2110 {
   2111 	ASSERT(ui_view->type == VT_UI_VIEW);
   2112 	UIView *view = &ui_view->u.view;
   2113 
   2114 	if (view->needed_height - r.size.h < view->offset)
   2115 		view->offset = view->needed_height - r.size.h;
   2116 
   2117 	if (view->needed_height - r.size.h < 0)
   2118 		view->offset = 0;
   2119 
   2120 	r.pos.y -= view->offset;
   2121 
   2122 	v2 size = {0};
   2123 
   2124 	Variable *var = view->child;
   2125 	switch (var->type) {
   2126 	case VT_GROUP: size = draw_ui_view_listing(ui, var, ui->arena, r, mouse, text_spec); break;
   2127 	case VT_BEAMFORMER_FRAME_VIEW: {
   2128 		BeamformerFrameView *bv = var->u.generic;
   2129 		if (frame_view_ready_to_present(bv))
   2130 			draw_beamformer_frame_view(ui, ui->arena, var, r, mouse);
   2131 	} break;
   2132 	case VT_COMPUTE_PROGRESS_BAR: {
   2133 		size = draw_compute_progress_bar(ui, ui->arena, &var->u.compute_progress_bar, r);
   2134 	} break;
   2135 	case VT_COMPUTE_LATEST_STATS_VIEW:
   2136 	case VT_COMPUTE_STATS_VIEW: {
   2137 		ComputeShaderStats *stats = var->u.compute_stats_view.stats;
   2138 		if (var->type == VT_COMPUTE_LATEST_STATS_VIEW)
   2139 			stats = *(ComputeShaderStats **)stats;
   2140 		size = draw_compute_stats_view(var->u.compute_stats_view.ctx, ui->arena, stats, r);
   2141 	} break;
   2142 	default: INVALID_CODE_PATH;
   2143 	}
   2144 
   2145 	view->needed_height = size.y;
   2146 }
   2147 
   2148 function void
   2149 draw_active_text_box(BeamformerUI *ui, Variable *var)
   2150 {
   2151 	InputState *is = &ui->text_input_state;
   2152 	Rect box       = ui->interaction.rect;
   2153 	Font *font     = ui->interaction.font;
   2154 
   2155 	s8 text          = {.len = is->count, .data = is->buf};
   2156 	v2 text_size     = measure_text(*font, text);
   2157 	v2 text_position = {.x = box.pos.x, .y = box.pos.y + (box.size.h - text_size.h) / 2};
   2158 
   2159 	f32 cursor_width   = (is->cursor == is->count) ? 0.55 * font->baseSize : 4;
   2160 	f32 cursor_offset  = measure_text(*font, (s8){.data = text.data, .len = is->cursor}).w;
   2161 	cursor_offset     += text_position.x;
   2162 
   2163 	box.size.w = MAX(box.size.w, text_size.w + cursor_width);
   2164 	Rect background = extend_rect_centered(box, (v2){.x = 12, .y = 8});
   2165 	box = extend_rect_centered(box, (v2){.x = 8, .y = 4});
   2166 
   2167 	Rect cursor = {
   2168 		.pos  = {.x = cursor_offset, .y = text_position.y},
   2169 		.size = {.w = cursor_width,  .h = text_size.h},
   2170 	};
   2171 
   2172 	v4 cursor_colour = FOCUSED_COLOUR;
   2173 	cursor_colour.a  = CLAMP01(is->cursor_blink_t);
   2174 
   2175 	TextSpec text_spec = {.font = font, .colour = lerp_v4(FG_COLOUR, HOVERED_COLOUR, var->hover_t)};
   2176 
   2177 	DrawRectangleRounded(background.rl, 0.2, 0, fade(BLACK, 0.8));
   2178 	DrawRectangleRounded(box.rl, 0.2, 0, colour_from_normalized(BG_COLOUR));
   2179 	draw_text(text, text_position, &text_spec);
   2180 	DrawRectanglePro(cursor.rl, (Vector2){0}, 0, colour_from_normalized(cursor_colour));
   2181 }
   2182 
   2183 function void
   2184 draw_active_menu(BeamformerUI *ui, Arena arena, Variable *menu, v2 mouse, Rect window)
   2185 {
   2186 	ASSERT(menu->type == VT_GROUP);
   2187 
   2188 	Font *font          = ui->interaction.font;
   2189 	f32 font_height     = font->baseSize;
   2190 	f32 max_label_width = 0;
   2191 
   2192 	Variable *item = menu->u.group.first;
   2193 	i32 item_count = 0;
   2194 	b32 radio = 0;
   2195 	while (item) {
   2196 		max_label_width = MAX(max_label_width, item->name_width);
   2197 		radio |= (item->flags & V_RADIO_BUTTON) != 0;
   2198 		item_count++;
   2199 		item = item->next;
   2200 	}
   2201 
   2202 	f32 radio_button_width = radio? font_height : 0;
   2203 	v2  at          = ui->interaction.rect.pos;
   2204 	f32 menu_width  = max_label_width + radio_button_width + 8;
   2205 	f32 menu_height = item_count * font_height + (item_count - 1) * 2;
   2206 	menu_height = MAX(menu_height, 0);
   2207 
   2208 	if (at.x + menu_width > window.size.w)
   2209 		at.x = window.size.w - menu_width  - 16;
   2210 	if (at.y + menu_height > window.size.h)
   2211 		at.y = window.size.h - menu_height - 12;
   2212 	/* TODO(rnp): scroll menu if it doesn't fit on screen */
   2213 
   2214 	Rect menu_rect = {.pos = at, .size = {.w = menu_width, .h = menu_height}};
   2215 	Rect bg_rect   = extend_rect_centered(menu_rect, (v2){.x = 12, .y = 8});
   2216 	menu_rect      = extend_rect_centered(menu_rect, (v2){.x = 6,  .y = 4});
   2217 	DrawRectangleRounded(bg_rect.rl,   0.1, 0, fade(BLACK, 0.8));
   2218 	DrawRectangleRounded(menu_rect.rl, 0.1, 0, colour_from_normalized(BG_COLOUR));
   2219 	v2 start = at;
   2220 	for (i32 i = 0; i < item_count - 1; i++) {
   2221 		at.y += 2 + font_height;
   2222 		DrawLineEx((v2){.x = at.x - 3, .y = at.y}.rl,
   2223 		           add_v2(at, (v2){.w = menu_width + 3}).rl, 2, fade(BLACK, 0.8));
   2224 	}
   2225 
   2226 	item = menu->u.group.first;
   2227 	TextSpec text_spec = {.font = font, .colour = FG_COLOUR, .limits.size.w = menu_width};
   2228 	at = start;
   2229 	while (item) {
   2230 		at.x = start.x;
   2231 		if (item->type == VT_CYCLER) {
   2232 			at.x += draw_text(item->name, at, &text_spec).x;
   2233 		} else if (item->flags & V_RADIO_BUTTON) {
   2234 			draw_text(item->name, at, &text_spec);
   2235 			at.x += max_label_width + 8;
   2236 		}
   2237 		at.y += draw_variable(ui, arena, item, at, mouse, FG_COLOUR, text_spec).y + 2;
   2238 		item = item->next;
   2239 	}
   2240 }
   2241 
   2242 function void
   2243 draw_layout_variable(BeamformerUI *ui, Variable *var, Rect draw_rect, v2 mouse)
   2244 {
   2245 	if (var->type != VT_UI_REGION_SPLIT) {
   2246 		v2 shrink = {.x = UI_REGION_PAD, .y = UI_REGION_PAD};
   2247 		draw_rect = shrink_rect_centered(draw_rect, shrink);
   2248 		draw_rect.size = floor_v2(draw_rect.size);
   2249 		BeginScissorMode(draw_rect.pos.x, draw_rect.pos.y, draw_rect.size.w, draw_rect.size.h);
   2250 		draw_rect = draw_title_bar(ui, ui->arena, var, draw_rect, mouse);
   2251 		EndScissorMode();
   2252 	}
   2253 
   2254 	/* TODO(rnp): post order traversal of the ui tree will remove the need for this */
   2255 	if (!CheckCollisionPointRec(mouse.rl, draw_rect.rl))
   2256 		mouse = (v2){.x = F32_INFINITY, .y = F32_INFINITY};
   2257 
   2258 	draw_rect.size = floor_v2(draw_rect.size);
   2259 	BeginScissorMode(draw_rect.pos.x, draw_rect.pos.y, draw_rect.size.w, draw_rect.size.h);
   2260 	switch (var->type) {
   2261 	case VT_UI_VIEW: {
   2262 		hover_var(ui, mouse, draw_rect, var);
   2263 		TextSpec text_spec = {.font = &ui->font, .colour = FG_COLOUR, .flags = TF_LIMITED};
   2264 		draw_ui_view(ui, var, draw_rect, mouse, text_spec);
   2265 	} break;
   2266 	case VT_UI_REGION_SPLIT: {
   2267 		RegionSplit *rs = &var->u.region_split;
   2268 
   2269 		Rect split, hover;
   2270 		switch (rs->direction) {
   2271 		case RSD_VERTICAL: {
   2272 			split_rect_vertical(draw_rect, rs->fraction, 0, &split);
   2273 			split.pos.x  += UI_REGION_PAD;
   2274 			split.pos.y  -= UI_SPLIT_HANDLE_THICK / 2;
   2275 			split.size.h  = UI_SPLIT_HANDLE_THICK;
   2276 			split.size.w -= 2 * UI_REGION_PAD;
   2277 			hover = extend_rect_centered(split, (v2){.y = 0.75 * UI_REGION_PAD});
   2278 		} break;
   2279 		case RSD_HORIZONTAL: {
   2280 			split_rect_horizontal(draw_rect, rs->fraction, 0, &split);
   2281 			split.pos.x  -= UI_SPLIT_HANDLE_THICK / 2;
   2282 			split.pos.y  += UI_REGION_PAD;
   2283 			split.size.w  = UI_SPLIT_HANDLE_THICK;
   2284 			split.size.h -= 2 * UI_REGION_PAD;
   2285 			hover = extend_rect_centered(split, (v2){.x = 0.75 * UI_REGION_PAD});
   2286 		} break;
   2287 		}
   2288 
   2289 		hover_var(ui, mouse, hover, var);
   2290 
   2291 		v4 colour = HOVERED_COLOUR;
   2292 		colour.a  = var->hover_t;
   2293 		DrawRectangleRounded(split.rl, 0.6, 0, colour_from_normalized(colour));
   2294 	} break;
   2295 	default: INVALID_CODE_PATH; break;
   2296 	}
   2297 	EndScissorMode();
   2298 }
   2299 
   2300 function void
   2301 draw_ui_regions(BeamformerUI *ui, Rect window, v2 mouse)
   2302 {
   2303 	struct region_frame {
   2304 		Variable *var;
   2305 		Rect      rect;
   2306 	} init[16];
   2307 
   2308 	struct {
   2309 		struct region_frame *data;
   2310 		iz count;
   2311 		iz capacity;
   2312 	} stack = {init, 0, ARRAY_COUNT(init)};
   2313 
   2314 	TempArena arena_savepoint = begin_temp_arena(&ui->arena);
   2315 
   2316 	*da_push(&ui->arena, &stack) = (struct region_frame){ui->regions, window};
   2317 	while (stack.count) {
   2318 		struct region_frame *top = stack.data + --stack.count;
   2319 		Rect rect = top->rect;
   2320 		draw_layout_variable(ui, top->var, rect, mouse);
   2321 
   2322 		if (top->var->type == VT_UI_REGION_SPLIT) {
   2323 			Rect first, second;
   2324 			RegionSplit *rs = &top->var->u.region_split;
   2325 			switch (rs->direction) {
   2326 			case RSD_VERTICAL: {
   2327 				split_rect_vertical(rect, rs->fraction, &first, &second);
   2328 			} break;
   2329 			case RSD_HORIZONTAL: {
   2330 				split_rect_horizontal(rect, rs->fraction, &first, &second);
   2331 			} break;
   2332 			}
   2333 
   2334 			*da_push(&ui->arena, &stack) = (struct region_frame){rs->right, second};
   2335 			*da_push(&ui->arena, &stack) = (struct region_frame){rs->left,  first};
   2336 		}
   2337 	}
   2338 
   2339 	end_temp_arena(arena_savepoint);
   2340 }
   2341 
   2342 function void
   2343 scroll_interaction(Variable *var, f32 delta)
   2344 {
   2345 	switch (var->type) {
   2346 	case VT_B32: var->u.b32  = !var->u.b32; break;
   2347 	case VT_F32: var->u.f32 += delta;       break;
   2348 	case VT_I32: var->u.i32 += delta;       break;
   2349 	case VT_U32: var->u.u32 += delta;       break;
   2350 	case VT_SCALED_F32: var->u.scaled_f32.val += delta * var->u.scaled_f32.scale; break;
   2351 	case VT_BEAMFORMER_FRAME_VIEW: {
   2352 		BeamformerFrameView *bv = var->u.generic;
   2353 		bv->needs_update     = 1;
   2354 		bv->threshold.u.f32 += delta;
   2355 	} break;
   2356 	case VT_BEAMFORMER_VARIABLE: {
   2357 		BeamformerVariable *bv = &var->u.beamformer_variable;
   2358 		switch (bv->store_type) {
   2359 		case VT_F32: {
   2360 			f32 val = *(f32 *)bv->store + delta * bv->scroll_scale;
   2361 			*(f32 *)bv->store = CLAMP(val, bv->limits.x, bv->limits.y);
   2362 		} break;
   2363 		INVALID_DEFAULT_CASE;
   2364 		}
   2365 	} break;
   2366 	case VT_CYCLER: {
   2367 		*var->u.cycler.state += delta > 0? 1 : -1;
   2368 		*var->u.cycler.state %= var->u.cycler.cycle_length;
   2369 	} break;
   2370 	case VT_UI_VIEW: {
   2371 		var->u.view.offset += UI_SCROLL_SPEED * delta;
   2372 		var->u.view.offset  = MAX(0, var->u.view.offset);
   2373 	} break;
   2374 	INVALID_DEFAULT_CASE;
   2375 	}
   2376 }
   2377 
   2378 function void
   2379 begin_text_input(InputState *is, Font *font, Rect r, Variable *var, v2 mouse)
   2380 {
   2381 	Stream s = {.cap = ARRAY_COUNT(is->buf), .data = is->buf};
   2382 	stream_append_variable(&s, var);
   2383 	is->count = s.widx;
   2384 
   2385 	/* NOTE: extra offset to help with putting a cursor at idx 0 */
   2386 	#define TEXT_HALF_CHAR_WIDTH 10
   2387 	f32 hover_p = CLAMP01((mouse.x - r.pos.x) / r.size.w);
   2388 	f32 x_off = TEXT_HALF_CHAR_WIDTH, x_bounds = r.size.w * hover_p;
   2389 	i32 i;
   2390 	for (i = 0; i < is->count && x_off < x_bounds; i++) {
   2391 		/* NOTE: assumes font glyphs are ordered ASCII */
   2392 		i32 idx  = is->buf[i] - 0x20;
   2393 		x_off   += font->glyphs[idx].advanceX;
   2394 		if (font->glyphs[idx].advanceX == 0)
   2395 			x_off += font->recs[idx].width;
   2396 	}
   2397 	is->cursor = i;
   2398 }
   2399 
   2400 function void
   2401 end_text_input(InputState *is, Variable *var)
   2402 {
   2403 	f64 value = parse_f64((s8){.len = is->count, .data = is->buf});
   2404 
   2405 	switch (var->type) {
   2406 	case VT_SCALED_F32: var->u.scaled_f32.val = value; break;
   2407 	case VT_F32:        var->u.f32            = value; break;
   2408 	case VT_BEAMFORMER_VARIABLE: {
   2409 		BeamformerVariable *bv = &var->u.beamformer_variable;
   2410 		switch (bv->store_type) {
   2411 		case VT_F32: {
   2412 			value = CLAMP(value / bv->display_scale, bv->limits.x, bv->limits.y);
   2413 			*(f32 *)bv->store = value;
   2414 		} break;
   2415 		INVALID_DEFAULT_CASE;
   2416 		}
   2417 		var->hover_t = 0;
   2418 	} break;
   2419 	INVALID_DEFAULT_CASE;
   2420 	}
   2421 }
   2422 
   2423 function void
   2424 update_text_input(InputState *is, Variable *var)
   2425 {
   2426 	ASSERT(is->cursor != -1);
   2427 
   2428 	is->cursor_blink_t += is->cursor_blink_scale * dt_for_frame;
   2429 	if (is->cursor_blink_t >= 1) is->cursor_blink_scale = -1.5f;
   2430 	if (is->cursor_blink_t <= 0) is->cursor_blink_scale =  1.5f;
   2431 
   2432 	var->hover_t -= 2 * HOVER_SPEED * dt_for_frame;
   2433 	var->hover_t  = CLAMP01(var->hover_t);
   2434 
   2435 	/* NOTE: handle multiple input keys on a single frame */
   2436 	for (i32 key = GetCharPressed();
   2437 	     is->count < countof(is->buf) && key > 0;
   2438 	     key = GetCharPressed())
   2439 	{
   2440 		b32 allow_key = (BETWEEN(key, '0', '9') || (key == '.') ||
   2441 		                 (key == '-' && is->cursor == 0));
   2442 		if (allow_key) {
   2443 			mem_move(is->buf + is->cursor + 1,
   2444 			         is->buf + is->cursor,
   2445 			         is->count - is->cursor);
   2446 			is->buf[is->cursor++] = key;
   2447 			is->count++;
   2448 		}
   2449 	}
   2450 
   2451 	is->cursor -= (IsKeyPressed(KEY_LEFT)  || IsKeyPressedRepeat(KEY_LEFT))  && is->cursor > 0;
   2452 	is->cursor += (IsKeyPressed(KEY_RIGHT) || IsKeyPressedRepeat(KEY_RIGHT)) && is->cursor < is->count;
   2453 
   2454 	if ((IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE)) && is->cursor > 0) {
   2455 		is->cursor--;
   2456 		if (is->cursor < countof(is->buf) - 1) {
   2457 			mem_move(is->buf + is->cursor,
   2458 			         is->buf + is->cursor + 1,
   2459 			         is->count - is->cursor - 1);
   2460 		}
   2461 		is->count--;
   2462 	}
   2463 
   2464 	if ((IsKeyPressed(KEY_DELETE) || IsKeyPressedRepeat(KEY_DELETE)) && is->cursor < is->count) {
   2465 		mem_move(is->buf + is->cursor,
   2466 		         is->buf + is->cursor + 1,
   2467 		         is->count - is->cursor - 1);
   2468 		is->count--;
   2469 	}
   2470 }
   2471 
   2472 function void
   2473 scale_bar_interaction(BeamformerUI *ui, ScaleBar *sb, v2 mouse)
   2474 {
   2475 	InteractionState *is    = &ui->interaction;
   2476 	b32 mouse_left_pressed  = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
   2477 	b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT);
   2478 	f32 mouse_wheel         = GetMouseWheelMoveV().y;
   2479 
   2480 	if (mouse_left_pressed) {
   2481 		v2 world_mouse = screen_point_to_world_2d(mouse, is->rect.pos,
   2482 		                                          add_v2(is->rect.pos, is->rect.size),
   2483 		                                          (v2){{*sb->min_value, *sb->min_value}},
   2484 		                                          (v2){{*sb->max_value, *sb->max_value}});
   2485 		f32 new_coord = F32_INFINITY;
   2486 		switch (sb->direction) {
   2487 		case SB_LATERAL: new_coord = world_mouse.x; break;
   2488 		case SB_AXIAL:   new_coord = world_mouse.y; break;
   2489 		}
   2490 		if (sb->zoom_starting_coord == F32_INFINITY) {
   2491 			sb->zoom_starting_coord = new_coord;
   2492 		} else {
   2493 			f32 min = sb->zoom_starting_coord;
   2494 			f32 max = new_coord;
   2495 			if (min > max) SWAP(min, max)
   2496 
   2497 			v2_sll *savepoint = SLLPop(ui->scale_bar_savepoint_freelist);
   2498 			if (!savepoint) savepoint = push_struct(&ui->arena, v2_sll);
   2499 
   2500 			savepoint->v.x = *sb->min_value;
   2501 			savepoint->v.y = *sb->max_value;
   2502 			SLLPush(savepoint, sb->savepoint_stack);
   2503 
   2504 			*sb->min_value = min;
   2505 			*sb->max_value = max;
   2506 
   2507 			sb->zoom_starting_coord = F32_INFINITY;
   2508 		}
   2509 	}
   2510 
   2511 	if (mouse_right_pressed) {
   2512 		v2_sll *savepoint = sb->savepoint_stack;
   2513 		if (savepoint) {
   2514 			*sb->min_value      = savepoint->v.x;
   2515 			*sb->max_value      = savepoint->v.y;
   2516 			sb->savepoint_stack = savepoint->next;
   2517 			SLLPush(savepoint, ui->scale_bar_savepoint_freelist);
   2518 		}
   2519 		sb->zoom_starting_coord = F32_INFINITY;
   2520 	}
   2521 
   2522 	if (mouse_wheel) {
   2523 		*sb->min_value += mouse_wheel * sb->scroll_scale.x;
   2524 		*sb->max_value += mouse_wheel * sb->scroll_scale.y;
   2525 	}
   2526 }
   2527 
   2528 function void
   2529 ui_button_interaction(BeamformerUI *ui, Variable *button)
   2530 {
   2531 	ASSERT(button->type == VT_UI_BUTTON);
   2532 	switch (button->u.button) {
   2533 	case UI_BID_FV_COPY_HORIZONTAL: {
   2534 		ui_copy_frame(ui, button->parent->parent, RSD_HORIZONTAL);
   2535 	} break;
   2536 	case UI_BID_FV_COPY_VERTICAL: {
   2537 		ui_copy_frame(ui, button->parent->parent, RSD_VERTICAL);
   2538 	} break;
   2539 	case UI_BID_GM_OPEN_LIVE_VIEW_RIGHT: {
   2540 		ui_add_live_frame_view(ui, button->parent->parent, RSD_HORIZONTAL);
   2541 	} break;
   2542 	case UI_BID_GM_OPEN_LIVE_VIEW_BELOW: {
   2543 		ui_add_live_frame_view(ui, button->parent->parent, RSD_VERTICAL);
   2544 	} break;
   2545 	case UI_BID_CLOSE_VIEW: {
   2546 		Variable *view   = button->parent;
   2547 		Variable *region = view->parent;
   2548 		ASSERT(view->type == VT_UI_VIEW && region->type == VT_UI_REGION_SPLIT);
   2549 
   2550 		Variable *parent    = region->parent;
   2551 		Variable *remaining = region->u.region_split.left;
   2552 		if (remaining == view) remaining = region->u.region_split.right;
   2553 
   2554 		ui_view_free(ui, view);
   2555 
   2556 		ASSERT(parent->type == VT_UI_REGION_SPLIT);
   2557 		if (parent->u.region_split.left == region) {
   2558 			parent->u.region_split.left  = remaining;
   2559 		} else {
   2560 			parent->u.region_split.right = remaining;
   2561 		}
   2562 		remaining->parent = parent;
   2563 
   2564 		SLLPush(region, ui->variable_freelist);
   2565 	} break;
   2566 	}
   2567 }
   2568 
   2569 function void
   2570 ui_begin_interact(BeamformerUI *ui, BeamformerInput *input, b32 scroll, b32 mouse_left_pressed)
   2571 {
   2572 	InteractionState *is = &ui->interaction;
   2573 	if (is->hot) {
   2574 		switch (is->hot->type) {
   2575 		case VT_NULL: is->type = IT_NOP; break;
   2576 		case VT_B32:  is->type = IT_SET; break;
   2577 		case VT_UI_REGION_SPLIT: { is->type = IT_DRAG; }                 break;
   2578 		case VT_UI_VIEW:         { if (scroll) is->type = IT_SCROLL; }   break;
   2579 		case VT_UI_BUTTON:       { ui_button_interaction(ui, is->hot); } break;
   2580 		case VT_SCALE_BAR:       { is->type = IT_SET; } break;
   2581 		case VT_BEAMFORMER_FRAME_VIEW:
   2582 		case VT_CYCLER: {
   2583 			if (scroll) is->type = IT_SCROLL;
   2584 			else        is->type = IT_SET;
   2585 		} break;
   2586 		case VT_GROUP: {
   2587 			if (mouse_left_pressed && is->hot->flags & V_MENU) {
   2588 				is->type = IT_MENU;
   2589 			} else {
   2590 				is->type = IT_SET;
   2591 			}
   2592 		} break;
   2593 		case VT_BEAMFORMER_VARIABLE: {
   2594 			if (is->hot->u.beamformer_variable.store_type == VT_B32) {
   2595 				is->type = IT_SET;
   2596 				break;
   2597 			}
   2598 		} /* FALLTHROUGH */
   2599 		case VT_SCALED_F32:
   2600 		case VT_F32: {
   2601 			if (scroll) {
   2602 				is->type = IT_SCROLL;
   2603 			} else if (mouse_left_pressed && is->hot->flags & V_TEXT) {
   2604 				is->type = IT_TEXT;
   2605 				begin_text_input(&ui->text_input_state, is->hot_font, is->hot_rect,
   2606 				                 is->hot, input->mouse);
   2607 			}
   2608 		} break;
   2609 		default: INVALID_CODE_PATH;
   2610 		}
   2611 	}
   2612 	if (is->type != IT_NONE) {
   2613 		is->last_rect = is->rect;
   2614 		is->active = is->hot;
   2615 		is->rect   = is->hot_rect;
   2616 		is->font   = is->hot_font;
   2617 	}
   2618 }
   2619 
   2620 function void
   2621 ui_end_interact(BeamformerUI *ui, v2 mouse)
   2622 {
   2623 	InteractionState *is = &ui->interaction;
   2624 	switch (is->type) {
   2625 	case IT_NOP:  break;
   2626 	case IT_MENU: break;
   2627 	case IT_DRAG: break;
   2628 	case IT_SET: {
   2629 		switch (is->active->type) {
   2630 		case VT_B32: { is->active->u.b32 = !is->active->u.b32; } break;
   2631 		case VT_GROUP: {
   2632 			is->active->u.group.expanded = !is->active->u.group.expanded;
   2633 		} break;
   2634 		case VT_CYCLER: {
   2635 			*is->active->u.cycler.state += 1;
   2636 			*is->active->u.cycler.state %= is->active->u.cycler.cycle_length;
   2637 		} break;
   2638 		case VT_SCALE_BAR: {
   2639 			scale_bar_interaction(ui, &is->active->u.scale_bar, mouse);
   2640 		} break;
   2641 		case VT_BEAMFORMER_FRAME_VIEW: {
   2642 			BeamformerFrameView *bv = is->hot->u.generic;
   2643 			bv->ruler.state++;
   2644 			switch (bv->ruler.state) {
   2645 			case RS_START:
   2646 			case RS_HOLD: {
   2647 				v2 r_max = add_v2(is->rect.pos, is->rect.size);
   2648 				v2 p = screen_point_to_world_2d(mouse, is->rect.pos, r_max,
   2649 				                                XZ(bv->min_coordinate),
   2650 				                                XZ(bv->max_coordinate));
   2651 				if (bv->ruler.state == RS_START) bv->ruler.start = p;
   2652 				else                             bv->ruler.end   = p;
   2653 			} break;
   2654 			default: bv->ruler.state = RS_NONE; break;
   2655 			}
   2656 		} break;
   2657 		default: INVALID_CODE_PATH;
   2658 		}
   2659 	} break;
   2660 	case IT_SCROLL:  scroll_interaction(is->active, GetMouseWheelMoveV().y); break;
   2661 	case IT_TEXT:    end_text_input(&ui->text_input_state, is->active);      break;
   2662 	default: INVALID_CODE_PATH;
   2663 	}
   2664 
   2665 	b32 menu_child = is->active->parent && is->active->parent->flags & V_MENU;
   2666 
   2667 	/* TODO(rnp): better way of clearing the state when the parent is a menu */
   2668 	if (menu_child) is->active->hover_t = 0;
   2669 
   2670 	if (is->active->flags & V_CAUSES_COMPUTE)
   2671 		ui->flush_params = 1;
   2672 	if (is->active->flags & V_UPDATE_VIEW) {
   2673 		Variable *parent = is->active->parent;
   2674 		BeamformerFrameView *frame;
   2675 		/* TODO(rnp): more straight forward way of achieving this */
   2676 		if (parent->type == VT_BEAMFORMER_FRAME_VIEW) {
   2677 			frame = parent->u.generic;
   2678 		} else {
   2679 			ASSERT(parent->flags & V_MENU);
   2680 			ASSERT(parent->parent->u.group.first->type == VT_BEAMFORMER_FRAME_VIEW);
   2681 			frame = parent->parent->u.group.first->u.generic;
   2682 		}
   2683 		frame->needs_update = 1;
   2684 	}
   2685 
   2686 	if (menu_child && (is->active->flags & V_CLOSES_MENU) == 0) {
   2687 		is->type   = IT_MENU;
   2688 		is->rect   = is->last_rect;
   2689 		is->active = is->active->parent;
   2690 	} else {
   2691 		is->type   = IT_NONE;
   2692 		is->active = 0;
   2693 	}
   2694 }
   2695 
   2696 function void
   2697 ui_interact(BeamformerUI *ui, BeamformerInput *input, uv2 window_size)
   2698 {
   2699 	InteractionState *is    = &ui->interaction;
   2700 	b32 mouse_left_pressed  = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
   2701 	b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT);
   2702 	b32 wheel_moved         = GetMouseWheelMoveV().y != 0;
   2703 	if (mouse_right_pressed || mouse_left_pressed || wheel_moved) {
   2704 		if (is->type != IT_NONE)
   2705 			ui_end_interact(ui, input->mouse);
   2706 		ui_begin_interact(ui, input, wheel_moved, mouse_left_pressed);
   2707 	}
   2708 
   2709 	if (IsKeyPressed(KEY_ENTER) && is->type == IT_TEXT)
   2710 		ui_end_interact(ui, input->mouse);
   2711 
   2712 	switch (is->type) {
   2713 	case IT_NONE: break;
   2714 	case IT_NOP:  break;
   2715 	case IT_MENU: break;
   2716 	case IT_SCROLL: ui_end_interact(ui, input->mouse);                    break;
   2717 	case IT_SET:    ui_end_interact(ui, input->mouse);                    break;
   2718 	case IT_TEXT:   update_text_input(&ui->text_input_state, is->active); break;
   2719 	case IT_DRAG: {
   2720 		if (!IsMouseButtonDown(MOUSE_BUTTON_LEFT) && !IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) {
   2721 			ui_end_interact(ui, input->mouse);
   2722 		} else {
   2723 			v2 ws     = {.w = window_size.w, .h = window_size.h};
   2724 			v2 dMouse = sub_v2(input->mouse, input->last_mouse);
   2725 			dMouse    = mul_v2(dMouse, (v2){.x = 1.0f / ws.w, .y = 1.0f / ws.h});
   2726 
   2727 			switch (is->active->type) {
   2728 			case VT_UI_REGION_SPLIT: {
   2729 				f32 min_fraction = 0;
   2730 				RegionSplit *rs = &is->active->u.region_split;
   2731 				switch (rs->direction) {
   2732 				case RSD_VERTICAL: {
   2733 					min_fraction  = (UI_SPLIT_HANDLE_THICK + 0.5 * UI_REGION_PAD) / ws.h;
   2734 					rs->fraction += dMouse.y;
   2735 				} break;
   2736 				case RSD_HORIZONTAL: {
   2737 					min_fraction  = (UI_SPLIT_HANDLE_THICK + 0.5 * UI_REGION_PAD) / ws.w;
   2738 					rs->fraction += dMouse.x;
   2739 				} break;
   2740 				}
   2741 				rs->fraction = CLAMP(rs->fraction, min_fraction, 1 - min_fraction);
   2742 			} break;
   2743 			default: break;
   2744 			}
   2745 		}
   2746 	} break;
   2747 	}
   2748 
   2749 	is->hot = 0;
   2750 }
   2751 
   2752 function void
   2753 ui_init(BeamformerCtx *ctx, Arena store)
   2754 {
   2755 	/* NOTE(rnp): store the ui at the base of the passed in arena and use the rest for
   2756 	 * temporary allocations within the ui. If needed we can recall this function to
   2757 	 * completely clear the ui state. The is that if we store pointers to static data
   2758 	 * such as embedded font data we will need to reset them when the executable reloads.
   2759 	 * We could also build some sort of ui structure here and store it then iterate over
   2760 	 * it to actually draw the ui. If we reload we may have changed it so we should
   2761 	 * rebuild it */
   2762 
   2763 	BeamformerUI *ui = ctx->ui;
   2764 
   2765 	/* NOTE(rnp): unload old data from GPU */
   2766 	if (ui) {
   2767 		UnloadFont(ui->font);
   2768 		UnloadFont(ui->small_font);
   2769 
   2770 		for (BeamformerFrameView *view = ui->views; view; view = view->next)
   2771 			if (view->texture)
   2772 				glDeleteTextures(1, &view->texture);
   2773 	}
   2774 
   2775 	ui = ctx->ui = push_struct(&store, typeof(*ui));
   2776 	ui->os    = &ctx->os;
   2777 	ui->arena = store;
   2778 	ui->frame_view_render_context = &ctx->frame_view_render_context;
   2779 
   2780 	/* TODO: build these into the binary */
   2781 	/* TODO(rnp): better font, this one is jank at small sizes */
   2782 	ui->font       = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 28, 0, 0);
   2783 	ui->small_font = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 20, 0, 0);
   2784 
   2785 	Variable *split = ui->regions = add_ui_split(ui, 0, &ui->arena, s8("UI Root"), 0.4,
   2786 	                                             RSD_HORIZONTAL, ui->font);
   2787 	split->u.region_split.left    = add_ui_split(ui, split, &ui->arena, s8(""), 0.475,
   2788 	                                             RSD_VERTICAL, ui->font);
   2789 	split->u.region_split.right   = add_beamformer_frame_view(ui, split, &ui->arena, FVT_LATEST, 0);
   2790 
   2791 	ui_fill_live_frame_view(ui, split->u.region_split.right->u.view.child->u.generic);
   2792 
   2793 	split = split->u.region_split.left;
   2794 	split->u.region_split.left  = add_beamformer_parameters_view(split, ctx);
   2795 	split->u.region_split.right = add_ui_split(ui, split, &ui->arena, s8(""), 0.22,
   2796 	                                           RSD_VERTICAL, ui->font);
   2797 	split = split->u.region_split.right;
   2798 
   2799 	split->u.region_split.left  = add_compute_progress_bar(split, ctx);
   2800 	split->u.region_split.right = add_compute_stats_view(ui, split, &ui->arena,
   2801 	                                                     VT_COMPUTE_LATEST_STATS_VIEW);
   2802 
   2803 	ComputeStatsView *compute_stats = &split->u.region_split.right->u.group.first->u.compute_stats_view;
   2804 	compute_stats->ctx   = ctx;
   2805 	compute_stats->stats = &ui->latest_compute_stats;
   2806 
   2807 	ctx->ui_read_params = 1;
   2808 
   2809 	/* NOTE(rnp): shrink variable size once this fires */
   2810 	ASSERT(ui->arena.beg - (u8 *)ui < KB(64));
   2811 }
   2812 
   2813 function void
   2814 validate_ui_parameters(BeamformerUIParameters *p)
   2815 {
   2816 	if (p->output_min_coordinate[0] > p->output_max_coordinate[0])
   2817 		SWAP(p->output_min_coordinate[0], p->output_max_coordinate[0])
   2818 	if (p->output_min_coordinate[2] > p->output_max_coordinate[2])
   2819 		SWAP(p->output_min_coordinate[2], p->output_max_coordinate[2])
   2820 }
   2821 
   2822 function void
   2823 draw_ui(BeamformerCtx *ctx, BeamformerInput *input, BeamformFrame *frame_to_draw, ImagePlaneTag frame_plane,
   2824         ComputeShaderStats *latest_compute_stats)
   2825 {
   2826 	BeamformerUI *ui = ctx->ui;
   2827 
   2828 	ui->latest_plane[IPT_LAST]    = frame_to_draw;
   2829 	ui->latest_plane[frame_plane] = frame_to_draw;
   2830 	ui->latest_compute_stats      = latest_compute_stats;
   2831 
   2832 	/* TODO(rnp): there should be a better way of detecting this */
   2833 	if (ctx->ui_read_params) {
   2834 		mem_copy(&ui->params, &ctx->shared_memory->parameters.output_min_coordinate, sizeof(ui->params));
   2835 		ui->flush_params    = 0;
   2836 		ctx->ui_read_params = 0;
   2837 	}
   2838 
   2839 	/* NOTE: process interactions first because the user interacted with
   2840 	 * the ui that was presented last frame */
   2841 	ui_interact(ui, input, ctx->window_size);
   2842 
   2843 	if (ui->flush_params) {
   2844 		validate_ui_parameters(&ui->params);
   2845 		BeamformWork *work = beamform_work_queue_push(ctx->beamform_work_queue);
   2846 		if (work && try_wait_sync(&ctx->shared_memory->parameters_sync, 0, ctx->os.wait_on_value)) {
   2847 			BeamformerUploadContext *uc = &work->upload_context;
   2848 			uc->shared_memory_offset = offsetof(BeamformerSharedMemory, parameters);
   2849 			uc->size = sizeof(ctx->shared_memory->parameters);
   2850 			uc->kind = BU_KIND_PARAMETERS;
   2851 			work->type = BW_UPLOAD_BUFFER;
   2852 			work->completion_barrier = (iptr)&ctx->shared_memory->parameters_sync;
   2853 			mem_copy(&ctx->shared_memory->parameters_ui, &ui->params, sizeof(ui->params));
   2854 			beamform_work_queue_push_commit(ctx->beamform_work_queue);
   2855 			ui->flush_params   = 0;
   2856 			ctx->start_compute = 1;
   2857 		}
   2858 	}
   2859 
   2860 	/* NOTE(rnp): can't render to a different framebuffer in the middle of BeginDrawing()... */
   2861 	Rect window_rect = {.size = {.w = ctx->window_size.w, .h = ctx->window_size.h}};
   2862 	update_frame_views(ui, window_rect);
   2863 
   2864 	BeginDrawing();
   2865 		glClearColor(BG_COLOUR.r, BG_COLOUR.g, BG_COLOUR.b, BG_COLOUR.a);
   2866 		glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
   2867 
   2868 		draw_ui_regions(ui, window_rect, input->mouse);
   2869 		if (ui->interaction.type == IT_TEXT)
   2870 			draw_active_text_box(ui, ui->interaction.active);
   2871 		if (ui->interaction.type == IT_MENU)
   2872 			draw_active_menu(ui, ui->arena, ui->interaction.active, input->mouse, window_rect);
   2873 	EndDrawing();
   2874 }