ogl_beamforming

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

ui.c (98889B)


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