ogl_beamforming

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

ui.c (131466B)


      1 /* See LICENSE for license details. */
      2 /* TODO(rnp):
      3  * [ ]: bug: group at end of parameter listing
      4  * [ ]: refactor: ui should be in its own thread and that thread should only be concerned with the ui
      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  * [ ]: allow views to collapse to just their title bar
     10  *      - title bar struct with expanded. Check when pushing onto draw stack; if expanded
     11  *        do normal behaviour else make size title bar size and ignore the splits fraction.
     12  * [ ]: enforce a minimum region size or allow regions themselves to scroll
     13  * [ ]: refactor: add_variable_no_link()
     14  * [ ]: refactor: draw_text_limited should clamp to rect and measure text itself
     15  * [ ]: draw the ui with a post-order traversal instead of pre-order traversal
     16  * [ ]: consider V_HOVER_GROUP and use that to implement submenus
     17  * [ ]: menu's need to support nested groups
     18  * [ ]: don't redraw on every refresh; instead redraw on mouse movement/event or when a new frame
     19  *      arrives. For animations the ui can have a list of "timers" which while active will
     20  *      do a redraw on every refresh until completed.
     21  * [ ]: show full non-truncated string on hover
     22  * [ ]: refactor: hovered element type and show hovered element in full even when truncated
     23  * [ ]: visual indicator for broken shader stage gh#27
     24  * [ ]: bug: cross-plane view with different dimensions for each plane
     25  * [ ]: refactor: make table_skip_rows useful
     26  * [ ]: refactor: better method of grouping variables for views such as FrameView/ComputeStatsView
     27  */
     28 
     29 #define BG_COLOUR              (v4){.r = 0.15, .g = 0.12, .b = 0.13, .a = 1.0}
     30 #define FG_COLOUR              (v4){.r = 0.92, .g = 0.88, .b = 0.78, .a = 1.0}
     31 #define FOCUSED_COLOUR         (v4){.r = 0.86, .g = 0.28, .b = 0.21, .a = 1.0}
     32 #define HOVERED_COLOUR         (v4){.r = 0.11, .g = 0.50, .b = 0.59, .a = 1.0}
     33 #define RULER_COLOUR           (v4){.r = 1.00, .g = 0.70, .b = 0.00, .a = 1.0}
     34 #define BORDER_COLOUR          v4_lerp(FG_COLOUR, BG_COLOUR, 0.85)
     35 
     36 #define MENU_PLUS_COLOUR       (v4){.r = 0.33, .g = 0.42, .b = 1.00, .a = 1.0}
     37 #define MENU_CLOSE_COLOUR      FOCUSED_COLOUR
     38 
     39 read_only global v4 g_colour_palette[] = {
     40 	{{0.32, 0.20, 0.50, 1.00}},
     41 	{{0.14, 0.39, 0.61, 1.00}},
     42 	{{0.61, 0.14, 0.25, 1.00}},
     43 	{{0.20, 0.60, 0.24, 1.00}},
     44 	{{0.80, 0.60, 0.20, 1.00}},
     45 	{{0.15, 0.51, 0.74, 1.00}},
     46 };
     47 
     48 #define HOVER_SPEED            5.0f
     49 #define BLINK_SPEED            1.5f
     50 
     51 #define TABLE_CELL_PAD_HEIGHT  2.0f
     52 #define TABLE_CELL_PAD_WIDTH   8.0f
     53 
     54 #define RULER_TEXT_PAD         10.0f
     55 #define RULER_TICK_LENGTH      20.0f
     56 
     57 #define UI_SPLIT_HANDLE_THICK  8.0f
     58 #define UI_REGION_PAD          32.0f
     59 
     60 /* TODO(rnp) smooth scroll */
     61 #define UI_SCROLL_SPEED 12.0f
     62 
     63 #define LISTING_LINE_PAD    6.0f
     64 #define TITLE_BAR_PAD       6.0f
     65 
     66 typedef struct v2_sll {
     67 	struct v2_sll *next;
     68 	v2             v;
     69 } v2_sll;
     70 
     71 typedef struct BeamformerUI BeamformerUI;
     72 typedef struct Variable     Variable;
     73 
     74 typedef struct {
     75 	u8   buf[64];
     76 	i32  count;
     77 	i32  cursor;
     78 	f32  cursor_blink_t;
     79 	f32  cursor_blink_scale;
     80 	Font *font, *hot_font;
     81 	Variable *container;
     82 } InputState;
     83 
     84 typedef enum {
     85 	RulerState_None,
     86 	RulerState_Start,
     87 	RulerState_Hold,
     88 } RulerState;
     89 
     90 typedef struct {
     91 	v2 start;
     92 	v2 end;
     93 	RulerState state;
     94 } Ruler;
     95 
     96 typedef enum {
     97 	SB_LATERAL,
     98 	SB_AXIAL,
     99 } ScaleBarDirection;
    100 
    101 typedef struct {
    102 	f32    *min_value, *max_value;
    103 	v2_sll *savepoint_stack;
    104 	v2      scroll_scale;
    105 	f32     zoom_starting_coord;
    106 	ScaleBarDirection direction;
    107 } ScaleBar;
    108 
    109 typedef struct { f32 val, scale; } scaled_f32;
    110 
    111 typedef enum {
    112 	RSD_VERTICAL,
    113 	RSD_HORIZONTAL,
    114 } RegionSplitDirection;
    115 
    116 typedef struct {
    117 	Variable *left;
    118 	Variable *right;
    119 	f32       fraction;
    120 	RegionSplitDirection direction;
    121 } RegionSplit;
    122 
    123 #define COMPUTE_STATS_VIEW_LIST \
    124 	X(Average, "Average") \
    125 	X(Bar,     "Bar")
    126 
    127 #define X(kind, ...) ComputeStatsViewKind_ ##kind,
    128 typedef enum {COMPUTE_STATS_VIEW_LIST ComputeStatsViewKind_Count} ComputeStatsViewKind;
    129 #undef X
    130 
    131 typedef struct {
    132 	ComputeShaderStats *compute_shader_stats;
    133 	Variable           *cycler;
    134 	ComputeStatsViewKind kind;
    135 } ComputeStatsView;
    136 
    137 typedef struct {
    138 	b32 *processing;
    139 	f32 *progress;
    140 	f32 display_t;
    141 	f32 display_t_velocity;
    142 } ComputeProgressBar;
    143 
    144 typedef enum {
    145 	VT_NULL,
    146 	VT_B32,
    147 	VT_F32,
    148 	VT_I32,
    149 	VT_U32,
    150 	VT_GROUP,
    151 	VT_CYCLER,
    152 	VT_SCALED_F32,
    153 	VT_BEAMFORMER_VARIABLE,
    154 	VT_BEAMFORMER_FRAME_VIEW,
    155 	VT_COMPUTE_STATS_VIEW,
    156 	VT_COMPUTE_PROGRESS_BAR,
    157 	VT_LIVE_CONTROLS_VIEW,
    158 	VT_SCALE_BAR,
    159 	VT_UI_BUTTON,
    160 	VT_UI_MENU,
    161 	VT_UI_REGION_SPLIT,
    162 	VT_UI_TEXT_BOX,
    163 	VT_UI_VIEW,
    164 	VT_X_PLANE_SHIFT,
    165 } VariableType;
    166 
    167 typedef enum {
    168 	VariableGroupKind_List,
    169 	/* NOTE(rnp): special group for vectors with components
    170 	 * stored in separate memory locations */
    171 	VariableGroupKind_Vector,
    172 } VariableGroupKind;
    173 
    174 typedef struct {
    175 	VariableGroupKind kind;
    176 	b32       expanded;
    177 	Variable *first;
    178 	Variable *last;
    179 	Variable *container;
    180 } VariableGroup;
    181 
    182 typedef enum {
    183 	UIViewFlag_CustomText = 1 << 0,
    184 	UIViewFlag_Floating   = 1 << 1,
    185 } UIViewFlags;
    186 
    187 typedef struct {
    188 	Variable *child;
    189 	Variable *close;
    190 	Variable *menu;
    191 	Rect      rect;
    192 	UIViewFlags flags;
    193 } UIView;
    194 
    195 /* X(id, text) */
    196 #define FRAME_VIEW_BUTTONS \
    197 	X(FV_COPY_HORIZONTAL, "Copy Horizontal") \
    198 	X(FV_COPY_VERTICAL,   "Copy Vertical")
    199 
    200 #define GLOBAL_MENU_BUTTONS \
    201 	X(GM_OPEN_VIEW_RIGHT,   "Open View Right") \
    202 	X(GM_OPEN_VIEW_BELOW,   "Open View Below")
    203 
    204 #define X(id, text) UI_BID_ ##id,
    205 typedef enum {
    206 	UI_BID_VIEW_CLOSE,
    207 	GLOBAL_MENU_BUTTONS
    208 	FRAME_VIEW_BUTTONS
    209 } UIButtonID;
    210 #undef X
    211 
    212 typedef struct {
    213 	s8  *labels;
    214 	u32 *state;
    215 	u32  cycle_length;
    216 } VariableCycler;
    217 
    218 typedef struct {
    219 	s8  suffix;
    220 	f32 display_scale;
    221 	f32 scroll_scale;
    222 	v2  limits;
    223 	f32 *store;
    224 } BeamformerVariable;
    225 
    226 typedef struct {
    227 	v3 start_point;
    228 	v3 end_point;
    229 } XPlaneShift;
    230 
    231 typedef enum {
    232 	V_INPUT          = 1 << 0,
    233 	V_TEXT           = 1 << 1,
    234 	V_RADIO_BUTTON   = 1 << 2,
    235 	V_EXTRA_ACTION   = 1 << 3,
    236 	V_HIDES_CURSOR   = 1 << 4,
    237 	V_LIVE_CONTROL   = 1 << 28,
    238 	V_CAUSES_COMPUTE = 1 << 29,
    239 	V_UPDATE_VIEW    = 1 << 30,
    240 } VariableFlags;
    241 
    242 struct Variable {
    243 	s8 name;
    244 	union {
    245 		void               *generic;
    246 		BeamformerVariable  beamformer_variable;
    247 		ComputeProgressBar  compute_progress_bar;
    248 		ComputeStatsView    compute_stats_view;
    249 		RegionSplit         region_split;
    250 		ScaleBar            scale_bar;
    251 		UIButtonID          button;
    252 		UIView              view;
    253 		VariableCycler      cycler;
    254 		VariableGroup       group;
    255 		XPlaneShift         x_plane_shift;
    256 		scaled_f32          scaled_real32;
    257 		b32                 bool32;
    258 		i32                 signed32;
    259 		u32                 unsigned32;
    260 		f32                 real32;
    261 	};
    262 	Variable *next;
    263 	Variable *parent;
    264 	VariableFlags flags;
    265 	VariableType  type;
    266 
    267 	f32 hover_t;
    268 	f32 name_width;
    269 };
    270 
    271 #define BEAMFORMER_FRAME_VIEW_KIND_LIST \
    272 	X(Latest,   "Latest")     \
    273 	X(3DXPlane, "3D X-Plane") \
    274 	X(Indexed,  "Indexed")    \
    275 	X(Copy,     "Copy")
    276 
    277 typedef enum {
    278 	#define X(kind, ...) BeamformerFrameViewKind_##kind,
    279 	BEAMFORMER_FRAME_VIEW_KIND_LIST
    280 	#undef X
    281 	BeamformerFrameViewKind_Count,
    282 } BeamformerFrameViewKind;
    283 
    284 typedef struct BeamformerFrameView BeamformerFrameView;
    285 struct BeamformerFrameView {
    286 	BeamformerFrameViewKind kind;
    287 	b32 dirty;
    288 	BeamformerFrame     *frame;
    289 	BeamformerFrameView *prev, *next;
    290 
    291 	uv2 texture_dim;
    292 	u32 textures[2];
    293 	u32 texture_mipmaps;
    294 
    295 	/* NOTE(rnp): any pointers to variables are added to the menu and will
    296 	 * be put onto the freelist if the view is closed. */
    297 
    298 	Variable *kind_cycler;
    299 	Variable *log_scale;
    300 	Variable threshold;
    301 	Variable dynamic_range;
    302 	Variable gamma;
    303 
    304 	union {
    305 		/* BeamformerFrameViewKind_Latest/BeamformerFrameViewKind_Indexed */
    306 		struct {
    307 			Variable lateral_scale_bar;
    308 			Variable axial_scale_bar;
    309 			Variable *lateral_scale_bar_active;
    310 			Variable *axial_scale_bar_active;
    311 			/* NOTE(rnp): if kind is Latest  selects which plane to use
    312 			 *            if kind is Indexed selects the index */
    313 			Variable *cycler;
    314 			u32 cycler_state;
    315 
    316 			Ruler ruler;
    317 
    318 			v3 min_coordinate;
    319 			v3 max_coordinate;
    320 		};
    321 
    322 		/* BeamformerFrameViewKind_3DXPlane */
    323 		struct {
    324 			Variable x_plane_shifts[2];
    325 			Variable *demo;
    326 			f32 rotation;
    327 			v3  hit_test_point;
    328 		};
    329 	};
    330 };
    331 
    332 typedef struct BeamformerLiveControlsView BeamformerLiveControlsView;
    333 struct BeamformerLiveControlsView {
    334 	Variable transmit_power;
    335 	Variable tgc_control_points[countof(((BeamformerLiveImagingParameters *)0)->tgc_control_points)];
    336 	Variable save_button;
    337 	Variable stop_button;
    338 	f32      save_button_blink_t;
    339 	f32      save_button_blink_scale;
    340 	u32      hot_field_flag;
    341 };
    342 
    343 typedef enum {
    344 	InteractionKind_None,
    345 	InteractionKind_Nop,
    346 	InteractionKind_Auto,
    347 	InteractionKind_Button,
    348 	InteractionKind_Drag,
    349 	InteractionKind_Menu,
    350 	InteractionKind_Ruler,
    351 	InteractionKind_Scroll,
    352 	InteractionKind_Set,
    353 	InteractionKind_Text,
    354 } InteractionKind;
    355 
    356 typedef struct {
    357 	InteractionKind kind;
    358 	union {
    359 		void     *generic;
    360 		Variable *var;
    361 	};
    362 	Rect rect;
    363 } Interaction;
    364 
    365 #define auto_interaction(r, v) (Interaction){.kind = InteractionKind_Auto, .var = v, .rect = r}
    366 
    367 struct BeamformerUI {
    368 	Arena arena;
    369 
    370 	Font font;
    371 	Font small_font;
    372 
    373 	Variable *regions;
    374 	Variable *variable_freelist;
    375 
    376 	Variable floating_widget_sentinal;
    377 
    378 	BeamformerFrameView *views;
    379 	BeamformerFrameView *view_freelist;
    380 	BeamformerFrame     *frame_freelist;
    381 
    382 	Interaction interaction;
    383 	Interaction hot_interaction;
    384 	Interaction next_interaction;
    385 
    386 	InputState  text_input_state;
    387 
    388 	/* TODO(rnp): ideally this isn't copied all over the place */
    389 	BeamformerRenderModel unit_cube_model;
    390 
    391 	v2_sll *scale_bar_savepoint_freelist;
    392 
    393 	BeamformerFrame *latest_plane[BeamformerViewPlaneTag_Count + 1];
    394 
    395 	BeamformerUIParameters params;
    396 	b32                    flush_params;
    397 
    398 	FrameViewRenderContext *frame_view_render_context;
    399 
    400 	SharedMemoryRegion  shared_memory;
    401 	BeamformerCtx      *beamformer_context;
    402 };
    403 
    404 typedef enum {
    405 	TF_NONE     = 0,
    406 	TF_ROTATED  = 1 << 0,
    407 	TF_LIMITED  = 1 << 1,
    408 	TF_OUTLINED = 1 << 2,
    409 } TextFlags;
    410 
    411 typedef enum {
    412 	TextAlignment_Center,
    413 	TextAlignment_Left,
    414 	TextAlignment_Right,
    415 } TextAlignment;
    416 
    417 typedef struct {
    418 	Font  *font;
    419 	Rect  limits;
    420 	v4    colour;
    421 	v4    outline_colour;
    422 	f32   outline_thick;
    423 	f32   rotation;
    424 	TextAlignment align;
    425 	TextFlags     flags;
    426 } TextSpec;
    427 
    428 typedef enum {
    429 	TRK_CELLS,
    430 	TRK_TABLE,
    431 } TableRowKind;
    432 
    433 typedef enum {
    434 	TableCellKind_None,
    435 	TableCellKind_Variable,
    436 	TableCellKind_VariableGroup,
    437 } TableCellKind;
    438 
    439 typedef struct {
    440 	s8 text;
    441 	union {
    442 		i64       integer;
    443 		Variable *var;
    444 		void     *generic;
    445 	};
    446 	TableCellKind kind;
    447 	f32 width;
    448 } TableCell;
    449 
    450 typedef struct {
    451 	void         *data;
    452 	TableRowKind  kind;
    453 } TableRow;
    454 
    455 typedef struct Table {
    456 	TableRow *data;
    457 	iz        count;
    458 	iz        capacity;
    459 
    460 	/* NOTE(rnp): counted by columns */
    461 	TextAlignment *alignment;
    462 	f32           *widths;
    463 
    464 	v4  border_colour;
    465 	f32 column_border_thick;
    466 	f32 row_border_thick;
    467 	v2  size;
    468 	v2  cell_pad;
    469 
    470 	/* NOTE(rnp): row count including nested tables */
    471 	i32 rows;
    472 	i32 columns;
    473 
    474 	struct Table *parent;
    475 } Table;
    476 
    477 typedef struct {
    478 	Table *table;
    479 	i32    row_index;
    480 } TableStackFrame;
    481 
    482 typedef struct {
    483 	TableStackFrame *data;
    484 	iz count;
    485 	iz capacity;
    486 } TableStack;
    487 
    488 typedef enum {
    489 	TIK_ROWS,
    490 	TIK_CELLS,
    491 } TableIteratorKind;
    492 
    493 typedef struct {
    494 	TableStack      stack;
    495 	TableStackFrame frame;
    496 
    497 	TableRow *row;
    498 	i16       column;
    499 	i16       sub_table_depth;
    500 
    501 	TableIteratorKind kind;
    502 
    503 	f32           start_x;
    504 	TextAlignment alignment;
    505 	Rect          cell_rect;
    506 } TableIterator;
    507 
    508 function v2
    509 measure_glyph(Font font, u32 glyph)
    510 {
    511 	ASSERT(glyph >= 0x20);
    512 	v2 result = {.y = font.baseSize};
    513 	/* NOTE: assumes font glyphs are ordered ASCII */
    514 	result.x = font.glyphs[glyph - 0x20].advanceX;
    515 	if (result.x == 0)
    516 		result.x = (font.recs[glyph - 0x20].width + font.glyphs[glyph - 0x20].offsetX);
    517 	return result;
    518 }
    519 
    520 function v2
    521 measure_text(Font font, s8 text)
    522 {
    523 	v2 result = {.y = font.baseSize};
    524 	for (iz i = 0; i < text.len; i++)
    525 		result.x += measure_glyph(font, text.data[i]).x;
    526 	return result;
    527 }
    528 
    529 function s8
    530 clamp_text_to_width(Font font, s8 text, f32 limit)
    531 {
    532 	s8  result = text;
    533 	f32 width  = 0;
    534 	for (iz i = 0; i < text.len; i++) {
    535 		f32 next = measure_glyph(font, text.data[i]).w;
    536 		if (width + next > limit) {
    537 			result.len = i;
    538 			break;
    539 		}
    540 		width += next;
    541 	}
    542 	return result;
    543 }
    544 
    545 function v2
    546 align_text_in_rect(s8 text, Rect r, Font font)
    547 {
    548 	v2 size   = measure_text(font, text);
    549 	v2 pos    = v2_add(r.pos, v2_scale(v2_sub(r.size, size), 0.5));
    550 	v2 result = clamp_v2_rect(pos, r);
    551 	return result;
    552 }
    553 
    554 function Texture
    555 make_raylib_texture(BeamformerFrameView *v)
    556 {
    557 	Texture result;
    558 	result.id      = v->textures[0];
    559 	result.width   = v->texture_dim.w;
    560 	result.height  = v->texture_dim.h;
    561 	result.mipmaps = v->texture_mipmaps;
    562 	result.format  = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8;
    563 	return result;
    564 }
    565 
    566 function void
    567 stream_append_variable(Stream *s, Variable *var)
    568 {
    569 	switch (var->type) {
    570 	case VT_UI_BUTTON:
    571 	case VT_GROUP:{ stream_append_s8(s, var->name); }break;
    572 	case VT_F32:{   stream_append_f64(s, var->real32, 100); }break;
    573 	case VT_B32:{   stream_append_s8(s, var->bool32 ? s8("True") : s8("False")); }break;
    574 	case VT_SCALED_F32:{ stream_append_f64(s, var->scaled_real32.val, 100); }break;
    575 	case VT_BEAMFORMER_VARIABLE:{
    576 		BeamformerVariable *bv = &var->beamformer_variable;
    577 		stream_append_f64(s, *bv->store * bv->display_scale, 100);
    578 	}break;
    579 	case VT_CYCLER:{
    580 		u32 index = *var->cycler.state;
    581 		if (var->cycler.labels) stream_append_s8(s, var->cycler.labels[index]);
    582 		else                    stream_append_u64(s, index);
    583 	}break;
    584 	InvalidDefaultCase;
    585 	}
    586 }
    587 
    588 function void
    589 stream_append_variable_group(Stream *s, Variable *var)
    590 {
    591 	switch (var->type) {
    592 	case VT_GROUP:{
    593 		switch (var->group.kind) {
    594 		case VariableGroupKind_Vector:{
    595 			Variable *v = var->group.first;
    596 			stream_append_s8(s, s8("{"));
    597 			while (v) {
    598 				stream_append_variable(s, v);
    599 				v = v->next;
    600 				if (v) stream_append_s8(s, s8(", "));
    601 			}
    602 			stream_append_s8(s, s8("}"));
    603 		}break;
    604 		InvalidDefaultCase;
    605 		}
    606 	}break;
    607 	InvalidDefaultCase;
    608 	}
    609 }
    610 
    611 function s8
    612 push_das_shader_kind(Stream *s, DASShaderKind shader, u32 transmit_count)
    613 {
    614 	#define X(type, id, pretty, fixed_tx) s8_comp(pretty),
    615 	read_only local_persist s8 pretty_names[DASShaderKind_Count + 1] = {DAS_TYPES s8_comp("Invalid")};
    616 	#undef X
    617 	#define X(type, id, pretty, fixed_tx) fixed_tx,
    618 	read_only local_persist u8 fixed_transmits[DASShaderKind_Count + 1] = {DAS_TYPES 0};
    619 	#undef X
    620 
    621 	stream_append_s8(s, pretty_names[MIN(shader, DASShaderKind_Count)]);
    622 	if (!fixed_transmits[MIN(shader, DASShaderKind_Count)]) {
    623 		stream_append_byte(s, '-');
    624 		stream_append_u64(s, transmit_count);
    625 	}
    626 
    627 	return stream_to_s8(s);
    628 }
    629 
    630 function s8
    631 push_custom_view_title(Stream *s, Variable *var)
    632 {
    633 	switch (var->type) {
    634 	case VT_COMPUTE_STATS_VIEW:{
    635 		stream_append_s8(s, s8("Compute Stats: "));
    636 		stream_append_variable(s, var->compute_stats_view.cycler);
    637 	}break;
    638 	case VT_COMPUTE_PROGRESS_BAR:{
    639 		stream_append_s8(s, s8("Compute Progress: "));
    640 		stream_append_f64(s, 100 * *var->compute_progress_bar.progress, 100);
    641 		stream_append_byte(s, '%');
    642 	} break;
    643 	case VT_BEAMFORMER_FRAME_VIEW:{
    644 		BeamformerFrameView *bv = var->generic;
    645 		stream_append_s8(s, s8("Frame View"));
    646 		switch (bv->kind) {
    647 		case BeamformerFrameViewKind_Copy:{ stream_append_s8(s, s8(": Copy [")); }break;
    648 		case BeamformerFrameViewKind_Latest:{
    649 			#define X(plane, id, pretty) s8_comp(": " pretty " ["),
    650 			read_only local_persist s8 labels[BeamformerViewPlaneTag_Count + 1] = {
    651 				BEAMFORMER_VIEW_PLANE_TAG_LIST
    652 				s8_comp(": Live [")
    653 			};
    654 			#undef X
    655 			stream_append_s8(s, labels[*bv->cycler->cycler.state % (BeamformerViewPlaneTag_Count + 1)]);
    656 		}break;
    657 		case BeamformerFrameViewKind_Indexed:{
    658 			stream_append_s8(s, s8(": Index {"));
    659 			stream_append_u64(s, *bv->cycler->cycler.state % MAX_BEAMFORMED_SAVED_FRAMES);
    660 			stream_append_s8(s, s8("} ["));
    661 		}break;
    662 		case BeamformerFrameViewKind_3DXPlane:{ stream_append_s8(s, s8(": 3D X-Plane")); }break;
    663 		InvalidDefaultCase;
    664 		}
    665 		if (bv->kind != BeamformerFrameViewKind_3DXPlane) {
    666 			stream_append_hex_u64(s, bv->frame? bv->frame->id : 0);
    667 			stream_append_byte(s, ']');
    668 		}
    669 	}break;
    670 	InvalidDefaultCase;
    671 	}
    672 	return stream_to_s8(s);
    673 }
    674 
    675 #define table_new(a, init, ...) table_new_(a, init, arg_list(TextAlignment, ##__VA_ARGS__))
    676 function Table *
    677 table_new_(Arena *a, i32 initial_capacity, TextAlignment *alignment, i32 columns)
    678 {
    679 	Table *result = push_struct(a, Table);
    680 	da_reserve(a, result, initial_capacity);
    681 	result->columns   = columns;
    682 	result->alignment = push_array(a, TextAlignment, columns);
    683 	result->widths    = push_array(a, f32, columns);
    684 	result->cell_pad  = (v2){{TABLE_CELL_PAD_WIDTH, TABLE_CELL_PAD_HEIGHT}};
    685 	mem_copy(result->alignment, alignment, sizeof(*alignment) * columns);
    686 	return result;
    687 }
    688 
    689 function i32
    690 table_skip_rows(Table *t, f32 draw_height, f32 text_height)
    691 {
    692 	i32 max_rows = draw_height / (text_height + t->cell_pad.h);
    693 	i32 result   = t->rows - MIN(t->rows, max_rows);
    694 	return result;
    695 }
    696 
    697 function TableIterator *
    698 table_iterator_new(Table *table, TableIteratorKind kind, Arena *a, i32 starting_row, v2 at, Font *font)
    699 {
    700 	TableIterator *result    = push_struct(a, TableIterator);
    701 	result->kind             = kind;
    702 	result->frame.table      = table;
    703 	result->frame.row_index  = starting_row;
    704 	result->start_x          = at.x;
    705 	result->cell_rect.size.h = font->baseSize;
    706 	result->cell_rect.pos    = v2_add(at, v2_scale(table->cell_pad, 0.5));
    707 	result->cell_rect.pos.y += (starting_row - 1) * (result->cell_rect.size.h + table->cell_pad.h + table->row_border_thick);
    708 	da_reserve(a, &result->stack, 4);
    709 	return result;
    710 }
    711 
    712 function void *
    713 table_iterator_next(TableIterator *it, Arena *a)
    714 {
    715 	void *result = 0;
    716 
    717 	if (!it->row || it->kind == TIK_ROWS) {
    718 		for (;;) {
    719 			TableRow *row = it->frame.table->data + it->frame.row_index++;
    720 			if (it->frame.row_index <= it->frame.table->count) {
    721 				if (row->kind == TRK_TABLE) {
    722 					*da_push(a, &it->stack) = it->frame;
    723 					it->frame = (TableStackFrame){.table = row->data};
    724 					it->sub_table_depth++;
    725 				} else {
    726 					result = row;
    727 					break;
    728 				}
    729 			} else if (it->stack.count) {
    730 				it->frame = it->stack.data[--it->stack.count];
    731 				it->sub_table_depth--;
    732 			} else {
    733 				break;
    734 			}
    735 		}
    736 		Table *t   = it->frame.table;
    737 		it->row    = result;
    738 		it->column = 0;
    739 		it->cell_rect.pos.x  = it->start_x + t->cell_pad.w / 2 +
    740 		                       it->cell_rect.size.h * it->sub_table_depth;
    741 		it->cell_rect.pos.y += it->cell_rect.size.h + t->row_border_thick + t->cell_pad.h;
    742 	}
    743 
    744 	if (it->row && it->kind == TIK_CELLS) {
    745 		Table *t   = it->frame.table;
    746 		i32 column = it->column++;
    747 		it->cell_rect.pos.x  += column > 0 ? it->cell_rect.size.w + t->cell_pad.w : 0;
    748 		it->cell_rect.size.w  = t->widths[column];
    749 		it->alignment         = t->alignment[column];
    750 		result                = (TableCell *)it->row->data + column;
    751 
    752 		if (it->column == t->columns)
    753 			it->row = 0;
    754 	}
    755 
    756 	return result;
    757 }
    758 
    759 function f32
    760 table_width(Table *t)
    761 {
    762 	f32 result = 0;
    763 	i32 valid  = 0;
    764 	for (i32 i = 0; i < t->columns; i++) {
    765 		result += t->widths[i];
    766 		if (t->widths[i] > 0) valid++;
    767 	}
    768 	result += t->cell_pad.w * valid;
    769 	result += MAX(0, (valid - 1)) * t->column_border_thick;
    770 	return result;
    771 }
    772 
    773 function v2
    774 table_extent(Table *t, Arena arena, Font *font)
    775 {
    776 	TableIterator *it = table_iterator_new(t, TIK_ROWS, &arena, 0, (v2){0}, font);
    777 	for (TableRow *row = table_iterator_next(it, &arena);
    778 	     row;
    779 	     row = table_iterator_next(it, &arena))
    780 	{
    781 		for (i32 i = 0; i < it->frame.table->columns; i++) {
    782 			TableCell *cell = (TableCell *)row->data + i;
    783 			if (!cell->text.len && cell->var && cell->var->flags & V_RADIO_BUTTON) {
    784 				cell->width = font->baseSize;
    785 			} else {
    786 				cell->width = measure_text(*font, cell->text).w;
    787 			}
    788 			it->frame.table->widths[i] = MAX(cell->width, it->frame.table->widths[i]);
    789 		}
    790 	}
    791 
    792 	t->size = (v2){.x = table_width(t), .y = it->cell_rect.pos.y - t->cell_pad.h / 2};
    793 	v2 result = t->size;
    794 	return result;
    795 }
    796 
    797 function v2
    798 table_cell_align(TableCell *cell, TextAlignment align, Rect r)
    799 {
    800 	v2 result = r.pos;
    801 	if (r.size.w >= cell->width) {
    802 		switch (align) {
    803 		case TextAlignment_Left:{}break;
    804 		case TextAlignment_Right:{  result.x += (r.size.w - cell->width);     }break;
    805 		case TextAlignment_Center:{ result.x += (r.size.w - cell->width) / 2; }break;
    806 		}
    807 	}
    808 	return result;
    809 }
    810 
    811 function TableCell
    812 table_variable_cell(Arena *a, Variable *var)
    813 {
    814 	TableCell result = {.var = var, .kind = TableCellKind_Variable};
    815 	if ((var->flags & V_RADIO_BUTTON) == 0) {
    816 		Stream text = arena_stream(*a);
    817 		stream_append_variable(&text, var);
    818 		result.text = arena_stream_commit(a, &text);
    819 	}
    820 	return result;
    821 }
    822 
    823 function TableRow *
    824 table_push_row(Table *t, Arena *a, TableRowKind kind)
    825 {
    826 	TableRow *result = da_push(a, t);
    827 	if (kind == TRK_CELLS) {
    828 		result->data = push_array(a, TableCell, t->columns);
    829 		/* NOTE(rnp): do not increase rows for an empty subtable */
    830 		t->rows++;
    831 	}
    832 	result->kind = kind;
    833 	return result;
    834 }
    835 
    836 function TableRow *
    837 table_push_parameter_row(Table *t, Arena *a, s8 label, Variable *var, s8 suffix)
    838 {
    839 	ASSERT(t->columns >= 3);
    840 	TableRow *result = table_push_row(t, a, TRK_CELLS);
    841 	TableCell *cells = result->data;
    842 
    843 	cells[0].text  = label;
    844 	cells[1]       = table_variable_cell(a, var);
    845 	cells[2].text  = suffix;
    846 
    847 	return result;
    848 }
    849 
    850 #define table_begin_subtable(t, a, ...) table_begin_subtable_(t, a, arg_list(TextAlignment, ##__VA_ARGS__))
    851 function Table *
    852 table_begin_subtable_(Table *table, Arena *a, TextAlignment *alignment, i32 columns)
    853 {
    854 	TableRow *row = table_push_row(table, a, TRK_TABLE);
    855 	Table *result = row->data = table_new_(a, 0, alignment, columns);
    856 	result->parent = table;
    857 	return result;
    858 }
    859 
    860 function Table *
    861 table_end_subtable(Table *table)
    862 {
    863 	Table *result = table->parent ? table->parent : table;
    864 	return result;
    865 }
    866 
    867 function void
    868 resize_frame_view(BeamformerFrameView *view, uv2 dim, b32 depth)
    869 {
    870 	glDeleteTextures(countof(view->textures), view->textures);
    871 	glCreateTextures(GL_TEXTURE_2D, countof(view->textures) - !!depth, view->textures);
    872 
    873 	view->texture_dim     = dim;
    874 	view->texture_mipmaps = ctz_u32(MAX(dim.x, dim.y)) + 1;
    875 	glTextureStorage2D(view->textures[0], view->texture_mipmaps, GL_RGBA8, dim.x, dim.y);
    876 	if (depth) glTextureStorage2D(view->textures[1], 1, GL_DEPTH_COMPONENT24, dim.x, dim.y);
    877 
    878 	glGenerateTextureMipmap(view->textures[0]);
    879 
    880 	/* NOTE(rnp): work around raylib's janky texture sampling */
    881 	v4 border_colour = {0};
    882 	if (view->kind == BeamformerFrameViewKind_Copy) border_colour = (v4){{0, 0, 0, 1}};
    883 	glTextureParameteri(view->textures[0], GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    884 	glTextureParameteri(view->textures[0], GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    885 	glTextureParameterfv(view->textures[0], GL_TEXTURE_BORDER_COLOR, border_colour.E);
    886 	/* TODO(rnp): better choice when depth component is included */
    887 	glTextureParameteri(view->textures[0], GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    888 	glTextureParameteri(view->textures[0], GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    889 
    890 	/* TODO(rnp): add some ID for the specific view here */
    891 	LABEL_GL_OBJECT(GL_TEXTURE, view->textures[0], s8("Frame View Texture"));
    892 }
    893 
    894 function void
    895 ui_beamformer_frame_view_release_subresources(BeamformerUI *ui, BeamformerFrameView *bv, BeamformerFrameViewKind kind)
    896 {
    897 	if (kind == BeamformerFrameViewKind_Copy && bv->frame) {
    898 		glDeleteTextures(1, &bv->frame->texture);
    899 		bv->frame->texture = 0;
    900 		SLLPush(bv->frame, ui->frame_freelist);
    901 	}
    902 
    903 	if (kind != BeamformerFrameViewKind_3DXPlane) {
    904 		if (bv->axial_scale_bar.scale_bar.savepoint_stack)
    905 			SLLPush(bv->axial_scale_bar.scale_bar.savepoint_stack, ui->scale_bar_savepoint_freelist);
    906 		if (bv->lateral_scale_bar.scale_bar.savepoint_stack)
    907 			SLLPush(bv->lateral_scale_bar.scale_bar.savepoint_stack, ui->scale_bar_savepoint_freelist);
    908 	}
    909 }
    910 
    911 function void
    912 ui_variable_free(BeamformerUI *ui, Variable *var)
    913 {
    914 	if (var) {
    915 		var->parent = 0;
    916 		while (var) {
    917 			if (var->type == VT_GROUP) {
    918 				var = var->group.first;
    919 			} else {
    920 				if (var->type == VT_BEAMFORMER_FRAME_VIEW) {
    921 					/* TODO(rnp): instead there should be a way of linking these up */
    922 					BeamformerFrameView *bv = var->generic;
    923 					ui_beamformer_frame_view_release_subresources(ui, bv, bv->kind);
    924 					DLLRemove(bv);
    925 					/* TODO(rnp): hack; use a sentinal */
    926 					if (bv == ui->views)
    927 						ui->views = bv->next;
    928 					SLLPush(bv, ui->view_freelist);
    929 				}
    930 
    931 				Variable *next = var->next;
    932 				SLLPush(var, ui->variable_freelist);
    933 				if (next) {
    934 					var = next;
    935 				} else {
    936 					var = var->parent;
    937 					/* NOTE(rnp): when we assign parent here we have already
    938 					 * released the children. Assign type so we don't loop */
    939 					if (var) var->type = VT_NULL;
    940 				}
    941 			}
    942 		}
    943 	}
    944 }
    945 
    946 function void
    947 ui_variable_free_group_items(BeamformerUI *ui, Variable *group)
    948 {
    949 	assert(group->type == VT_GROUP);
    950 	/* NOTE(rnp): prevent traversal back to us */
    951 	group->group.last->parent = 0;
    952 	ui_variable_free(ui, group->group.first);
    953 	group->group.first = group->group.last = 0;
    954 }
    955 
    956 function void
    957 ui_view_free(BeamformerUI *ui, Variable *view)
    958 {
    959 	assert(view->type == VT_UI_VIEW);
    960 	ui_variable_free(ui, view->view.child);
    961 	ui_variable_free(ui, view->view.close);
    962 	ui_variable_free(ui, view->view.menu);
    963 	ui_variable_free(ui, view);
    964 }
    965 
    966 function Variable *
    967 fill_variable(Variable *var, Variable *group, s8 name, u32 flags, VariableType type, Font font)
    968 {
    969 	var->flags      = flags;
    970 	var->type       = type;
    971 	var->name       = name;
    972 	var->parent     = group;
    973 	var->name_width = measure_text(font, name).x;
    974 
    975 	if (group && group->type == VT_GROUP) {
    976 		if (group->group.last) group->group.last = group->group.last->next = var;
    977 		else                   group->group.last = group->group.first      = var;
    978 	}
    979 
    980 	return var;
    981 }
    982 
    983 function Variable *
    984 add_variable(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, u32 flags,
    985              VariableType type, Font font)
    986 {
    987 	Variable *result = SLLPop(ui->variable_freelist);
    988 	if (result) zero_struct(result);
    989 	else        result = push_struct(arena, Variable);
    990 	return fill_variable(result, group, name, flags, type, font);
    991 }
    992 
    993 function Variable *
    994 add_variable_group(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, VariableGroupKind kind, Font font)
    995 {
    996 	Variable *result   = add_variable(ui, group, arena, name, V_INPUT, VT_GROUP, font);
    997 	result->group.kind = kind;
    998 	return result;
    999 }
   1000 
   1001 function Variable *
   1002 end_variable_group(Variable *group)
   1003 {
   1004 	ASSERT(group->type == VT_GROUP);
   1005 	return group->parent;
   1006 }
   1007 
   1008 function void
   1009 fill_variable_cycler(Variable *cycler, u32 *store, s8 *labels, u32 cycle_count)
   1010 {
   1011 	cycler->cycler.cycle_length = cycle_count;
   1012 	cycler->cycler.state        = store;
   1013 	cycler->cycler.labels       = labels;
   1014 }
   1015 
   1016 function Variable *
   1017 add_variable_cycler(BeamformerUI *ui, Variable *group, Arena *arena, u32 flags, Font font, s8 name,
   1018                     u32 *store, s8 *labels, u32 cycle_count)
   1019 {
   1020 	Variable *result = add_variable(ui, group, arena, name, V_INPUT|flags, VT_CYCLER, font);
   1021 	fill_variable_cycler(result, store, labels, cycle_count);
   1022 	return result;
   1023 }
   1024 
   1025 function Variable *
   1026 add_button(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, UIButtonID id,
   1027            u32 flags, Font font)
   1028 {
   1029 	Variable *result = add_variable(ui, group, arena, name, V_INPUT|flags, VT_UI_BUTTON, font);
   1030 	result->button   = id;
   1031 	return result;
   1032 }
   1033 
   1034 function Variable *
   1035 add_ui_split(BeamformerUI *ui, Variable *parent, Arena *arena, s8 name, f32 fraction,
   1036              RegionSplitDirection direction, Font font)
   1037 {
   1038 	Variable *result = add_variable(ui, parent, arena, name, V_HIDES_CURSOR, VT_UI_REGION_SPLIT, font);
   1039 	result->region_split.direction = direction;
   1040 	result->region_split.fraction  = fraction;
   1041 	return result;
   1042 }
   1043 
   1044 function Variable *
   1045 add_global_menu_to_group(BeamformerUI *ui, Arena *arena, Variable *group)
   1046 {
   1047 	#define X(id, text) add_button(ui, group, arena, s8(text), UI_BID_ ##id, 0, ui->small_font);
   1048 	GLOBAL_MENU_BUTTONS
   1049 	#undef X
   1050 	return group;
   1051 }
   1052 
   1053 function Variable *
   1054 add_global_menu(BeamformerUI *ui, Arena *arena, Variable *parent)
   1055 {
   1056 	Variable *result = add_variable_group(ui, 0, arena, s8(""), VariableGroupKind_List, ui->small_font);
   1057 	result->parent = parent;
   1058 	return add_global_menu_to_group(ui, arena, result);
   1059 }
   1060 
   1061 function Variable *
   1062 add_ui_view(BeamformerUI *ui, Variable *parent, Arena *arena, s8 name, u32 view_flags, b32 menu, b32 closable)
   1063 {
   1064 	Variable *result = add_variable(ui, parent, arena, name, 0, VT_UI_VIEW, ui->small_font);
   1065 	UIView   *view   = &result->view;
   1066 	view->flags      = view_flags;
   1067 	if (menu) view->menu = add_global_menu(ui, arena, result);
   1068 	if (closable) {
   1069 		view->close = add_button(ui, 0, arena, s8(""), UI_BID_VIEW_CLOSE, 0, ui->small_font);
   1070 		/* NOTE(rnp): we do this explicitly so that close doesn't end up in the view group */
   1071 		view->close->parent = result;
   1072 	}
   1073 	return result;
   1074 }
   1075 
   1076 function Variable *
   1077 add_floating_view(BeamformerUI *ui, Arena *arena, VariableType type, v2 at, Variable *child, b32 closable)
   1078 {
   1079 	Variable *result = add_ui_view(ui, 0, arena, s8(""), UIViewFlag_Floating, 0, closable);
   1080 	result->type          = type;
   1081 	result->view.rect.pos = at;
   1082 	result->view.child    = child;
   1083 
   1084 	result->parent = &ui->floating_widget_sentinal;
   1085 	result->next   = ui->floating_widget_sentinal.next;
   1086 	result->next->parent = result;
   1087 	ui->floating_widget_sentinal.next = result;
   1088 	return result;
   1089 }
   1090 
   1091 function void
   1092 fill_beamformer_variable(Variable *var, s8 suffix, f32 *store, v2 limits, f32 display_scale, f32 scroll_scale)
   1093 {
   1094 	BeamformerVariable *bv = &var->beamformer_variable;
   1095 	bv->suffix        = suffix;
   1096 	bv->store         = store;
   1097 	bv->display_scale = display_scale;
   1098 	bv->scroll_scale  = scroll_scale;
   1099 	bv->limits        = limits;
   1100 }
   1101 
   1102 function void
   1103 add_beamformer_variable(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, s8 suffix, f32 *store,
   1104                         v2 limits, f32 display_scale, f32 scroll_scale, u32 flags, Font font)
   1105 {
   1106 	Variable *var = add_variable(ui, group, arena, name, flags, VT_BEAMFORMER_VARIABLE, font);
   1107 	fill_beamformer_variable(var, suffix, store, limits, display_scale, scroll_scale);
   1108 }
   1109 
   1110 function Variable *
   1111 add_beamformer_parameters_view(Variable *parent, BeamformerCtx *ctx)
   1112 {
   1113 	BeamformerUI *ui           = ctx->ui;
   1114 	BeamformerUIParameters *bp = &ui->params;
   1115 
   1116 	v2 v2_inf = {.x = -F32_INFINITY, .y = F32_INFINITY};
   1117 
   1118 	/* TODO(rnp): this can be closable once we have a way of opening new views */
   1119 	Variable *result = add_ui_view(ui, parent, &ui->arena, s8("Parameters"), 0, 1, 0);
   1120 	Variable *group  = result->view.child = add_variable(ui, result, &ui->arena, s8(""), 0,
   1121 	                                                     VT_GROUP, ui->font);
   1122 
   1123 	add_beamformer_variable(ui, group, &ui->arena, s8("Sampling Frequency:"), s8("[MHz]"),
   1124 	                        &bp->sampling_frequency, (v2){0}, 1e-6, 0, 0, ui->font);
   1125 
   1126 	add_beamformer_variable(ui, group, &ui->arena, s8("Center Frequency:"), s8("[MHz]"),
   1127 	                        &bp->center_frequency, (v2){.y = 100e-6}, 1e-6, 1e5,
   1128 	                        V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
   1129 
   1130 	add_beamformer_variable(ui, group, &ui->arena, s8("Speed of Sound:"), s8("[m/s]"),
   1131 	                        &bp->speed_of_sound, (v2){.y = 1e6}, 1, 10,
   1132 	                        V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
   1133 
   1134 	group = add_variable_group(ui, group, &ui->arena, s8("Lateral Extent:"),
   1135 	                           VariableGroupKind_Vector, ui->font);
   1136 	{
   1137 		add_beamformer_variable(ui, group, &ui->arena, s8("Min:"), s8("[mm]"),
   1138 		                       bp->output_min_coordinate + 0, v2_inf, 1e3, 0.5e-3,
   1139 		                       V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
   1140 
   1141 		add_beamformer_variable(ui, group, &ui->arena, s8("Max:"), s8("[mm]"),
   1142 		                        bp->output_max_coordinate + 0, v2_inf, 1e3, 0.5e-3,
   1143 		                        V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
   1144 	}
   1145 	group = end_variable_group(group);
   1146 
   1147 	group = add_variable_group(ui, group, &ui->arena, s8("Axial Extent:"),
   1148 	                           VariableGroupKind_Vector, ui->font);
   1149 	{
   1150 		add_beamformer_variable(ui, group, &ui->arena, s8("Min:"), s8("[mm]"),
   1151 		                        bp->output_min_coordinate + 2, v2_inf, 1e3, 0.5e-3,
   1152 		                        V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
   1153 
   1154 		add_beamformer_variable(ui, group, &ui->arena, s8("Max:"), s8("[mm]"),
   1155 		                        bp->output_max_coordinate + 2, v2_inf, 1e3, 0.5e-3,
   1156 		                        V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
   1157 	}
   1158 	group = end_variable_group(group);
   1159 
   1160 	add_beamformer_variable(ui, group, &ui->arena, s8("Off Axis Position:"), s8("[mm]"),
   1161 	                        &bp->off_axis_pos, (v2){.x = -1e3, .y = 1e3}, 0.25e3, 0.5e-3,
   1162 	                        V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
   1163 
   1164 	read_only local_persist s8 beamform_plane_labels[] = {s8_comp("XZ"), s8_comp("YZ")};
   1165 	add_variable_cycler(ui, group, &ui->arena, V_CAUSES_COMPUTE, ui->font, s8("Beamform Plane:"),
   1166 	                    (u32 *)&bp->beamform_plane, beamform_plane_labels, countof(beamform_plane_labels));
   1167 
   1168 	add_beamformer_variable(ui, group, &ui->arena, s8("F#:"), s8(""), &bp->f_number, (v2){.y = 1e3},
   1169 	                        1, 0.1, V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font);
   1170 
   1171 	read_only local_persist s8 true_false_labels[] = {s8_comp("False"), s8_comp("True")};
   1172 	add_variable_cycler(ui, group, &ui->arena, V_CAUSES_COMPUTE, ui->font, s8("Interpolate:"),
   1173 	                    &bp->interpolate, true_false_labels, countof(true_false_labels));
   1174 
   1175 	add_variable_cycler(ui, group, &ui->arena, V_CAUSES_COMPUTE, ui->font, s8("Coherency Weighting:"),
   1176 	                    &bp->coherency_weighting, true_false_labels, countof(true_false_labels));
   1177 
   1178 	return result;
   1179 }
   1180 
   1181 function void
   1182 ui_beamformer_frame_view_convert(BeamformerUI *ui, Arena *arena, Variable *view, Variable *menu,
   1183                                  BeamformerFrameViewKind kind, BeamformerFrameView *old, b32 log_scale)
   1184 {
   1185 	assert(menu->group.first == menu->group.last && menu->group.first == 0);
   1186 	assert(view->type == VT_BEAMFORMER_FRAME_VIEW);
   1187 
   1188 	BeamformerFrameView *bv = view->generic;
   1189 	bv->kind  = kind;
   1190 	bv->dirty = 1;
   1191 
   1192 	fill_variable(&bv->dynamic_range, view, s8("Dynamic Range:"), V_INPUT|V_TEXT|V_UPDATE_VIEW,
   1193 	              VT_F32, ui->small_font);
   1194 	fill_variable(&bv->threshold, view, s8("Threshold:"), V_INPUT|V_TEXT|V_UPDATE_VIEW,
   1195 	              VT_F32, ui->small_font);
   1196 	fill_variable(&bv->gamma, view, s8("Gamma:"), V_INPUT|V_TEXT|V_UPDATE_VIEW,
   1197 	              VT_SCALED_F32, ui->small_font);
   1198 
   1199 	bv->dynamic_range.real32      = old? old->dynamic_range.real32      : 50.0f;
   1200 	bv->threshold.real32          = old? old->threshold.real32          : 55.0f;
   1201 	bv->gamma.scaled_real32.val   = old? old->gamma.scaled_real32.val   : 1.0f;
   1202 	bv->gamma.scaled_real32.scale = old? old->gamma.scaled_real32.scale : 0.05f;
   1203 	bv->min_coordinate = (old && old->frame)? old->frame->min_coordinate.xyz : (v3){0};
   1204 	bv->max_coordinate = (old && old->frame)? old->frame->max_coordinate.xyz : (v3){0};
   1205 
   1206 	#define X(_t, pretty) s8_comp(pretty),
   1207 	read_only local_persist s8 kind_labels[] = {BEAMFORMER_FRAME_VIEW_KIND_LIST};
   1208 	#undef X
   1209 	bv->kind_cycler = add_variable_cycler(ui, menu, arena, V_EXTRA_ACTION, ui->small_font,
   1210 	                                      s8("Kind:"), &bv->kind, kind_labels, countof(kind_labels));
   1211 
   1212 	switch (kind) {
   1213 	case BeamformerFrameViewKind_3DXPlane:{
   1214 		view->flags |= V_HIDES_CURSOR;
   1215 		resize_frame_view(bv, (uv2){{FRAME_VIEW_RENDER_TARGET_SIZE}}, 0);
   1216 		glTextureParameteri(bv->textures[0], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
   1217 		glTextureParameteri(bv->textures[0], GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
   1218 		fill_variable(bv->x_plane_shifts + 0, view, s8("XZ Shift"), V_INPUT|V_HIDES_CURSOR,
   1219 		              VT_X_PLANE_SHIFT, ui->small_font);
   1220 		fill_variable(bv->x_plane_shifts + 1, view, s8("YZ Shift"), V_INPUT|V_HIDES_CURSOR,
   1221 		              VT_X_PLANE_SHIFT, ui->small_font);
   1222 		bv->demo = add_variable(ui, menu, arena, s8("Demo Mode"), V_INPUT|V_RADIO_BUTTON, VT_B32, ui->small_font);
   1223 	}break;
   1224 	default:{
   1225 		view->flags &= ~V_HIDES_CURSOR;
   1226 		fill_variable(&bv->lateral_scale_bar, view, s8(""), V_INPUT, VT_SCALE_BAR, ui->small_font);
   1227 		fill_variable(&bv->axial_scale_bar,   view, s8(""), V_INPUT, VT_SCALE_BAR, ui->small_font);
   1228 		ScaleBar *lateral            = &bv->lateral_scale_bar.scale_bar;
   1229 		ScaleBar *axial              = &bv->axial_scale_bar.scale_bar;
   1230 		lateral->direction           = SB_LATERAL;
   1231 		axial->direction             = SB_AXIAL;
   1232 		lateral->scroll_scale        = (v2){{-0.5e-3, 0.5e-3}};
   1233 		axial->scroll_scale          = (v2){{ 0,      1.0e-3}};
   1234 		lateral->zoom_starting_coord = F32_INFINITY;
   1235 		axial->zoom_starting_coord   = F32_INFINITY;
   1236 
   1237 		b32 copy = kind == BeamformerFrameViewKind_Copy;
   1238 		lateral->min_value = copy ? &bv->min_coordinate.x : ui->params.output_min_coordinate + 0;
   1239 		lateral->max_value = copy ? &bv->max_coordinate.x : ui->params.output_max_coordinate + 0;
   1240 		axial->min_value   = copy ? &bv->min_coordinate.z : ui->params.output_min_coordinate + 2;
   1241 		axial->max_value   = copy ? &bv->max_coordinate.z : ui->params.output_max_coordinate + 2;
   1242 
   1243 		#define X(id, text) add_button(ui, menu, arena, s8(text), UI_BID_ ##id, 0, ui->small_font);
   1244 		FRAME_VIEW_BUTTONS
   1245 		#undef X
   1246 
   1247 		bv->axial_scale_bar_active   = add_variable(ui, menu, arena, s8("Axial Scale Bar"),
   1248 		                                            V_INPUT|V_RADIO_BUTTON, VT_B32, ui->small_font);
   1249 		bv->lateral_scale_bar_active = add_variable(ui, menu, arena, s8("Lateral Scale Bar"),
   1250 		                                            V_INPUT|V_RADIO_BUTTON, VT_B32, ui->small_font);
   1251 
   1252 		if (kind == BeamformerFrameViewKind_Latest) {
   1253 			bv->axial_scale_bar_active->bool32   = 1;
   1254 			bv->lateral_scale_bar_active->bool32 = 1;
   1255 			bv->axial_scale_bar.flags   |= V_CAUSES_COMPUTE;
   1256 			bv->lateral_scale_bar.flags |= V_CAUSES_COMPUTE;
   1257 		}
   1258 	}break;
   1259 	}
   1260 
   1261 	bv->log_scale = add_variable(ui, menu, arena, s8("Log Scale"),
   1262 	                             V_INPUT|V_UPDATE_VIEW|V_RADIO_BUTTON, VT_B32, ui->small_font);
   1263 	bv->log_scale->bool32 = log_scale;
   1264 
   1265 	switch (kind) {
   1266 	case BeamformerFrameViewKind_Latest:{
   1267 		#define X(_type, _id, pretty) s8_comp(pretty),
   1268 		read_only local_persist s8 labels[] = {BEAMFORMER_VIEW_PLANE_TAG_LIST s8_comp("Any")};
   1269 		#undef X
   1270 		bv->cycler = add_variable_cycler(ui, menu, arena, 0, ui->small_font, s8("Live:"),
   1271 		                                 &bv->cycler_state, labels, countof(labels));
   1272 		bv->cycler_state = BeamformerViewPlaneTag_Count;
   1273 	}break;
   1274 	case BeamformerFrameViewKind_Indexed:{
   1275 		bv->cycler = add_variable_cycler(ui, menu, arena, 0, ui->small_font, s8("Index:"),
   1276 		                                 &bv->cycler_state, 0, MAX_BEAMFORMED_SAVED_FRAMES);
   1277 	}break;
   1278 	default:{}break;
   1279 	}
   1280 
   1281 	add_global_menu_to_group(ui, arena, menu);
   1282 }
   1283 
   1284 function BeamformerFrameView *
   1285 ui_beamformer_frame_view_new(BeamformerUI *ui, Arena *arena)
   1286 {
   1287 	BeamformerFrameView *result = SLLPop(ui->view_freelist);
   1288 	if (result) zero_struct(result);
   1289 	else        result = push_struct(arena, typeof(*result));
   1290 	DLLPushDown(result, ui->views);
   1291 	return result;
   1292 }
   1293 
   1294 function Variable *
   1295 add_beamformer_frame_view(BeamformerUI *ui, Variable *parent, Arena *arena,
   1296                           BeamformerFrameViewKind kind, b32 closable, BeamformerFrameView *old)
   1297 {
   1298 	/* TODO(rnp): this can be always closable once we have a way of opening new views */
   1299 	Variable *result = add_ui_view(ui, parent, arena, s8(""), UIViewFlag_CustomText, 1, closable);
   1300 	Variable *var = result->view.child = add_variable(ui, result, arena, s8(""), 0,
   1301 	                                                  VT_BEAMFORMER_FRAME_VIEW, ui->small_font);
   1302 	Variable *menu = result->view.menu = add_variable_group(ui, 0, arena, s8(""),
   1303 	                                                        VariableGroupKind_List, ui->small_font);
   1304 	menu->parent = result;
   1305 	var->generic = ui_beamformer_frame_view_new(ui, arena);
   1306 	ui_beamformer_frame_view_convert(ui, arena, var, menu, kind, old, old? old->log_scale->bool32 : 0);
   1307 	return result;
   1308 }
   1309 
   1310 function Variable *
   1311 add_compute_progress_bar(Variable *parent, BeamformerCtx *ctx)
   1312 {
   1313 	BeamformerUI *ui = ctx->ui;
   1314 	/* TODO(rnp): this can be closable once we have a way of opening new views */
   1315 	Variable *result = add_ui_view(ui, parent, &ui->arena, s8(""), UIViewFlag_CustomText, 1, 0);
   1316 	result->view.child = add_variable(ui, result, &ui->arena, s8(""), 0,
   1317 	                                  VT_COMPUTE_PROGRESS_BAR, ui->small_font);
   1318 	ComputeProgressBar *bar = &result->view.child->compute_progress_bar;
   1319 	bar->progress   = &ctx->csctx.processing_progress;
   1320 	bar->processing = &ctx->csctx.processing_compute;
   1321 
   1322 	return result;
   1323 }
   1324 
   1325 function Variable *
   1326 add_compute_stats_view(BeamformerUI *ui, Variable *parent, Arena *arena, BeamformerCtx *ctx)
   1327 {
   1328 	/* TODO(rnp): this can be closable once we have a way of opening new views */
   1329 	Variable *result   = add_ui_view(ui, parent, arena, s8(""), UIViewFlag_CustomText, 0, 0);
   1330 	result->view.child = add_variable(ui, result, &ui->arena, s8(""), 0,
   1331 	                                  VT_COMPUTE_STATS_VIEW, ui->small_font);
   1332 
   1333 	Variable *menu = result->view.menu = add_variable_group(ui, 0, arena, s8(""),
   1334 	                                                        VariableGroupKind_List, ui->small_font);
   1335 	menu->parent = result;
   1336 
   1337 	#define X(_k, label) s8_comp(label),
   1338 	read_only local_persist s8 labels[] = {COMPUTE_STATS_VIEW_LIST};
   1339 	#undef X
   1340 
   1341 	ComputeStatsView *csv = &result->view.child->compute_stats_view;
   1342 	csv->compute_shader_stats = ctx->compute_shader_stats;
   1343 	csv->cycler = add_variable_cycler(ui, menu, arena, 0, ui->small_font, s8("Stats View:"),
   1344 	                                  &csv->kind, labels, countof(labels));
   1345 	add_global_menu_to_group(ui, arena, menu);
   1346 	return result;
   1347 }
   1348 
   1349 function Variable *
   1350 add_live_controls_view(BeamformerUI *ui, Variable *parent, Arena *arena)
   1351 {
   1352 	BeamformerSharedMemory *sm = ui->shared_memory.region;
   1353 	BeamformerLiveImagingParameters *lip = &sm->live_imaging_parameters;
   1354 	/* TODO(rnp): this can be closable once we have a way of opening new views */
   1355 	Variable *result = add_ui_view(ui, parent, &ui->arena, s8("Live Controls"), 0, 1, 0);
   1356 	result->view.child = add_variable(ui, result, &ui->arena, s8(""), 0,
   1357 	                                  VT_LIVE_CONTROLS_VIEW, ui->small_font);
   1358 	Variable *view = result->view.child;
   1359 	BeamformerLiveControlsView *lv = view->generic = push_struct(arena, typeof(*lv));
   1360 
   1361 	fill_variable(&lv->transmit_power, view, s8(""), V_INPUT|V_LIVE_CONTROL,
   1362 	              VT_BEAMFORMER_VARIABLE, ui->small_font);
   1363 	fill_beamformer_variable(&lv->transmit_power, s8(""), &lip->transmit_power, (v2){{0, 1}}, 100, 0.05);
   1364 
   1365 	for (u32 i = 0; i < countof(lv->tgc_control_points); i++) {
   1366 		Variable *v = lv->tgc_control_points + i;
   1367 		fill_variable(v, view, s8(""), V_INPUT|V_LIVE_CONTROL, VT_BEAMFORMER_VARIABLE, ui->small_font);
   1368 		fill_beamformer_variable(v, s8(""), lip->tgc_control_points + i, (v2){{0, 1}}, 0, 0.05);
   1369 	}
   1370 
   1371 	fill_variable(&lv->stop_button, view, s8("Stop Imaging"), V_INPUT|V_LIVE_CONTROL,
   1372 	              VT_B32, ui->small_font);
   1373 
   1374 	read_only local_persist s8 save_labels[] = {s8_comp("Save Data"), s8_comp("Saving...")};
   1375 	fill_variable(&lv->save_button, view, s8("Save Data"), V_INPUT|V_LIVE_CONTROL,
   1376 	              VT_CYCLER, ui->small_font);
   1377 	fill_variable_cycler(&lv->save_button, &lip->save_active, save_labels, countof(save_labels));
   1378 
   1379 	return result;
   1380 }
   1381 
   1382 function Variable *
   1383 ui_split_region(BeamformerUI *ui, Variable *region, Variable *split_side, RegionSplitDirection direction)
   1384 {
   1385 	Variable *result = add_ui_split(ui, region, &ui->arena, s8(""), 0.5, direction, ui->small_font);
   1386 	if (split_side == region->region_split.left) {
   1387 		region->region_split.left  = result;
   1388 	} else {
   1389 		region->region_split.right = result;
   1390 	}
   1391 	split_side->parent = result;
   1392 	result->region_split.left = split_side;
   1393 	return result;
   1394 }
   1395 
   1396 function void
   1397 ui_add_live_frame_view(BeamformerUI *ui, Variable *view, RegionSplitDirection direction,
   1398                        BeamformerFrameViewKind kind)
   1399 {
   1400 	Variable *region = view->parent;
   1401 	assert(region->type == VT_UI_REGION_SPLIT);
   1402 	assert(view->type   == VT_UI_VIEW);
   1403 	Variable *new_region = ui_split_region(ui, region, view, direction);
   1404 	new_region->region_split.right = add_beamformer_frame_view(ui, new_region, &ui->arena, kind, 1, 0);
   1405 }
   1406 
   1407 function void
   1408 ui_beamformer_frame_view_copy_frame(BeamformerUI *ui, BeamformerFrameView *new, BeamformerFrameView *old)
   1409 {
   1410 	assert(old->frame);
   1411 	new->frame = SLLPop(ui->frame_freelist);
   1412 	if (!new->frame) new->frame = push_struct(&ui->arena, typeof(*new->frame));
   1413 
   1414 	mem_copy(new->frame, old->frame, sizeof(*new->frame));
   1415 	new->frame->texture = 0;
   1416 	new->frame->next    = 0;
   1417 	alloc_beamform_frame(0, new->frame, old->frame->dim, s8("Frame Copy: "), ui->arena);
   1418 
   1419 	glCopyImageSubData(old->frame->texture, GL_TEXTURE_3D, 0, 0, 0, 0,
   1420 	                   new->frame->texture, GL_TEXTURE_3D, 0, 0, 0, 0,
   1421 	                   new->frame->dim.x, new->frame->dim.y, new->frame->dim.z);
   1422 	glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT);
   1423 	/* TODO(rnp): x vs y here */
   1424 	resize_frame_view(new, (uv2){.x = new->frame->dim.x, .y = new->frame->dim.z}, 1);
   1425 }
   1426 
   1427 function void
   1428 ui_copy_frame(BeamformerUI *ui, Variable *view, RegionSplitDirection direction)
   1429 {
   1430 	Variable *region = view->parent;
   1431 	assert(region->type == VT_UI_REGION_SPLIT);
   1432 	assert(view->type   == VT_UI_VIEW);
   1433 
   1434 	BeamformerFrameView *old = view->view.child->generic;
   1435 	/* TODO(rnp): hack; it would be better if this was unreachable with a 0 old->frame */
   1436 	if (!old->frame)
   1437 		return;
   1438 
   1439 	Variable *new_region = ui_split_region(ui, region, view, direction);
   1440 	new_region->region_split.right = add_beamformer_frame_view(ui, new_region, &ui->arena,
   1441 	                                                           BeamformerFrameViewKind_Copy, 1, old);
   1442 
   1443 	BeamformerFrameView *bv = new_region->region_split.right->view.child->generic;
   1444 	ui_beamformer_frame_view_copy_frame(ui, bv, old);
   1445 }
   1446 
   1447 function v3
   1448 beamformer_frame_view_plane_size(BeamformerUI *ui, BeamformerFrameView *view)
   1449 {
   1450 	v3 result;
   1451 	if (view->kind == BeamformerFrameViewKind_3DXPlane) {
   1452 		v3 min = v4_from_f32_array(ui->params.output_min_coordinate).xyz;
   1453 		v3 max = v4_from_f32_array(ui->params.output_max_coordinate).xyz;
   1454 		result = v3_sub(max, min);
   1455 		swap(result.y, result.z);
   1456 		result.x = MAX(1e-3, result.x);
   1457 		result.y = MAX(1e-3, result.y);
   1458 		result.z = MAX(1e-3, result.z);
   1459 	} else {
   1460 		v2 size = v2_sub(XZ(view->max_coordinate), XZ(view->min_coordinate));
   1461 		result  = (v3){.x = size.x, .y = size.y};
   1462 	}
   1463 	return result;
   1464 }
   1465 
   1466 function f32
   1467 x_plane_rotation_for_view_plane(BeamformerFrameView *view, BeamformerViewPlaneTag tag)
   1468 {
   1469 	f32 result = view->rotation;
   1470 	if (tag == BeamformerViewPlaneTag_YZ)
   1471 		result += 0.25f;
   1472 	return result;
   1473 }
   1474 
   1475 function v2
   1476 normalized_p_in_rect(Rect r, v2 p, b32 invert_y)
   1477 {
   1478 	v2 result = v2_div(v2_scale(v2_sub(p, r.pos), 2.0f), r.size);
   1479 	if (invert_y) result = (v2){{result.x - 1.0f, 1.0f - result.y}};
   1480 	else          result = v2_sub(result, (v2){{1.0f, 1.0f}});
   1481 	return result;
   1482 }
   1483 
   1484 function v3
   1485 x_plane_position(BeamformerUI *ui)
   1486 {
   1487 	f32 y_min = ui->params.output_min_coordinate[2];
   1488 	f32 y_max = ui->params.output_max_coordinate[2];
   1489 	v3 result = {.y = y_min + (y_max - y_min) / 2};
   1490 	return result;
   1491 }
   1492 
   1493 function v3
   1494 offset_x_plane_position(BeamformerUI *ui, BeamformerFrameView *view, BeamformerViewPlaneTag tag)
   1495 {
   1496 	BeamformerSharedMemory          *sm = ui->shared_memory.region;
   1497 	BeamformerLiveImagingParameters *li = &sm->live_imaging_parameters;
   1498 	m4 x_rotation = m4_rotation_about_y(x_plane_rotation_for_view_plane(view, tag));
   1499 	v3 Z = x_rotation.c[2].xyz;
   1500 	v3 offset = v3_scale(Z, li->image_plane_offsets[tag]);
   1501 	v3 result = v3_add(x_plane_position(ui), offset);
   1502 	return result;
   1503 }
   1504 
   1505 function v3
   1506 camera_for_x_plane_view(BeamformerUI *ui, BeamformerFrameView *view)
   1507 {
   1508 	v3 size   = beamformer_frame_view_plane_size(ui, view);
   1509 	v3 target = x_plane_position(ui);
   1510 	f32 dist  = v2_magnitude(XY(size));
   1511 	v3 result = v3_add(target, (v3){{dist, -0.5f * size.y * tan_f32(50.0f * PI / 180.0f), dist}});
   1512 	return result;
   1513 }
   1514 
   1515 function m4
   1516 view_matrix_for_x_plane_view(BeamformerUI *ui, BeamformerFrameView *view, v3 camera)
   1517 {
   1518 	assert(view->kind == BeamformerFrameViewKind_3DXPlane);
   1519 	m4 result = camera_look_at(camera, x_plane_position(ui));
   1520 	return result;
   1521 }
   1522 
   1523 function m4
   1524 projection_matrix_for_x_plane_view(BeamformerFrameView *view)
   1525 {
   1526 	assert(view->kind == BeamformerFrameViewKind_3DXPlane);
   1527 	f32 aspect = (f32)view->texture_dim.w / view->texture_dim.h;
   1528 	m4 result = perspective_projection(10e-3, 500e-3, 45.0f * PI / 180.0f, aspect);
   1529 	return result;
   1530 }
   1531 
   1532 function ray
   1533 ray_for_x_plane_view(BeamformerUI *ui, BeamformerFrameView *view, v2 uv)
   1534 {
   1535 	assert(view->kind == BeamformerFrameViewKind_3DXPlane);
   1536 	ray result  = {.origin = camera_for_x_plane_view(ui, view)};
   1537 	v4 ray_clip = {{uv.x, uv.y, -1.0f, 1.0f}};
   1538 
   1539 	/* TODO(rnp): combine these so we only do one matrix inversion */
   1540 	m4 proj_m   = projection_matrix_for_x_plane_view(view);
   1541 	m4 view_m   = view_matrix_for_x_plane_view(ui, view, result.origin);
   1542 	m4 proj_inv = m4_inverse(proj_m);
   1543 	m4 view_inv = m4_inverse(view_m);
   1544 
   1545 	v4 ray_eye  = {.z = -1};
   1546 	ray_eye.x   = v4_dot(m4_row(proj_inv, 0), ray_clip);
   1547 	ray_eye.y   = v4_dot(m4_row(proj_inv, 1), ray_clip);
   1548 	result.direction = v3_normalize(m4_mul_v4(view_inv, ray_eye).xyz);
   1549 
   1550 	return result;
   1551 }
   1552 
   1553 function BeamformerViewPlaneTag
   1554 view_plane_tag_from_x_plane_shift(BeamformerFrameView *view, Variable *x_plane_shift)
   1555 {
   1556 	assert(BETWEEN(x_plane_shift, view->x_plane_shifts + 0, view->x_plane_shifts + 1));
   1557 	BeamformerViewPlaneTag result = BeamformerViewPlaneTag_XZ;
   1558 	if (x_plane_shift == view->x_plane_shifts + 1)
   1559 		result = BeamformerViewPlaneTag_YZ;
   1560 	return result;
   1561 }
   1562 
   1563 function void
   1564 render_single_xplane(BeamformerUI *ui, BeamformerFrameView *view, Variable *x_plane_shift,
   1565                      u32 program, f32 rotation_turns, v3 translate, BeamformerViewPlaneTag tag)
   1566 {
   1567 	u32 texture = 0;
   1568 	if (ui->latest_plane[tag])
   1569 		texture = ui->latest_plane[tag]->texture;
   1570 
   1571 	v3 scale = beamformer_frame_view_plane_size(ui, view);
   1572 	m4 model_transform = y_aligned_volume_transform(scale, translate, rotation_turns);
   1573 
   1574 	v4 colour = v4_lerp(FG_COLOUR, HOVERED_COLOUR, x_plane_shift->hover_t);
   1575 	glProgramUniformMatrix4fv(program, FRAME_VIEW_MODEL_MATRIX_LOC, 1, 0, model_transform.E);
   1576 	glProgramUniform4fv(program, FRAME_VIEW_BB_COLOUR_LOC, 1, colour.E);
   1577 	glProgramUniform1ui(program, FRAME_VIEW_SOLID_BB_LOC, 0);
   1578 	glBindTextureUnit(0, texture);
   1579 	glDrawElements(GL_TRIANGLES, ui->unit_cube_model.elements, GL_UNSIGNED_SHORT,
   1580 	               (void *)ui->unit_cube_model.elements_offset);
   1581 
   1582 	XPlaneShift *xp = &x_plane_shift->x_plane_shift;
   1583 	v3 xp_delta = v3_sub(xp->end_point, xp->start_point);
   1584 	if (!f32_cmp(v3_magnitude(xp_delta), 0)) {
   1585 		m4 x_rotation = m4_rotation_about_y(rotation_turns);
   1586 		v3 Z = x_rotation.c[2].xyz;
   1587 		v3 f = v3_scale(Z, v3_dot(Z, v3_sub(xp->end_point, xp->start_point)));
   1588 
   1589 		/* TODO(rnp): there is no reason to compute the rotation matrix again */
   1590 		model_transform = y_aligned_volume_transform(scale, v3_add(f, translate), rotation_turns);
   1591 
   1592 		glProgramUniformMatrix4fv(program, FRAME_VIEW_MODEL_MATRIX_LOC, 1, 0, model_transform.E);
   1593 		glProgramUniform1ui(program, FRAME_VIEW_SOLID_BB_LOC, 1);
   1594 		glProgramUniform4fv(program, FRAME_VIEW_BB_COLOUR_LOC, 1, HOVERED_COLOUR.E);
   1595 		glDrawElements(GL_TRIANGLES, ui->unit_cube_model.elements, GL_UNSIGNED_SHORT,
   1596 		               (void *)ui->unit_cube_model.elements_offset);
   1597 	}
   1598 }
   1599 
   1600 function void
   1601 render_3D_xplane(BeamformerUI *ui, BeamformerFrameView *view, u32 program)
   1602 {
   1603 	if (view->demo->bool32) {
   1604 		view->rotation += dt_for_frame * 0.125f;
   1605 		if (view->rotation > 1.0f) view->rotation -= 1.0f;
   1606 	}
   1607 
   1608 	v3 camera     = camera_for_x_plane_view(ui, view);
   1609 	m4 view_m     = view_matrix_for_x_plane_view(ui, view, camera);
   1610 	m4 projection = projection_matrix_for_x_plane_view(view);
   1611 
   1612 	glProgramUniformMatrix4fv(program, FRAME_VIEW_VIEW_MATRIX_LOC,  1, 0, view_m.E);
   1613 	glProgramUniformMatrix4fv(program, FRAME_VIEW_PROJ_MATRIX_LOC,  1, 0, projection.E);
   1614 	glProgramUniform1f(program, FRAME_VIEW_BB_FRACTION_LOC, FRAME_VIEW_BB_FRACTION);
   1615 
   1616 	v3 model_translate = offset_x_plane_position(ui, view, BeamformerViewPlaneTag_XZ);
   1617 	render_single_xplane(ui, view, view->x_plane_shifts + 0, program,
   1618 	                     x_plane_rotation_for_view_plane(view, BeamformerViewPlaneTag_XZ),
   1619 	                     model_translate, BeamformerViewPlaneTag_XZ);
   1620 	model_translate = offset_x_plane_position(ui, view, BeamformerViewPlaneTag_YZ);
   1621 	model_translate.y -= 0.0001;
   1622 	render_single_xplane(ui, view, view->x_plane_shifts + 1, program,
   1623 	                     x_plane_rotation_for_view_plane(view, BeamformerViewPlaneTag_YZ),
   1624 	                     model_translate, BeamformerViewPlaneTag_YZ);
   1625 }
   1626 
   1627 function void
   1628 render_2D_plane(BeamformerUI *ui, BeamformerFrameView *view, u32 program)
   1629 {
   1630 	m4 view_m     = m4_identity();
   1631 	v3 size       = beamformer_frame_view_plane_size(ui, view);
   1632 	m4 model      = m4_scale(size);
   1633 	m4 projection = orthographic_projection(0, 1, size.y / 2, size.x / 2);
   1634 
   1635 	glProgramUniformMatrix4fv(program, FRAME_VIEW_MODEL_MATRIX_LOC, 1, 0, model.E);
   1636 	glProgramUniformMatrix4fv(program, FRAME_VIEW_VIEW_MATRIX_LOC,  1, 0, view_m.E);
   1637 	glProgramUniformMatrix4fv(program, FRAME_VIEW_PROJ_MATRIX_LOC,  1, 0, projection.E);
   1638 
   1639 	glProgramUniform1f(program, FRAME_VIEW_BB_FRACTION_LOC, 0);
   1640 	glBindTextureUnit(0, view->frame->texture);
   1641 	glDrawElements(GL_TRIANGLES, ui->unit_cube_model.elements, GL_UNSIGNED_SHORT,
   1642 	               (void *)ui->unit_cube_model.elements_offset);
   1643 }
   1644 
   1645 function b32
   1646 frame_view_ready_to_present(BeamformerUI *ui, BeamformerFrameView *view)
   1647 {
   1648 	b32 result  = !uv2_equal((uv2){0}, view->texture_dim) && view->frame;
   1649 	result     |= view->kind == BeamformerFrameViewKind_3DXPlane &&
   1650 	              ui->latest_plane[BeamformerViewPlaneTag_Count];
   1651 	return result;
   1652 }
   1653 
   1654 function b32
   1655 view_update(BeamformerUI *ui, BeamformerFrameView *view)
   1656 {
   1657 	if (view->kind == BeamformerFrameViewKind_Latest) {
   1658 		u32 index = *view->cycler->cycler.state;
   1659 		view->dirty |= view->frame != ui->latest_plane[index];
   1660 		view->frame  = ui->latest_plane[index];
   1661 		if (view->dirty) {
   1662 			view->min_coordinate = v4_from_f32_array(ui->params.output_min_coordinate).xyz;
   1663 			view->max_coordinate = v4_from_f32_array(ui->params.output_max_coordinate).xyz;
   1664 		}
   1665 	}
   1666 
   1667 	/* TODO(rnp): x-z or y-z */
   1668 	/* TODO(rnp): add method of setting a target size in frame view */
   1669 	uv2 current = view->texture_dim;
   1670 	uv2 target  = {.w = ui->params.output_points[0], .h = ui->params.output_points[2]};
   1671 	if (view->kind != BeamformerFrameViewKind_Copy &&
   1672 	    view->kind != BeamformerFrameViewKind_3DXPlane &&
   1673 	    !uv2_equal(current, target) && !uv2_equal(target, (uv2){0}))
   1674 	{
   1675 		resize_frame_view(view, target, 1);
   1676 		view->dirty = 1;
   1677 	}
   1678 	view->dirty |= ui->frame_view_render_context->updated;
   1679 	view->dirty |= view->kind == BeamformerFrameViewKind_3DXPlane;
   1680 
   1681 	b32 result = frame_view_ready_to_present(ui, view) && view->dirty;
   1682 	return result;
   1683 }
   1684 
   1685 function void
   1686 update_frame_views(BeamformerUI *ui, Rect window)
   1687 {
   1688 	FrameViewRenderContext *ctx = ui->frame_view_render_context;
   1689 	b32 fbo_bound = 0;
   1690 	for (BeamformerFrameView *view = ui->views; view; view = view->next) {
   1691 		if (view_update(ui, view)) {
   1692 			if (!fbo_bound) {
   1693 				fbo_bound = 1;
   1694 				glBindFramebuffer(GL_FRAMEBUFFER, ctx->framebuffers[0]);
   1695 				glUseProgram(ctx->shader);
   1696 				glBindVertexArray(ui->unit_cube_model.vao);
   1697 				glEnable(GL_DEPTH_TEST);
   1698 			}
   1699 
   1700 			u32 fb      = ctx->framebuffers[0];
   1701 			u32 program = ctx->shader;
   1702 			glViewport(0, 0, view->texture_dim.w, view->texture_dim.h);
   1703 			glProgramUniform1f(program,  FRAME_VIEW_THRESHOLD_LOC,     view->threshold.real32);
   1704 			glProgramUniform1f(program,  FRAME_VIEW_DYNAMIC_RANGE_LOC, view->dynamic_range.real32);
   1705 			glProgramUniform1f(program,  FRAME_VIEW_GAMMA_LOC,         view->gamma.scaled_real32.val);
   1706 			glProgramUniform1ui(program, FRAME_VIEW_LOG_SCALE_LOC,     view->log_scale->bool32);
   1707 
   1708 			if (view->kind == BeamformerFrameViewKind_3DXPlane) {
   1709 				glNamedFramebufferRenderbuffer(fb, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, ctx->renderbuffers[0]);
   1710 				glNamedFramebufferRenderbuffer(fb, GL_DEPTH_ATTACHMENT,  GL_RENDERBUFFER, ctx->renderbuffers[1]);
   1711 				glClearNamedFramebufferfv(fb, GL_COLOR, 0, (f32 []){0, 0, 0, 0});
   1712 				glClearNamedFramebufferfv(fb, GL_DEPTH, 0, (f32 []){1});
   1713 				render_3D_xplane(ui, view, program);
   1714 				/* NOTE(rnp): resolve multisampled scene */
   1715 				glNamedFramebufferTexture(ctx->framebuffers[1], GL_COLOR_ATTACHMENT0, view->textures[0], 0);
   1716 				glBlitNamedFramebuffer(fb, ctx->framebuffers[1], 0, 0, FRAME_VIEW_RENDER_TARGET_SIZE,
   1717 				                       0, 0, FRAME_VIEW_RENDER_TARGET_SIZE, GL_COLOR_BUFFER_BIT, GL_NEAREST);
   1718 			} else {
   1719 				glNamedFramebufferTexture(fb, GL_COLOR_ATTACHMENT0, view->textures[0], 0);
   1720 				glNamedFramebufferTexture(fb, GL_DEPTH_ATTACHMENT,  view->textures[1], 0);
   1721 				glClearNamedFramebufferfv(fb, GL_COLOR, 0, (f32 []){0, 0, 0, 0});
   1722 				glClearNamedFramebufferfv(fb, GL_DEPTH, 0, (f32 []){1});
   1723 				render_2D_plane(ui, view, program);
   1724 			}
   1725 			glGenerateTextureMipmap(view->textures[0]);
   1726 			view->dirty = 0;
   1727 		}
   1728 	}
   1729 	if (fbo_bound) {
   1730 		glBindFramebuffer(GL_FRAMEBUFFER, 0);
   1731 		glViewport(window.pos.x, window.pos.y, window.size.w, window.size.h);
   1732 		/* NOTE(rnp): I don't trust raylib to not mess with us */
   1733 		glBindVertexArray(0);
   1734 		glDisable(GL_DEPTH_TEST);
   1735 	}
   1736 }
   1737 
   1738 function Color
   1739 colour_from_normalized(v4 rgba)
   1740 {
   1741 	return (Color){.r = rgba.r * 255.0f, .g = rgba.g * 255.0f,
   1742 	               .b = rgba.b * 255.0f, .a = rgba.a * 255.0f};
   1743 }
   1744 
   1745 function Color
   1746 fade(Color a, f32 visibility)
   1747 {
   1748 	a.a = (u8)((f32)a.a * visibility);
   1749 	return a;
   1750 }
   1751 
   1752 function v2
   1753 draw_text_base(Font font, s8 text, v2 pos, Color colour)
   1754 {
   1755 	v2 off = v2_floor(pos);
   1756 	for (iz i = 0; i < text.len; i++) {
   1757 		/* NOTE: assumes font glyphs are ordered ASCII */
   1758 		i32 idx = text.data[i] - 0x20;
   1759 		Rectangle dst = {
   1760 			off.x + font.glyphs[idx].offsetX - font.glyphPadding,
   1761 			off.y + font.glyphs[idx].offsetY - font.glyphPadding,
   1762 			font.recs[idx].width  + 2.0f * font.glyphPadding,
   1763 			font.recs[idx].height + 2.0f * font.glyphPadding
   1764 		};
   1765 		Rectangle src = {
   1766 			font.recs[idx].x - font.glyphPadding,
   1767 			font.recs[idx].y - font.glyphPadding,
   1768 			font.recs[idx].width  + 2.0f * font.glyphPadding,
   1769 			font.recs[idx].height + 2.0f * font.glyphPadding
   1770 		};
   1771 		DrawTexturePro(font.texture, src, dst, (Vector2){0}, 0, colour);
   1772 
   1773 		off.x += font.glyphs[idx].advanceX;
   1774 		if (font.glyphs[idx].advanceX == 0)
   1775 			off.x += font.recs[idx].width;
   1776 	}
   1777 	v2 result = {.x = off.x - pos.x, .y = font.baseSize};
   1778 	return result;
   1779 }
   1780 
   1781 /* NOTE(rnp): expensive but of the available options in raylib this gives the best results */
   1782 function v2
   1783 draw_outlined_text(s8 text, v2 pos, TextSpec *ts)
   1784 {
   1785 	f32 ow = ts->outline_thick;
   1786 	Color outline = colour_from_normalized(ts->outline_colour);
   1787 	Color colour  = colour_from_normalized(ts->colour);
   1788 	draw_text_base(*ts->font, text, v2_sub(pos, (v2){{ ow,  ow}}), outline);
   1789 	draw_text_base(*ts->font, text, v2_sub(pos, (v2){{ ow, -ow}}), outline);
   1790 	draw_text_base(*ts->font, text, v2_sub(pos, (v2){{-ow,  ow}}), outline);
   1791 	draw_text_base(*ts->font, text, v2_sub(pos, (v2){{-ow, -ow}}), outline);
   1792 
   1793 	v2 result = draw_text_base(*ts->font, text, pos, colour);
   1794 
   1795 	return result;
   1796 }
   1797 
   1798 function v2
   1799 draw_text(s8 text, v2 pos, TextSpec *ts)
   1800 {
   1801 	if (ts->flags & TF_ROTATED) {
   1802 		rlPushMatrix();
   1803 		rlTranslatef(pos.x, pos.y, 0);
   1804 		rlRotatef(ts->rotation, 0, 0, 1);
   1805 		pos = (v2){0};
   1806 	}
   1807 
   1808 	v2 result   = measure_text(*ts->font, text);
   1809 	/* TODO(rnp): the size of this should be stored for each font */
   1810 	s8 ellipsis = s8("...");
   1811 	b32 clamped = ts->flags & TF_LIMITED && result.w > ts->limits.size.w;
   1812 	if (clamped) {
   1813 		f32 ellipsis_width = measure_text(*ts->font, ellipsis).x;
   1814 		if (ellipsis_width < ts->limits.size.w) {
   1815 			text = clamp_text_to_width(*ts->font, text, ts->limits.size.w - ellipsis_width);
   1816 		} else {
   1817 			text.len     = 0;
   1818 			ellipsis.len = 0;
   1819 		}
   1820 	}
   1821 
   1822 	Color colour = colour_from_normalized(ts->colour);
   1823 	if (ts->flags & TF_OUTLINED) result.x = draw_outlined_text(text, pos, ts).x;
   1824 	else                         result.x = draw_text_base(*ts->font, text, pos, colour).x;
   1825 
   1826 	if (clamped) {
   1827 		pos.x += result.x;
   1828 		if (ts->flags & TF_OUTLINED) result.x += draw_outlined_text(ellipsis, pos, ts).x;
   1829 		else                         result.x += draw_text_base(*ts->font, ellipsis, pos,
   1830 		                                                        colour).x;
   1831 	}
   1832 
   1833 	if (ts->flags & TF_ROTATED) rlPopMatrix();
   1834 
   1835 	return result;
   1836 }
   1837 
   1838 function Rect
   1839 extend_rect_centered(Rect r, v2 delta)
   1840 {
   1841 	r.size.w += delta.x;
   1842 	r.size.h += delta.y;
   1843 	r.pos.x  -= delta.x / 2;
   1844 	r.pos.y  -= delta.y / 2;
   1845 	return r;
   1846 }
   1847 
   1848 function Rect
   1849 shrink_rect_centered(Rect r, v2 delta)
   1850 {
   1851 	delta.x   = MIN(delta.x, r.size.w);
   1852 	delta.y   = MIN(delta.y, r.size.h);
   1853 	r.size.w -= delta.x;
   1854 	r.size.h -= delta.y;
   1855 	r.pos.x  += delta.x / 2;
   1856 	r.pos.y  += delta.y / 2;
   1857 	return r;
   1858 }
   1859 
   1860 function Rect
   1861 scale_rect_centered(Rect r, v2 scale)
   1862 {
   1863 	Rect or   = r;
   1864 	r.size.w *= scale.x;
   1865 	r.size.h *= scale.y;
   1866 	r.pos.x  += (or.size.w - r.size.w) / 2;
   1867 	r.pos.y  += (or.size.h - r.size.h) / 2;
   1868 	return r;
   1869 }
   1870 
   1871 function b32
   1872 interactions_equal(Interaction a, Interaction b)
   1873 {
   1874 	b32 result = (a.kind == b.kind) && (a.generic == b.generic);
   1875 	return result;
   1876 }
   1877 
   1878 function b32
   1879 interaction_is_sticky(Interaction a)
   1880 {
   1881 	b32 result = a.kind == InteractionKind_Text || a.kind == InteractionKind_Ruler;
   1882 	return result;
   1883 }
   1884 
   1885 function b32
   1886 interaction_is_hot(BeamformerUI *ui, Interaction a)
   1887 {
   1888 	b32 result = interactions_equal(ui->hot_interaction, a);
   1889 	return result;
   1890 }
   1891 
   1892 function b32
   1893 point_in_rect(v2 p, Rect r)
   1894 {
   1895 	v2  end    = v2_add(r.pos, r.size);
   1896 	b32 result = BETWEEN(p.x, r.pos.x, end.x) & BETWEEN(p.y, r.pos.y, end.y);
   1897 	return result;
   1898 }
   1899 
   1900 function v2
   1901 screen_point_to_world_2d(v2 p, v2 screen_min, v2 screen_max, v2 world_min, v2 world_max)
   1902 {
   1903 	v2 pixels_to_m = v2_div(v2_sub(world_max, world_min), v2_sub(screen_max, screen_min));
   1904 	v2 result      = v2_add(v2_mul(v2_sub(p, screen_min), pixels_to_m), world_min);
   1905 	return result;
   1906 }
   1907 
   1908 function v2
   1909 world_point_to_screen_2d(v2 p, v2 world_min, v2 world_max, v2 screen_min, v2 screen_max)
   1910 {
   1911 	v2 m_to_pixels = v2_div(v2_sub(screen_max, screen_min), v2_sub(world_max, world_min));
   1912 	v2 result      = v2_add(v2_mul(v2_sub(p, world_min), m_to_pixels), screen_min);
   1913 	return result;
   1914 }
   1915 
   1916 function b32
   1917 hover_interaction(BeamformerUI *ui, v2 mouse, Interaction interaction)
   1918 {
   1919 	Variable *var = interaction.var;
   1920 	b32 result = point_in_rect(mouse, interaction.rect);
   1921 	if (result) ui->next_interaction = interaction;
   1922 	if (interaction_is_hot(ui, interaction)) var->hover_t += HOVER_SPEED * dt_for_frame;
   1923 	else                                     var->hover_t -= HOVER_SPEED * dt_for_frame;
   1924 	var->hover_t = CLAMP01(var->hover_t);
   1925 	return result;
   1926 }
   1927 
   1928 function void
   1929 draw_close_button(BeamformerUI *ui, Variable *close, v2 mouse, Rect r, v2 x_scale)
   1930 {
   1931 	assert(close->type == VT_UI_BUTTON);
   1932 	hover_interaction(ui, mouse, auto_interaction(r, close));
   1933 
   1934 	Color colour = colour_from_normalized(v4_lerp(MENU_CLOSE_COLOUR, FG_COLOUR, close->hover_t));
   1935 	r = scale_rect_centered(r, x_scale);
   1936 	DrawLineEx(r.pos.rl, v2_add(r.pos, r.size).rl, 4, colour);
   1937 	DrawLineEx(v2_add(r.pos, (v2){.x = r.size.w}).rl,
   1938 	           v2_add(r.pos, (v2){.y = r.size.h}).rl, 4, colour);
   1939 }
   1940 
   1941 function Rect
   1942 draw_title_bar(BeamformerUI *ui, Arena arena, Variable *ui_view, Rect r, v2 mouse)
   1943 {
   1944 	assert(ui_view->type == VT_UI_VIEW);
   1945 	UIView *view = &ui_view->view;
   1946 
   1947 	s8 title = ui_view->name;
   1948 	if (view->flags & UIViewFlag_CustomText) {
   1949 		Stream buf = arena_stream(arena);
   1950 		push_custom_view_title(&buf, ui_view->view.child);
   1951 		title = arena_stream_commit(&arena, &buf);
   1952 	}
   1953 
   1954 	Rect result, title_rect;
   1955 	cut_rect_vertical(r, ui->small_font.baseSize + TITLE_BAR_PAD, &title_rect, &result);
   1956 	cut_rect_vertical(result, LISTING_LINE_PAD, 0, &result);
   1957 
   1958 	DrawRectangleRec(title_rect.rl, BLACK);
   1959 
   1960 	title_rect = shrink_rect_centered(title_rect, (v2){.x = 1.5 * TITLE_BAR_PAD});
   1961 	DrawRectangleRounded(title_rect.rl, 0.5, 0, fade(colour_from_normalized(BG_COLOUR), 0.55));
   1962 	title_rect = shrink_rect_centered(title_rect, (v2){.x = 3 * TITLE_BAR_PAD});
   1963 
   1964 	if (view->close) {
   1965 		Rect close;
   1966 		cut_rect_horizontal(title_rect, title_rect.size.w - title_rect.size.h, &title_rect, &close);
   1967 		draw_close_button(ui, view->close, mouse, close, (v2){{.4, .4}});
   1968 	}
   1969 
   1970 	if (view->menu) {
   1971 		Rect menu;
   1972 		cut_rect_horizontal(title_rect, title_rect.size.w - title_rect.size.h, &title_rect, &menu);
   1973 		Interaction interaction = {.kind = InteractionKind_Menu, .var = view->menu, .rect = menu};
   1974 		hover_interaction(ui, mouse, interaction);
   1975 
   1976 		Color colour = colour_from_normalized(v4_lerp(MENU_PLUS_COLOUR, FG_COLOUR, view->menu->hover_t));
   1977 		menu = shrink_rect_centered(menu, (v2){.x = 14, .y = 14});
   1978 		DrawLineEx(v2_add(menu.pos, (v2){.x = menu.size.w / 2}).rl,
   1979 		           v2_add(menu.pos, (v2){.x = menu.size.w / 2, .y = menu.size.h}).rl, 4, colour);
   1980 		DrawLineEx(v2_add(menu.pos, (v2){.y = menu.size.h / 2}).rl,
   1981 		           v2_add(menu.pos, (v2){.x = menu.size.w, .y = menu.size.h / 2}).rl, 4, colour);
   1982 	}
   1983 
   1984 	v2 title_pos = title_rect.pos;
   1985 	title_pos.y += 0.5 * TITLE_BAR_PAD;
   1986 	TextSpec text_spec = {.font = &ui->small_font, .flags = TF_LIMITED, .colour = FG_COLOUR,
   1987 	                      .limits.size = title_rect.size};
   1988 	draw_text(title, title_pos, &text_spec);
   1989 
   1990 	return result;
   1991 }
   1992 
   1993 /* TODO(rnp): once this has more callers decide if it would be better for this to take
   1994  * an orientation rather than force CCW/right-handed */
   1995 function void
   1996 draw_ruler(BeamformerUI *ui, Arena arena, v2 start_point, v2 end_point,
   1997            f32 start_value, f32 end_value, f32 *markers, u32 marker_count,
   1998            u32 segments, s8 suffix, v4 marker_colour, v4 txt_colour)
   1999 {
   2000 	b32 draw_plus = SIGN(start_value) != SIGN(end_value);
   2001 
   2002 	end_point    = v2_sub(end_point, start_point);
   2003 	f32 rotation = atan2_f32(end_point.y, end_point.x) * 180 / PI;
   2004 
   2005 	rlPushMatrix();
   2006 	rlTranslatef(start_point.x, start_point.y, 0);
   2007 	rlRotatef(rotation, 0, 0, 1);
   2008 
   2009 	f32 inc       = v2_magnitude(end_point) / segments;
   2010 	f32 value_inc = (end_value - start_value) / segments;
   2011 	f32 value     = start_value;
   2012 
   2013 	Stream buf = arena_stream(arena);
   2014 	v2 sp = {0}, ep = {.y = RULER_TICK_LENGTH};
   2015 	v2 tp = {.x = ui->small_font.baseSize / 2, .y = ep.y + RULER_TEXT_PAD};
   2016 	TextSpec text_spec = {.font = &ui->small_font, .rotation = 90, .colour = txt_colour, .flags = TF_ROTATED};
   2017 	Color rl_txt_colour = colour_from_normalized(txt_colour);
   2018 	for (u32 j = 0; j <= segments; j++) {
   2019 		DrawLineEx(sp.rl, ep.rl, 3, rl_txt_colour);
   2020 
   2021 		stream_reset(&buf, 0);
   2022 		if (draw_plus && value > 0) stream_append_byte(&buf, '+');
   2023 		stream_append_f64(&buf, value, 10);
   2024 		stream_append_s8(&buf, suffix);
   2025 		draw_text(stream_to_s8(&buf), tp, &text_spec);
   2026 
   2027 		value += value_inc;
   2028 		sp.x  += inc;
   2029 		ep.x  += inc;
   2030 		tp.x  += inc;
   2031 	}
   2032 
   2033 	Color rl_marker_colour = colour_from_normalized(marker_colour);
   2034 	ep.y += RULER_TICK_LENGTH;
   2035 	for (u32 i = 0; i < marker_count; i++) {
   2036 		if (markers[i] < F32_INFINITY) {
   2037 			ep.x  = sp.x = markers[i];
   2038 			DrawLineEx(sp.rl, ep.rl, 3, rl_marker_colour);
   2039 			DrawCircleV(ep.rl, 3, rl_marker_colour);
   2040 		}
   2041 	}
   2042 
   2043 	rlPopMatrix();
   2044 }
   2045 
   2046 function void
   2047 do_scale_bar(BeamformerUI *ui, Arena arena, Variable *scale_bar, v2 mouse, Rect draw_rect,
   2048              f32 start_value, f32 end_value, s8 suffix)
   2049 {
   2050 	assert(scale_bar->type == VT_SCALE_BAR);
   2051 	ScaleBar *sb = &scale_bar->scale_bar;
   2052 
   2053 	v2 txt_s = measure_text(ui->small_font, s8("-288.8 mm"));
   2054 
   2055 	Rect tick_rect = draw_rect;
   2056 	v2   start_pos = tick_rect.pos;
   2057 	v2   end_pos   = tick_rect.pos;
   2058 	v2   relative_mouse = v2_sub(mouse, tick_rect.pos);
   2059 
   2060 	f32  markers[2];
   2061 	u32  marker_count = 1;
   2062 
   2063 	v2 world_zoom_point  = {{sb->zoom_starting_coord, sb->zoom_starting_coord}};
   2064 	v2 screen_zoom_point = world_point_to_screen_2d(world_zoom_point,
   2065 	                                                (v2){{*sb->min_value, *sb->min_value}},
   2066 	                                                (v2){{*sb->max_value, *sb->max_value}},
   2067 	                                                (v2){0}, tick_rect.size);
   2068 	u32  tick_count;
   2069 	if (sb->direction == SB_AXIAL) {
   2070 		tick_rect.size.x  = RULER_TEXT_PAD + RULER_TICK_LENGTH + txt_s.x;
   2071 		tick_count        = tick_rect.size.y / (1.5 * ui->small_font.baseSize);
   2072 		start_pos.y      += tick_rect.size.y;
   2073 		markers[0]        = tick_rect.size.y - screen_zoom_point.y;
   2074 		markers[1]        = tick_rect.size.y - relative_mouse.y;
   2075 	} else {
   2076 		tick_rect.size.y  = RULER_TEXT_PAD + RULER_TICK_LENGTH + txt_s.x;
   2077 		tick_count        = tick_rect.size.x / (1.5 * ui->small_font.baseSize);
   2078 		end_pos.x        += tick_rect.size.x;
   2079 		markers[0]        = screen_zoom_point.x;
   2080 		markers[1]        = relative_mouse.x;
   2081 	}
   2082 
   2083 	if (hover_interaction(ui, mouse, auto_interaction(tick_rect, scale_bar)))
   2084 		marker_count = 2;
   2085 
   2086 	draw_ruler(ui, arena, start_pos, end_pos, start_value, end_value, markers, marker_count,
   2087 	           tick_count, suffix, RULER_COLOUR, v4_lerp(FG_COLOUR, HOVERED_COLOUR, scale_bar->hover_t));
   2088 }
   2089 
   2090 function v2
   2091 draw_radio_button(BeamformerUI *ui, Variable *var, v2 at, v2 mouse, v4 base_colour, f32 size)
   2092 {
   2093 	assert(var->type == VT_B32);
   2094 	b32 value = var->bool32;
   2095 
   2096 	v2 result = (v2){.x = size, .y = size};
   2097 	Rect hover_rect   = {.pos = at, .size = result};
   2098 	hover_rect.pos.y += 1;
   2099 	hover_interaction(ui, mouse, auto_interaction(hover_rect, var));
   2100 
   2101 	hover_rect = shrink_rect_centered(hover_rect, (v2){.x = 8, .y = 8});
   2102 	Rect inner = shrink_rect_centered(hover_rect, (v2){.x = 4, .y = 4});
   2103 	v4 fill = v4_lerp(value? base_colour : (v4){0}, HOVERED_COLOUR, var->hover_t);
   2104 	DrawRectangleRoundedLinesEx(hover_rect.rl, 0.2, 0, 2, colour_from_normalized(base_colour));
   2105 	DrawRectangleRec(inner.rl, colour_from_normalized(fill));
   2106 
   2107 	return result;
   2108 }
   2109 
   2110 function f32
   2111 draw_variable_slider(BeamformerUI *ui, Variable *var, Rect r, f32 fill, v4 fill_colour, v2 mouse)
   2112 {
   2113 	f32  border_thick    = 3;
   2114 	f32  bar_height_frac = 0.8;
   2115 	v2   bar_size        = {{6, bar_height_frac * r.size.y}};
   2116 
   2117 	Rect inner  = shrink_rect_centered(r, (v2){{2 * border_thick, // NOTE(rnp): raylib jank
   2118 	                                            MAX(0, 2 * (r.size.y - bar_size.y))}});
   2119 	Rect filled = inner;
   2120 	filled.size.w *= fill;
   2121 
   2122 	Rect bar;
   2123 	bar.pos  = v2_add(r.pos, (v2){{fill * (r.size.w - bar_size.w), (1 - bar_height_frac) * 0.5 * r.size.y}});
   2124 	bar.size = bar_size;
   2125 	v4 bar_colour = v4_lerp(FG_COLOUR, FOCUSED_COLOUR, var->hover_t);
   2126 
   2127 	hover_interaction(ui, mouse, auto_interaction(inner, var));
   2128 
   2129 	DrawRectangleRec(filled.rl, colour_from_normalized(fill_colour));
   2130 	DrawRectangleRoundedLinesEx(inner.rl, 0.2, 0, border_thick, BLACK);
   2131 	DrawRectangleRounded(bar.rl, 0.6, 1, colour_from_normalized(bar_colour));
   2132 
   2133 	return r.size.y;
   2134 }
   2135 
   2136 function v2
   2137 draw_fancy_button(BeamformerUI *ui, Variable *var, s8 label, Rect r, v4 border_colour, v2 mouse, TextSpec ts)
   2138 {
   2139 	assert(ts.font->baseSize <= r.size.h * 0.8);
   2140 	f32 pad    = 0.1 * r.size.h;
   2141 
   2142 	v2   shadow_off   = {{2.5, 3}};
   2143 	f32  border_thick = 3;
   2144 	v2   border_size  = v2_add((v2){{pad + 2 * border_thick, pad}}, shadow_off);
   2145 
   2146 	Rect border = shrink_rect_centered(r,      border_size);
   2147 	Rect inner  = shrink_rect_centered(border, (v2){{pad, pad}});
   2148 
   2149 	ts.limits.size = inner.size;
   2150 	hover_interaction(ui, mouse, auto_interaction(inner, var));
   2151 
   2152 	border.pos = v2_add(border.pos, shadow_off);
   2153 
   2154 	DrawRectangleRoundedLinesEx(border.rl, 0.6, 0, border_thick, fade(BLACK, 0.8));
   2155 	border.pos = v2_sub(border.pos, shadow_off);
   2156 	DrawRectangleRounded(border.rl, 0.6, 1, colour_from_normalized(BG_COLOUR));
   2157 	DrawRectangleRoundedLinesEx(border.rl, 0.6, 0, border_thick, colour_from_normalized(border_colour));
   2158 
   2159 	/* TODO(rnp): teach draw_text() about alignment */
   2160 	v2 at = align_text_in_rect(label, inner, *ts.font);
   2161 	at = v2_add(at, (v2){{3, 3}});
   2162 	v4 base_colour = ts.colour;
   2163 	ts.colour = (v4){{0, 0, 0, 0.8}};
   2164 	draw_text(label, at, &ts);
   2165 
   2166 	at = v2_sub(at, (v2){{3, 3}});
   2167 	ts.colour = v4_lerp(base_colour, HOVERED_COLOUR, var->hover_t);
   2168 	draw_text(label, at, &ts);
   2169 
   2170 	v2 result = v2_add(r.size, border_size);
   2171 	return result;
   2172 }
   2173 
   2174 function v2
   2175 draw_variable(BeamformerUI *ui, Arena arena, Variable *var, v2 at, v2 mouse, v4 base_colour, TextSpec text_spec)
   2176 {
   2177 	v2 result;
   2178 	if (var->flags & V_RADIO_BUTTON) {
   2179 		result = draw_radio_button(ui, var, at, mouse, base_colour, text_spec.font->baseSize);
   2180 	} else {
   2181 		Stream buf = arena_stream(arena);
   2182 		stream_append_variable(&buf, var);
   2183 		s8 text = arena_stream_commit(&arena, &buf);
   2184 		result = measure_text(*text_spec.font, text);
   2185 
   2186 		if (var->flags & V_INPUT) {
   2187 			Rect text_rect = {.pos = at, .size = result};
   2188 			text_rect = extend_rect_centered(text_rect, (v2){.x = 8});
   2189 			if (hover_interaction(ui, mouse, auto_interaction(text_rect, var)) && (var->flags & V_TEXT))
   2190 				ui->text_input_state.hot_font = text_spec.font;
   2191 			text_spec.colour = v4_lerp(base_colour, HOVERED_COLOUR, var->hover_t);
   2192 		}
   2193 
   2194 		draw_text(text, at, &text_spec);
   2195 	}
   2196 	return result;
   2197 }
   2198 
   2199 function void
   2200 draw_table_cell(BeamformerUI *ui, Arena arena, TableCell *cell, Rect cell_rect,
   2201                 TextAlignment alignment, TextSpec ts, v2 mouse)
   2202 {
   2203 	f32 x_off  = cell_rect.pos.x;
   2204 	v2 cell_at = table_cell_align(cell, alignment, cell_rect);
   2205 	ts.limits.size.w -= (cell_at.x - x_off);
   2206 	cell_rect.size.w  = MIN(ts.limits.size.w, cell_rect.size.w);
   2207 
   2208 	/* TODO(rnp): push truncated text for hovering */
   2209 	switch (cell->kind) {
   2210 	case TableCellKind_None:{ draw_text(cell->text, cell_at, &ts); }break;
   2211 	case TableCellKind_Variable:{
   2212 		if (cell->var->flags & V_INPUT) {
   2213 			draw_variable(ui, arena, cell->var, cell_at, mouse, ts.colour, ts);
   2214 		} else if (cell->text.len) {
   2215 			draw_text(cell->text, cell_at, &ts);
   2216 		}
   2217 	}break;
   2218 	case TableCellKind_VariableGroup:{
   2219 		Variable *v = cell->var->group.first;
   2220 		f32 dw = draw_text(s8("{"), cell_at, &ts).x;
   2221 		while (v) {
   2222 			cell_at.x        += dw;
   2223 			ts.limits.size.w -= dw;
   2224 			dw = draw_variable(ui, arena, v, cell_at, mouse, ts.colour, ts).x;
   2225 
   2226 			v = v->next;
   2227 			if (v) {
   2228 				cell_at.x        += dw;
   2229 				ts.limits.size.w -= dw;
   2230 				dw = draw_text(s8(", "), cell_at, &ts).x;
   2231 			}
   2232 		}
   2233 		cell_at.x        += dw;
   2234 		ts.limits.size.w -= dw;
   2235 		draw_text(s8("}"), cell_at, &ts);
   2236 	}break;
   2237 	}
   2238 }
   2239 
   2240 function void
   2241 draw_table_borders(Table *t, Rect r, f32 line_height)
   2242 {
   2243 	if (t->column_border_thick > 0) {
   2244 		v2 start  = {.x = r.pos.x, .y = r.pos.y + t->cell_pad.h / 2};
   2245 		v2 end    = start;
   2246 		end.y    += t->size.y - t->cell_pad.y;
   2247 		for (i32 i = 0; i < t->columns - 1; i++) {
   2248 			f32 dx = t->widths[i] + t->cell_pad.w + t->column_border_thick;
   2249 			start.x += dx;
   2250 			end.x   += dx;
   2251 			if (t->widths[i + 1] > 0)
   2252 				DrawLineEx(start.rl, end.rl, t->column_border_thick, fade(BLACK, 0.8));
   2253 		}
   2254 	}
   2255 
   2256 	if (t->row_border_thick > 0) {
   2257 		v2 start  = {.x = r.pos.x + t->cell_pad.w / 2, .y = r.pos.y};
   2258 		v2 end    = start;
   2259 		end.x    += t->size.x - t->cell_pad.x;
   2260 		for (i32 i = 0; i < t->rows - 1; i++) {
   2261 			f32 dy   = line_height + t->cell_pad.y + t->row_border_thick;
   2262 			start.y += dy;
   2263 			end.y   += dy;
   2264 			DrawLineEx(start.rl, end.rl, t->row_border_thick, fade(BLACK, 0.8));
   2265 		}
   2266 	}
   2267 }
   2268 
   2269 function v2
   2270 draw_table(BeamformerUI *ui, Arena arena, Table *table, Rect draw_rect, TextSpec ts, v2 mouse, b32 skip_rows)
   2271 {
   2272 	ts.flags |= TF_LIMITED;
   2273 
   2274 	v2 result         = {.x = table_width(table)};
   2275 	i32 row_index     = skip_rows? table_skip_rows(table, draw_rect.size.h, ts.font->baseSize) : 0;
   2276 	TableIterator *it = table_iterator_new(table, TIK_CELLS, &arena, row_index, draw_rect.pos, ts.font);
   2277 	for (TableCell *cell = table_iterator_next(it, &arena);
   2278 	     cell;
   2279 	     cell = table_iterator_next(it, &arena))
   2280 	{
   2281 		ts.limits.size.w = draw_rect.size.w - (it->cell_rect.pos.x - it->start_x);
   2282 		draw_table_cell(ui, arena, cell, it->cell_rect, it->alignment, ts, mouse);
   2283 	}
   2284 	draw_table_borders(table, draw_rect, ts.font->baseSize);
   2285 	result.y = it->cell_rect.pos.y - draw_rect.pos.y - table->cell_pad.h / 2;
   2286 	return result;
   2287 }
   2288 
   2289 function void
   2290 draw_view_ruler(BeamformerFrameView *view, Arena a, Rect view_rect, TextSpec ts)
   2291 {
   2292 	v2 vr_max_p = v2_add(view_rect.pos, view_rect.size);
   2293 	v2 start_p  = world_point_to_screen_2d(view->ruler.start, XZ(view->min_coordinate),
   2294 	                                       XZ(view->max_coordinate), view_rect.pos, vr_max_p);
   2295 	v2 end_p    = world_point_to_screen_2d(view->ruler.end, XZ(view->min_coordinate),
   2296 	                                       XZ(view->max_coordinate), view_rect.pos, vr_max_p);
   2297 
   2298 	Color rl_colour = colour_from_normalized(ts.colour);
   2299 	DrawCircleV(start_p.rl, 3, rl_colour);
   2300 	DrawLineEx(end_p.rl, start_p.rl, 2, rl_colour);
   2301 	DrawCircleV(end_p.rl, 3, rl_colour);
   2302 
   2303 	Stream buf = arena_stream(a);
   2304 	stream_append_f64(&buf, 1e3 * v2_magnitude(v2_sub(view->ruler.end, view->ruler.start)), 100);
   2305 	stream_append_s8(&buf, s8(" mm"));
   2306 
   2307 	v2 txt_p = start_p;
   2308 	v2 txt_s = measure_text(*ts.font, stream_to_s8(&buf));
   2309 	v2 pixel_delta = v2_sub(start_p, end_p);
   2310 	if (pixel_delta.y < 0) txt_p.y -= txt_s.y;
   2311 	if (pixel_delta.x < 0) txt_p.x -= txt_s.x;
   2312 	if (txt_p.x < view_rect.pos.x) txt_p.x = view_rect.pos.x;
   2313 	if (txt_p.x + txt_s.x > vr_max_p.x) txt_p.x -= (txt_p.x + txt_s.x) - vr_max_p.x;
   2314 
   2315 	draw_text(stream_to_s8(&buf), txt_p, &ts);
   2316 }
   2317 
   2318 function v2
   2319 draw_frame_view_controls(BeamformerUI *ui, Arena arena, BeamformerFrameView *view, Rect vr, v2 mouse)
   2320 {
   2321 	TextSpec text_spec = {.font = &ui->small_font, .flags = TF_LIMITED|TF_OUTLINED,
   2322 	                      .colour = RULER_COLOUR, .outline_thick = 1, .outline_colour.a = 1,
   2323 	                      .limits.size.x = vr.size.w};
   2324 
   2325 	Table *table = table_new(&arena, 3, TextAlignment_Left, TextAlignment_Left, TextAlignment_Left);
   2326 	table_push_parameter_row(table, &arena, view->gamma.name,     &view->gamma,     s8(""));
   2327 	table_push_parameter_row(table, &arena, view->threshold.name, &view->threshold, s8(""));
   2328 	if (view->log_scale->bool32)
   2329 		table_push_parameter_row(table, &arena, view->dynamic_range.name, &view->dynamic_range, s8("[dB]"));
   2330 
   2331 	Rect table_rect = vr;
   2332 	f32 height      = table_extent(table, arena, text_spec.font).y;
   2333 	height          = MIN(height, vr.size.h);
   2334 	table_rect.pos.w  += 8;
   2335 	table_rect.pos.y  += vr.size.h - height - 8;
   2336 	table_rect.size.h  = height;
   2337 	table_rect.size.w  = vr.size.w - 16;
   2338 
   2339 	return draw_table(ui, arena, table, table_rect, text_spec, mouse, 0);
   2340 }
   2341 
   2342 function void
   2343 draw_3D_xplane_frame_view(BeamformerUI *ui, Arena arena, Variable *var, Rect display_rect, v2 mouse)
   2344 {
   2345 	assert(var->type == VT_BEAMFORMER_FRAME_VIEW);
   2346 	BeamformerFrameView *view  = var->generic;
   2347 
   2348 	f32 aspect = (f32)view->texture_dim.w / (f32)view->texture_dim.h;
   2349 	Rect vr = display_rect;
   2350 	if (aspect > 1.0f) vr.size.w = vr.size.h;
   2351 	else               vr.size.h = vr.size.w;
   2352 
   2353 	if (vr.size.w > display_rect.size.w) {
   2354 		vr.size.w -= (vr.size.w - display_rect.size.w);
   2355 		vr.size.h  = vr.size.w / aspect;
   2356 	} else if (vr.size.h > display_rect.size.h) {
   2357 		vr.size.h -= (vr.size.h - display_rect.size.h);
   2358 		vr.size.w  = vr.size.h * aspect;
   2359 	}
   2360 	vr.pos = v2_add(vr.pos, v2_scale(v2_sub(display_rect.size, vr.size), 0.5));
   2361 
   2362 	i32 id = -1;
   2363 	if (hover_interaction(ui, mouse, auto_interaction(vr, var))) {
   2364 		ray mouse_ray  = ray_for_x_plane_view(ui, view, normalized_p_in_rect(vr, mouse, 0));
   2365 		v3  x_size     = v3_scale(beamformer_frame_view_plane_size(ui, view), 0.5f);
   2366 
   2367 		f32 rotation   = x_plane_rotation_for_view_plane(view, BeamformerViewPlaneTag_XZ);
   2368 		m4  x_rotation = m4_rotation_about_y(rotation);
   2369 		v3  x_position = offset_x_plane_position(ui, view, BeamformerViewPlaneTag_XZ);
   2370 
   2371 		f32 test[2] = {0};
   2372 		test[0] = obb_raycast(x_rotation, x_size, x_position, mouse_ray);
   2373 
   2374 		x_position = offset_x_plane_position(ui, view, BeamformerViewPlaneTag_YZ);
   2375 		rotation   = x_plane_rotation_for_view_plane(view, BeamformerViewPlaneTag_YZ);
   2376 		x_rotation = m4_rotation_about_y(rotation);
   2377 		test[1] = obb_raycast(x_rotation, x_size, x_position, mouse_ray);
   2378 
   2379 		if (test[0] >= 0 && test[1] >= 0) id = test[1] < test[0]? 1 : 0;
   2380 		else if (test[0] >= 0) id = 0;
   2381 		else if (test[1] >= 0) id = 1;
   2382 
   2383 		if (id != -1) {
   2384 			view->hit_test_point = v3_add(mouse_ray.origin, v3_scale(mouse_ray.direction, test[id]));
   2385 		}
   2386 	}
   2387 
   2388 	for (i32 i = 0; i < countof(view->x_plane_shifts); i++) {
   2389 		Variable *var = view->x_plane_shifts + i;
   2390 		Interaction interaction = auto_interaction(vr, var);
   2391 		if (id == i) ui->next_interaction = interaction;
   2392 		if (interaction_is_hot(ui, interaction)) var->hover_t += HOVER_SPEED * dt_for_frame;
   2393 		else                                     var->hover_t -= HOVER_SPEED * dt_for_frame;
   2394 		var->hover_t = CLAMP01(var->hover_t);
   2395 	}
   2396 
   2397 	Rectangle  tex_r  = {0, 0, view->texture_dim.w, view->texture_dim.h};
   2398 	NPatchInfo tex_np = {tex_r, 0, 0, 0, 0, NPATCH_NINE_PATCH};
   2399 	DrawTextureNPatch(make_raylib_texture(view), tex_np, vr.rl, (Vector2){0}, 0, WHITE);
   2400 
   2401 	draw_frame_view_controls(ui, arena, view, vr, mouse);
   2402 }
   2403 
   2404 function void
   2405 draw_beamformer_frame_view(BeamformerUI *ui, Arena a, Variable *var, Rect display_rect, v2 mouse)
   2406 {
   2407 	assert(var->type == VT_BEAMFORMER_FRAME_VIEW);
   2408 	BeamformerFrameView *view  = var->generic;
   2409 	BeamformerFrame     *frame = view->frame;
   2410 
   2411 	v2 txt_s = measure_text(ui->small_font, s8("-288.8 mm"));
   2412 	f32 scale_bar_size = 1.2 * txt_s.x + RULER_TICK_LENGTH;
   2413 
   2414 	v3 min = view->min_coordinate;
   2415 	v3 max = view->max_coordinate;
   2416 	v2 requested_dim = v2_sub(XZ(max), XZ(min));
   2417 	f32 aspect = requested_dim.w / requested_dim.h;
   2418 
   2419 	Rect vr = display_rect;
   2420 	v2 scale_bar_area = {0};
   2421 	if (view->axial_scale_bar_active->bool32) {
   2422 		vr.pos.y         += 0.5 * ui->small_font.baseSize;
   2423 		scale_bar_area.x += scale_bar_size;
   2424 		scale_bar_area.y += ui->small_font.baseSize;
   2425 	}
   2426 
   2427 	if (view->lateral_scale_bar_active->bool32) {
   2428 		vr.pos.x         += 0.5 * ui->small_font.baseSize;
   2429 		scale_bar_area.x += ui->small_font.baseSize;
   2430 		scale_bar_area.y += scale_bar_size;
   2431 	}
   2432 
   2433 	vr.size = v2_sub(vr.size, scale_bar_area);
   2434 	if (aspect > 1) vr.size.h = vr.size.w / aspect;
   2435 	else            vr.size.w = vr.size.h * aspect;
   2436 
   2437 	v2 occupied = v2_add(vr.size, scale_bar_area);
   2438 	if (occupied.w > display_rect.size.w) {
   2439 		vr.size.w -= (occupied.w - display_rect.size.w);
   2440 		vr.size.h  = vr.size.w / aspect;
   2441 	} else if (occupied.h > display_rect.size.h) {
   2442 		vr.size.h -= (occupied.h - display_rect.size.h);
   2443 		vr.size.w  = vr.size.h * aspect;
   2444 	}
   2445 	occupied = v2_add(vr.size, scale_bar_area);
   2446 	vr.pos   = v2_add(vr.pos, v2_scale(v2_sub(display_rect.size, occupied), 0.5));
   2447 
   2448 	/* TODO(rnp): make this depend on the requested draw orientation (x-z or y-z or x-y) */
   2449 	v2 output_dim = v2_sub(XZ(frame->max_coordinate), XZ(frame->min_coordinate));
   2450 	v2 pixels_per_meter = {
   2451 		.w = (f32)view->texture_dim.w / output_dim.w,
   2452 		.h = (f32)view->texture_dim.h / output_dim.h,
   2453 	};
   2454 
   2455 	/* NOTE(rnp): math to resize the texture without stretching when the view changes
   2456 	 * but the texture hasn't been (or cannot be) rebeamformed */
   2457 	v2 texture_points  = v2_mul(pixels_per_meter, requested_dim);
   2458 	/* TODO(rnp): this also depends on x-y, y-z, x-z */
   2459 	v2 texture_start   = {
   2460 		.x = pixels_per_meter.x * 0.5 * (output_dim.x - requested_dim.x),
   2461 		.y = pixels_per_meter.y * (frame->max_coordinate.z - max.z),
   2462 	};
   2463 
   2464 	Rectangle  tex_r  = {texture_start.x, texture_start.y, texture_points.x, texture_points.y};
   2465 	NPatchInfo tex_np = { tex_r, 0, 0, 0, 0, NPATCH_NINE_PATCH };
   2466 	DrawTextureNPatch(make_raylib_texture(view), tex_np, vr.rl, (Vector2){0}, 0, WHITE);
   2467 
   2468 	v2 start_pos  = vr.pos;
   2469 	start_pos.y  += vr.size.y;
   2470 
   2471 	if (vr.size.w > 0 && view->lateral_scale_bar_active->bool32) {
   2472 		do_scale_bar(ui, a, &view->lateral_scale_bar, mouse,
   2473 		             (Rect){.pos = start_pos, .size = vr.size},
   2474 		             *view->lateral_scale_bar.scale_bar.min_value * 1e3,
   2475 		             *view->lateral_scale_bar.scale_bar.max_value * 1e3, s8(" mm"));
   2476 	}
   2477 
   2478 	start_pos    = vr.pos;
   2479 	start_pos.x += vr.size.x;
   2480 
   2481 	if (vr.size.h > 0 && view->axial_scale_bar_active->bool32) {
   2482 		do_scale_bar(ui, a, &view->axial_scale_bar, mouse,
   2483 		             (Rect){.pos = start_pos, .size = vr.size},
   2484 		             *view->axial_scale_bar.scale_bar.max_value * 1e3,
   2485 		             *view->axial_scale_bar.scale_bar.min_value * 1e3, s8(" mm"));
   2486 	}
   2487 
   2488 	TextSpec text_spec = {.font = &ui->small_font, .flags = TF_LIMITED|TF_OUTLINED,
   2489 	                      .colour = RULER_COLOUR, .outline_thick = 1, .outline_colour.a = 1,
   2490 	                      .limits.size.x = vr.size.w};
   2491 
   2492 	f32 draw_table_width = vr.size.w;
   2493 	/* NOTE: avoid hover_t modification */
   2494 	Interaction viewer = auto_interaction(vr, var);
   2495 	if (point_in_rect(mouse, viewer.rect)) {
   2496 		ui->next_interaction = viewer;
   2497 
   2498 		v2 world = screen_point_to_world_2d(mouse, vr.pos, v2_add(vr.pos, vr.size),
   2499 		                                    XZ(view->min_coordinate),
   2500 		                                    XZ(view->max_coordinate));
   2501 		Stream buf = arena_stream(a);
   2502 		stream_append_v2(&buf, v2_scale(world, 1e3));
   2503 
   2504 		text_spec.limits.size.w -= 4;
   2505 		v2 txt_s = measure_text(*text_spec.font, stream_to_s8(&buf));
   2506 		v2 txt_p = {
   2507 			.x = vr.pos.x + vr.size.w - txt_s.w - 4,
   2508 			.y = vr.pos.y + vr.size.h - txt_s.h - 4,
   2509 		};
   2510 		txt_p.x = MAX(vr.pos.x, txt_p.x);
   2511 		draw_table_width -= draw_text(stream_to_s8(&buf), txt_p, &text_spec).w;
   2512 		text_spec.limits.size.w += 4;
   2513 	}
   2514 
   2515 	{
   2516 		Stream buf = arena_stream(a);
   2517 		s8 shader  = push_das_shader_kind(&buf, frame->das_shader_kind, frame->compound_count);
   2518 		text_spec.font = &ui->font;
   2519 		text_spec.limits.size.w -= 16;
   2520 		v2 txt_s   = measure_text(*text_spec.font, shader);
   2521 		v2 txt_p  = {
   2522 			.x = vr.pos.x + vr.size.w - txt_s.w - 16,
   2523 			.y = vr.pos.y + 4,
   2524 		};
   2525 		txt_p.x = MAX(vr.pos.x, txt_p.x);
   2526 		draw_text(stream_to_s8(&buf), txt_p, &text_spec);
   2527 		text_spec.font = &ui->small_font;
   2528 		text_spec.limits.size.w += 16;
   2529 	}
   2530 
   2531 	if (view->ruler.state != RulerState_None) draw_view_ruler(view, a, vr, text_spec);
   2532 
   2533 	vr.size.w = draw_table_width;
   2534 	draw_frame_view_controls(ui, a, view, vr, mouse);
   2535 }
   2536 
   2537 function v2
   2538 draw_compute_progress_bar(BeamformerUI *ui, Arena arena, ComputeProgressBar *state, Rect r)
   2539 {
   2540 	if (*state->processing) state->display_t_velocity += 65 * dt_for_frame;
   2541 	else                    state->display_t_velocity -= 45 * dt_for_frame;
   2542 
   2543 	state->display_t_velocity = CLAMP(state->display_t_velocity, -10, 10);
   2544 	state->display_t += state->display_t_velocity * dt_for_frame;
   2545 	state->display_t  = CLAMP01(state->display_t);
   2546 
   2547 	if (state->display_t > (1.0 / 255.0)) {
   2548 		Rect outline = {.pos = r.pos, .size = {.w = r.size.w, .h = ui->font.baseSize}};
   2549 		outline      = scale_rect_centered(outline, (v2){.x = 0.96, .y = 0.7});
   2550 		Rect filled  = outline;
   2551 		filled.size.w *= *state->progress;
   2552 		DrawRectangleRounded(filled.rl, 2, 0, fade(colour_from_normalized(HOVERED_COLOUR),
   2553 		                                           state->display_t));
   2554 		DrawRectangleRoundedLinesEx(outline.rl, 2, 0, 3, fade(BLACK, state->display_t));
   2555 	}
   2556 
   2557 	v2 result = {.x = r.size.w, .y = ui->font.baseSize};
   2558 	return result;
   2559 }
   2560 
   2561 function s8
   2562 push_compute_time(Arena *arena, s8 prefix, f32 time)
   2563 {
   2564 	Stream sb = arena_stream(*arena);
   2565 	stream_append_s8(&sb, prefix);
   2566 	stream_append_f64_e(&sb, time);
   2567 	return arena_stream_commit(arena, &sb);
   2568 }
   2569 
   2570 function v2
   2571 draw_compute_stats_bar_view(BeamformerUI *ui, Arena arena, ComputeShaderStats *stats, u32 *stages,
   2572                             u32 stages_count, f32 compute_time_sum, TextSpec ts, Rect r, v2 mouse)
   2573 {
   2574 	read_only local_persist s8 frame_labels[] = {s8_comp("0:"), s8_comp("-1:"), s8_comp("-2:"), s8_comp("-3:")};
   2575 	f32 total_times[countof(frame_labels)] = {0};
   2576 	Table *table = table_new(&arena, countof(frame_labels), TextAlignment_Right, TextAlignment_Left);
   2577 	for (u32 i = 0; i < countof(frame_labels); i++) {
   2578 		TableCell *cells = table_push_row(table, &arena, TRK_CELLS)->data;
   2579 		cells[0].text = frame_labels[i];
   2580 		u32 frame_index = (stats->latest_frame_index - i) % countof(stats->table.times);
   2581 		u32 seen_shaders = 0;
   2582 		for (u32 j = 0; j < stages_count; j++) {
   2583 			if ((seen_shaders & (1 << stages[j])) == 0)
   2584 				total_times[i] += stats->table.times[frame_index][stages[j]];
   2585 			seen_shaders |= (1 << stages[j]);
   2586 		}
   2587 	}
   2588 
   2589 	#define X(e, n, s, h, pn) [BeamformerShaderKind_##e] = s8_comp(pn ": "),
   2590 	read_only local_persist s8 labels[BeamformerShaderKind_ComputeCount] = {COMPUTE_SHADERS};
   2591 	#undef X
   2592 
   2593 	v2 result = table_extent(table, arena, ts.font);
   2594 
   2595 	f32 remaining_width = r.size.w - result.w - table->cell_pad.w;
   2596 	f32 average_width = 0.8 * remaining_width;
   2597 
   2598 	s8 mouse_text = s8("");
   2599 	v2 text_pos;
   2600 
   2601 	u32 row_index = 0;
   2602 	TableIterator *it = table_iterator_new(table, TIK_ROWS, &arena, 0, r.pos, ts.font);
   2603 	for (TableRow *row = table_iterator_next(it, &arena);
   2604 	     row;
   2605 	     row = table_iterator_next(it, &arena))
   2606 	{
   2607 		Rect cr   = it->cell_rect;
   2608 		cr.size.w = table->widths[0];
   2609 		ts.limits.size.w = cr.size.w;
   2610 		draw_table_cell(ui, arena, (TableCell *)row->data, cr, table->alignment[0], ts, mouse);
   2611 
   2612 		u32 frame_index = (stats->latest_frame_index - row_index) % countof(stats->table.times);
   2613 		f32 total_width = average_width * total_times[row_index] / compute_time_sum;
   2614 		Rect rect;
   2615 		rect.pos  = v2_add(cr.pos, (v2){.x = cr.size.w + table->cell_pad.w , .y = cr.size.h * 0.15});
   2616 		rect.size = (v2){.y = 0.7 * cr.size.h};
   2617 		for (u32 i = 0; i < stages_count; i++) {
   2618 			rect.size.w = total_width * stats->table.times[frame_index][stages[i]] / total_times[row_index];
   2619 			Color color = colour_from_normalized(g_colour_palette[stages[i] % countof(g_colour_palette)]);
   2620 			DrawRectangleRec(rect.rl, color);
   2621 			if (point_in_rect(mouse, rect)) {
   2622 				text_pos   = v2_add(rect.pos, (v2){.x = table->cell_pad.w});
   2623 				mouse_text = push_compute_time(&arena, labels[stages[i]],
   2624 				                               stats->table.times[frame_index][stages[i]]);
   2625 			}
   2626 			rect.pos.x += rect.size.w;
   2627 		}
   2628 		row_index++;
   2629 	}
   2630 
   2631 	v2 start = v2_add(r.pos, (v2){.x = table->widths[0] + average_width + table->cell_pad.w});
   2632 	v2 end   = v2_add(start, (v2){.y = result.y});
   2633 	DrawLineEx(start.rl, end.rl, 4, colour_from_normalized(FG_COLOUR));
   2634 
   2635 	if (mouse_text.len) {
   2636 		ts.font = &ui->small_font;
   2637 		ts.flags &= ~TF_LIMITED;
   2638 		ts.flags |=  TF_OUTLINED;
   2639 		ts.outline_colour = (v4){.a = 1};
   2640 		ts.outline_thick  = 1;
   2641 		draw_text(mouse_text, text_pos, &ts);
   2642 	}
   2643 
   2644 	return result;
   2645 }
   2646 
   2647 function void
   2648 push_table_time_row(Table *table, Arena *arena, s8 label, f32 time)
   2649 {
   2650 	assert(table->columns == 3);
   2651 	TableCell *cells = table_push_row(table, arena, TRK_CELLS)->data;
   2652 	cells[0].text = label;
   2653 	cells[1].text = push_compute_time(arena, s8(""), time);
   2654 	cells[2].text = s8("[s]");
   2655 }
   2656 
   2657 function v2
   2658 draw_compute_stats_view(BeamformerUI *ui, Arena arena, Variable *view, Rect r, v2 mouse)
   2659 {
   2660 	assert(view->type == VT_COMPUTE_STATS_VIEW);
   2661 
   2662 	ComputeStatsView *csv = &view->compute_stats_view;
   2663 	BeamformerSharedMemory *sm    = ui->shared_memory.region;
   2664 	ComputeShaderStats     *stats = csv->compute_shader_stats;
   2665 	f32 compute_time_sum = 0;
   2666 	u32 stages           = sm->compute_stages_count;
   2667 	TextSpec text_spec   = {.font = &ui->font, .colour = FG_COLOUR, .flags = TF_LIMITED};
   2668 
   2669 	u32 compute_stages[MAX_COMPUTE_SHADER_STAGES];
   2670 	mem_copy(compute_stages, sm->compute_stages, stages * sizeof(*compute_stages));
   2671 
   2672 	static_assert(BeamformerShaderKind_ComputeCount <= 32, "shader kind bitfield test");
   2673 	u32 seen_shaders = 0;
   2674 	for (u32 i = 0; i < stages; i++) {
   2675 		BeamformerShaderKind index = compute_stages[i];
   2676 		if ((seen_shaders & (1 << index)) == 0)
   2677 			compute_time_sum += stats->average_times[index];
   2678 		seen_shaders |= (1 << index);
   2679 	}
   2680 
   2681 	v2 result = {0};
   2682 
   2683 	Table *table = table_new(&arena, 2, TextAlignment_Left, TextAlignment_Left, TextAlignment_Left);
   2684 	switch (csv->kind) {
   2685 	case ComputeStatsViewKind_Average:{
   2686 		#define X(e, n, s, h, pn) [BeamformerShaderKind_##e] = s8_comp(pn ":"),
   2687 		read_only local_persist s8 labels[BeamformerShaderKind_ComputeCount] = {COMPUTE_SHADERS};
   2688 		#undef X
   2689 		da_reserve(&arena, table, stages);
   2690 		for (u32 i = 0; i < stages; i++) {
   2691 			push_table_time_row(table, &arena, labels[compute_stages[i]],
   2692 			                    stats->average_times[compute_stages[i]]);
   2693 		}
   2694 	}break;
   2695 	case ComputeStatsViewKind_Bar:{
   2696 		result = draw_compute_stats_bar_view(ui, arena, stats, compute_stages, stages, compute_time_sum,
   2697 		                                     text_spec, r, mouse);
   2698 		r.pos = v2_add(r.pos, (v2){.y = result.y});
   2699 	}break;
   2700 	InvalidDefaultCase;
   2701 	}
   2702 
   2703 	push_table_time_row(table, &arena, s8("Compute Total:"),   compute_time_sum);
   2704 	push_table_time_row(table, &arena, s8("RF Upload Delta:"), stats->rf_time_delta_average);
   2705 
   2706 	result = v2_add(result, table_extent(table, arena, text_spec.font));
   2707 	draw_table(ui, arena, table, r, text_spec, (v2){0}, 0);
   2708 	return result;
   2709 }
   2710 
   2711 function v2
   2712 draw_live_controls_view(BeamformerUI *ui, Arena arena, Variable *var, Rect r, v2 mouse)
   2713 {
   2714 	BeamformerSharedMemory          *sm  = ui->shared_memory.region;
   2715 	BeamformerLiveImagingParameters *lip = &sm->live_imaging_parameters;
   2716 	BeamformerLiveControlsView      *lv  = var->generic;
   2717 
   2718 	TextSpec text_spec = {.font = &ui->font, .colour = FG_COLOUR, .flags = TF_LIMITED};
   2719 	text_spec.limits.size.w = r.size.w;
   2720 
   2721 	v2 slider_size = {{MIN(140.0f, r.size.w), ui->font.baseSize}};
   2722 	v2 button_size = {{MIN(r.size.w, slider_size.x + ui->font.baseSize), ui->font.baseSize * 1.5}};
   2723 
   2724 	f32 text_off   = r.pos.x + 0.5 * MAX(0, (r.size.w - slider_size.w - ui->font.baseSize));
   2725 	f32 slider_off = r.pos.x + 0.5 * (r.size.w - slider_size.w);
   2726 	f32 button_off = r.pos.x + 0.5 * (r.size.w - button_size.w);
   2727 
   2728 	v2 at = {{text_off, r.pos.y}};
   2729 
   2730 	v4 hsv_power_slider = {{0.35 * ease_cubic(1.0f - lip->transmit_power), 0.65f, 0.65f, 1}};
   2731 	at.y += draw_text(s8("Power:"), at, &text_spec).y;
   2732 	at.x  = slider_off;
   2733 	at.y += draw_variable_slider(ui, &lv->transmit_power, (Rect){.pos = at, .size = slider_size},
   2734 	                             lip->transmit_power, hsv_to_rgb(hsv_power_slider), mouse);
   2735 
   2736 	at.x  = text_off;
   2737 	at.y += draw_text(s8("TGC:"), at, &text_spec).y;
   2738 	at.x  = slider_off;
   2739 	for (u32 i = 0; i < countof(lip->tgc_control_points); i++) {
   2740 		Variable *v = lv->tgc_control_points + i;
   2741 		at.y += draw_variable_slider(ui, v, (Rect){.pos = at, .size = slider_size},
   2742 		                             lip->tgc_control_points[i], g_colour_palette[1], mouse);
   2743 
   2744 		if (interaction_is_hot(ui, auto_interaction(r, v)))
   2745 			lv->hot_field_flag = BeamformerLiveImagingDirtyFlags_TGCControlPoints;
   2746 	}
   2747 
   2748 	at.x  = button_off;
   2749 	at.y += ui->font.baseSize * 0.5;
   2750 	at.y += draw_fancy_button(ui, &lv->stop_button, lv->stop_button.name,
   2751 	                          (Rect){.pos = at, .size = button_size},
   2752 	                          BORDER_COLOUR, mouse, text_spec).y;
   2753 
   2754 	if (lip->save_enabled) {
   2755 		b32 active = lip->save_active;
   2756 		s8  label  = lv->save_button.cycler.labels[active % lv->save_button.cycler.cycle_length];
   2757 
   2758 		lv->save_button_blink_t += lv->save_button_blink_scale * dt_for_frame;
   2759 		if (lv->save_button_blink_t >= 1.0f) lv->save_button_blink_scale = -BLINK_SPEED;
   2760 		if (lv->save_button_blink_t <= 0.0f) lv->save_button_blink_scale =  BLINK_SPEED;
   2761 
   2762 		v4 border_colour = BORDER_COLOUR;
   2763 		if (active) border_colour = v4_lerp(border_colour, FOCUSED_COLOUR, ease_cubic(lv->save_button_blink_t));
   2764 
   2765 		at.y += ui->font.baseSize * 0.25;
   2766 		at.y += draw_fancy_button(ui, &lv->save_button, label, (Rect){.pos = at, .size = button_size},
   2767 		                          border_colour, mouse, text_spec).y;
   2768 
   2769 		if (interaction_is_hot(ui, auto_interaction(r, &lv->save_button)))
   2770 			lv->hot_field_flag = BeamformerLiveImagingDirtyFlags_SaveData;
   2771 	}
   2772 
   2773 	if (interaction_is_hot(ui, auto_interaction(r, &lv->transmit_power)))
   2774 		lv->hot_field_flag = BeamformerLiveImagingDirtyFlags_TransmitPower;
   2775 	if (interaction_is_hot(ui, auto_interaction(r, &lv->stop_button)))
   2776 		lv->hot_field_flag = BeamformerLiveImagingDirtyFlags_StopImaging;
   2777 
   2778 	v2 result = {{r.size.w, at.y - r.pos.y}};
   2779 	return result;
   2780 }
   2781 
   2782 struct variable_iterator { Variable *current; };
   2783 function i32
   2784 variable_iterator_next(struct variable_iterator *it)
   2785 {
   2786 	i32 result = 0;
   2787 
   2788 	if (it->current->type == VT_GROUP && it->current->group.expanded) {
   2789 		it->current = it->current->group.first;
   2790 		result++;
   2791 	} else {
   2792 		while (it->current) {
   2793 			if (it->current->next) {
   2794 				it->current = it->current->next;
   2795 				break;
   2796 			}
   2797 			it->current = it->current->parent;
   2798 			result--;
   2799 		}
   2800 	}
   2801 
   2802 	return result;
   2803 }
   2804 
   2805 function v2
   2806 draw_ui_view_menu(BeamformerUI *ui, Variable *group, Arena arena, Rect r, v2 mouse, TextSpec text_spec)
   2807 {
   2808 	assert(group->type == VT_GROUP);
   2809 	Table *table = table_new(&arena, 0, TextAlignment_Left, TextAlignment_Right);
   2810 	table->row_border_thick = 2.0f;
   2811 	table->cell_pad         = (v2){{16.0f, 8.0f}};
   2812 
   2813 	i32 nesting = 0;
   2814 	for (struct variable_iterator it = {group->group.first};
   2815 	     it.current;
   2816 	     nesting = variable_iterator_next(&it))
   2817 	{
   2818 		(void)nesting;
   2819 		assert(nesting == 0);
   2820 		Variable *var = it.current;
   2821 		TableCell *cells = table_push_row(table, &arena, TRK_CELLS)->data;
   2822 		switch (var->type) {
   2823 		case VT_B32:
   2824 		case VT_CYCLER:
   2825 		{
   2826 			cells[0] = (TableCell){.text = var->name};
   2827 			cells[1] = table_variable_cell(&arena, var);
   2828 		}break;
   2829 		case VT_UI_BUTTON:{
   2830 			cells[0] = (TableCell){.text = var->name, .kind = TableCellKind_Variable, .var = var};
   2831 		}break;
   2832 		InvalidDefaultCase;
   2833 		}
   2834 	}
   2835 
   2836 	r.size = table_extent(table, arena, text_spec.font);
   2837 	return draw_table(ui, arena, table, r, text_spec, mouse, 0);
   2838 }
   2839 
   2840 function v2
   2841 draw_ui_view_listing(BeamformerUI *ui, Variable *group, Arena arena, Rect r, v2 mouse, TextSpec text_spec)
   2842 {
   2843 	assert(group->type == VT_GROUP);
   2844 	Table *table = table_new(&arena, 0, TextAlignment_Left, TextAlignment_Left, TextAlignment_Right);
   2845 
   2846 	i32 nesting = 0;
   2847 	for (struct variable_iterator it = {group->group.first};
   2848 	     it.current;
   2849 	     nesting = variable_iterator_next(&it))
   2850 	{
   2851 		while (nesting > 0) {
   2852 			table = table_begin_subtable(table, &arena, TextAlignment_Left,
   2853 			                             TextAlignment_Center, TextAlignment_Right);
   2854 			nesting--;
   2855 		}
   2856 		while (nesting < 0) { table = table_end_subtable(table); nesting++; }
   2857 
   2858 		Variable *var = it.current;
   2859 		switch (var->type) {
   2860 		case VT_CYCLER:
   2861 		case VT_BEAMFORMER_VARIABLE:
   2862 		{
   2863 			s8 suffix = s8("");
   2864 			if (var->type == VT_BEAMFORMER_VARIABLE)
   2865 				suffix = var->beamformer_variable.suffix;
   2866 			table_push_parameter_row(table, &arena, var->name, var, suffix);
   2867 		}break;
   2868 		case VT_GROUP:{
   2869 			VariableGroup *g = &var->group;
   2870 
   2871 			TableCell *cells = table_push_row(table, &arena, TRK_CELLS)->data;
   2872 			cells[0] = (TableCell){.text = var->name, .kind = TableCellKind_Variable, .var = var};
   2873 
   2874 			if (!g->expanded) {
   2875 				Stream sb = arena_stream(arena);
   2876 				stream_append_variable_group(&sb, var);
   2877 				cells[1].kind = TableCellKind_VariableGroup;
   2878 				cells[1].text = arena_stream_commit(&arena, &sb);
   2879 				cells[1].var  = var;
   2880 
   2881 				Variable *v = g->first;
   2882 				assert(!v || v->type == VT_BEAMFORMER_VARIABLE);
   2883 				/* NOTE(rnp): assume the suffix is the same for all elements */
   2884 				if (v) cells[2].text = v->beamformer_variable.suffix;
   2885 			}
   2886 		}break;
   2887 		InvalidDefaultCase;
   2888 		}
   2889 	}
   2890 
   2891 	v2 result = table_extent(table, arena, text_spec.font);
   2892 	draw_table(ui, arena, table, r, text_spec, mouse, 0);
   2893 	return result;
   2894 }
   2895 
   2896 function Rect
   2897 draw_ui_view_container(BeamformerUI *ui, Variable *var, v2 mouse, Rect bounds)
   2898 {
   2899 	UIView *fw = &var->view;
   2900 	Rect result = fw->rect;
   2901 	if (fw->rect.size.x > 0 && fw->rect.size.y > 0) {
   2902 		f32 line_height = ui->small_font.baseSize;
   2903 
   2904 		f32 pad = MAX(line_height + 5, UI_REGION_PAD);
   2905 		if (fw->rect.pos.y < pad)
   2906 			fw->rect.pos.y += pad - fw->rect.pos.y;
   2907 		result = fw->rect;
   2908 
   2909 		f32 delta_x = (result.pos.x + result.size.x) - (bounds.size.x + bounds.pos.x);
   2910 		if (delta_x > 0) {
   2911 			result.pos.x -= delta_x;
   2912 			result.pos.x  = MAX(0, result.pos.x);
   2913 		}
   2914 
   2915 		Rect container = result;
   2916 		if (fw->close) {
   2917 			container.pos.y  -= 5 + line_height;
   2918 			container.size.y += 2 + line_height;
   2919 			Rect handle = {{container.pos, (v2){.x = container.size.w, .y = 2 + line_height}}};
   2920 			Rect close;
   2921 			hover_interaction(ui, mouse, auto_interaction(container, var));
   2922 			cut_rect_horizontal(handle, handle.size.w - handle.size.h - 6, 0, &close);
   2923 			close.size.w = close.size.h;
   2924 			DrawRectangleRounded(handle.rl, 0.1, 0, colour_from_normalized(BG_COLOUR));
   2925 			DrawRectangleRoundedLinesEx(handle.rl, 0.2, 0, 2, BLACK);
   2926 			draw_close_button(ui, fw->close, mouse, close, (v2){{0.45, 0.45}});
   2927 		} else {
   2928 			hover_interaction(ui, mouse, auto_interaction(container, var));
   2929 		}
   2930 		f32 roundness = 12.0f / fw->rect.size.y;
   2931 		DrawRectangleRounded(result.rl, roundness / 2, 0, colour_from_normalized(BG_COLOUR));
   2932 		DrawRectangleRoundedLinesEx(result.rl, roundness, 0, 2, BLACK);
   2933 	}
   2934 	return result;
   2935 }
   2936 
   2937 function void
   2938 draw_ui_view(BeamformerUI *ui, Variable *ui_view, Rect r, v2 mouse, TextSpec text_spec)
   2939 {
   2940 	assert(ui_view->type == VT_UI_VIEW || ui_view->type == VT_UI_MENU || ui_view->type == VT_UI_TEXT_BOX);
   2941 
   2942 	UIView *view = &ui_view->view;
   2943 
   2944 	if (view->flags & UIViewFlag_Floating) {
   2945 		r = draw_ui_view_container(ui, ui_view, mouse, r);
   2946 	} else {
   2947 		if (view->rect.size.h - r.size.h < view->rect.pos.h)
   2948 			view->rect.pos.h = view->rect.size.h - r.size.h;
   2949 
   2950 		if (view->rect.size.h - r.size.h < 0)
   2951 			view->rect.pos.h = 0;
   2952 
   2953 		r.pos.y -= view->rect.pos.h;
   2954 	}
   2955 
   2956 	v2 size = {0};
   2957 
   2958 	Variable *var = view->child;
   2959 	switch (var->type) {
   2960 	case VT_GROUP:{
   2961 		if (ui_view->type == VT_UI_MENU)
   2962 			size = draw_ui_view_menu(ui, var, ui->arena, r, mouse, text_spec);
   2963 		else {
   2964 			size = draw_ui_view_listing(ui, var, ui->arena, r, mouse, text_spec);
   2965 		}
   2966 	}break;
   2967 	case VT_BEAMFORMER_FRAME_VIEW: {
   2968 		BeamformerFrameView *bv = var->generic;
   2969 		if (frame_view_ready_to_present(ui, bv)) {
   2970 			if (bv->kind == BeamformerFrameViewKind_3DXPlane)
   2971 				draw_3D_xplane_frame_view(ui, ui->arena, var, r, mouse);
   2972 			else
   2973 				draw_beamformer_frame_view(ui, ui->arena, var, r, mouse);
   2974 		}
   2975 	} break;
   2976 	case VT_COMPUTE_PROGRESS_BAR: {
   2977 		size = draw_compute_progress_bar(ui, ui->arena, &var->compute_progress_bar, r);
   2978 	} break;
   2979 	case VT_COMPUTE_STATS_VIEW:{ size = draw_compute_stats_view(ui, ui->arena, var, r, mouse); }break;
   2980 	case VT_LIVE_CONTROLS_VIEW:{
   2981 		if (view->rect.size.h - r.size.h < 0)
   2982 			r.pos.y += 0.5 * (r.size.h - view->rect.size.h);
   2983 		BeamformerSharedMemory *sm = ui->shared_memory.region;
   2984 		if (sm->live_imaging_parameters.active)
   2985 			size = draw_live_controls_view(ui, ui->arena, var, r, mouse);
   2986 	}break;
   2987 	InvalidDefaultCase;
   2988 	}
   2989 
   2990 	view->rect.size = size;
   2991 }
   2992 
   2993 function void
   2994 draw_layout_variable(BeamformerUI *ui, Variable *var, Rect draw_rect, v2 mouse)
   2995 {
   2996 	if (var->type != VT_UI_REGION_SPLIT) {
   2997 		v2 shrink = {.x = UI_REGION_PAD, .y = UI_REGION_PAD};
   2998 		draw_rect = shrink_rect_centered(draw_rect, shrink);
   2999 		draw_rect.size = v2_floor(draw_rect.size);
   3000 		BeginScissorMode(draw_rect.pos.x, draw_rect.pos.y, draw_rect.size.w, draw_rect.size.h);
   3001 		draw_rect = draw_title_bar(ui, ui->arena, var, draw_rect, mouse);
   3002 		EndScissorMode();
   3003 	}
   3004 
   3005 	/* TODO(rnp): post order traversal of the ui tree will remove the need for this */
   3006 	if (!CheckCollisionPointRec(mouse.rl, draw_rect.rl))
   3007 		mouse = (v2){.x = F32_INFINITY, .y = F32_INFINITY};
   3008 
   3009 	draw_rect.size = v2_floor(draw_rect.size);
   3010 	BeginScissorMode(draw_rect.pos.x, draw_rect.pos.y, draw_rect.size.w, draw_rect.size.h);
   3011 	switch (var->type) {
   3012 	case VT_UI_VIEW: {
   3013 		hover_interaction(ui, mouse, auto_interaction(draw_rect, var));
   3014 		TextSpec text_spec = {.font = &ui->font, .colour = FG_COLOUR, .flags = TF_LIMITED};
   3015 		draw_ui_view(ui, var, draw_rect, mouse, text_spec);
   3016 	} break;
   3017 	case VT_UI_REGION_SPLIT: {
   3018 		RegionSplit *rs = &var->region_split;
   3019 
   3020 		Rect split, hover;
   3021 		switch (rs->direction) {
   3022 		case RSD_VERTICAL: {
   3023 			split_rect_vertical(draw_rect, rs->fraction, 0, &split);
   3024 			split.pos.x  += UI_REGION_PAD;
   3025 			split.pos.y  -= UI_SPLIT_HANDLE_THICK / 2;
   3026 			split.size.h  = UI_SPLIT_HANDLE_THICK;
   3027 			split.size.w -= 2 * UI_REGION_PAD;
   3028 			hover = extend_rect_centered(split, (v2){.y = 0.75 * UI_REGION_PAD});
   3029 		} break;
   3030 		case RSD_HORIZONTAL: {
   3031 			split_rect_horizontal(draw_rect, rs->fraction, 0, &split);
   3032 			split.pos.x  -= UI_SPLIT_HANDLE_THICK / 2;
   3033 			split.pos.y  += UI_REGION_PAD;
   3034 			split.size.w  = UI_SPLIT_HANDLE_THICK;
   3035 			split.size.h -= 2 * UI_REGION_PAD;
   3036 			hover = extend_rect_centered(split, (v2){.x = 0.75 * UI_REGION_PAD});
   3037 		} break;
   3038 		}
   3039 
   3040 		Interaction drag = {.kind = InteractionKind_Drag, .rect = hover, .var = var};
   3041 		hover_interaction(ui, mouse, drag);
   3042 
   3043 		v4 colour = HOVERED_COLOUR;
   3044 		colour.a  = var->hover_t;
   3045 		DrawRectangleRounded(split.rl, 0.6, 0, colour_from_normalized(colour));
   3046 	} break;
   3047 	InvalidDefaultCase;
   3048 	}
   3049 	EndScissorMode();
   3050 }
   3051 
   3052 function void
   3053 draw_ui_regions(BeamformerUI *ui, Rect window, v2 mouse)
   3054 {
   3055 	struct region_frame {
   3056 		Variable *var;
   3057 		Rect      rect;
   3058 	} init[16];
   3059 
   3060 	struct {
   3061 		struct region_frame *data;
   3062 		iz count;
   3063 		iz capacity;
   3064 	} stack = {init, 0, ARRAY_COUNT(init)};
   3065 
   3066 	TempArena arena_savepoint = begin_temp_arena(&ui->arena);
   3067 
   3068 	*da_push(&ui->arena, &stack) = (struct region_frame){ui->regions, window};
   3069 	while (stack.count) {
   3070 		struct region_frame *top = stack.data + --stack.count;
   3071 		Rect rect = top->rect;
   3072 		draw_layout_variable(ui, top->var, rect, mouse);
   3073 
   3074 		if (top->var->type == VT_UI_REGION_SPLIT) {
   3075 			Rect first, second;
   3076 			RegionSplit *rs = &top->var->region_split;
   3077 			switch (rs->direction) {
   3078 			case RSD_VERTICAL: {
   3079 				split_rect_vertical(rect, rs->fraction, &first, &second);
   3080 			} break;
   3081 			case RSD_HORIZONTAL: {
   3082 				split_rect_horizontal(rect, rs->fraction, &first, &second);
   3083 			} break;
   3084 			}
   3085 
   3086 			*da_push(&ui->arena, &stack) = (struct region_frame){rs->right, second};
   3087 			*da_push(&ui->arena, &stack) = (struct region_frame){rs->left,  first};
   3088 		}
   3089 	}
   3090 
   3091 	end_temp_arena(arena_savepoint);
   3092 }
   3093 
   3094 function void
   3095 draw_floating_widgets(BeamformerUI *ui, Rect window_rect, v2 mouse)
   3096 {
   3097 	TextSpec text_spec = {.font = &ui->small_font, .colour = FG_COLOUR};
   3098 	window_rect = shrink_rect_centered(window_rect, (v2){{UI_REGION_PAD, UI_REGION_PAD}});
   3099 	for (Variable *var = ui->floating_widget_sentinal.parent;
   3100 	     var != &ui->floating_widget_sentinal;
   3101 	     var = var->parent)
   3102 	{
   3103 		if (var->type == VT_UI_TEXT_BOX) {
   3104 			UIView *fw = &var->view;
   3105 			InputState *is = &ui->text_input_state;
   3106 
   3107 			draw_ui_view_container(ui, var, mouse, fw->rect);
   3108 
   3109 			f32 cursor_width = (is->cursor == is->count) ? 0.55 * is->font->baseSize : 4;
   3110 			s8 text      = {.len = is->count, .data = is->buf};
   3111 			v2 text_size = measure_text(*is->font, text);
   3112 
   3113 			f32 text_pad = 4.0f;
   3114 			f32 desired_width = text_pad + text_size.w + cursor_width;
   3115 			fw->rect.size = (v2){{MAX(desired_width, fw->rect.size.w), text_size.h + text_pad}};
   3116 
   3117 			v2 text_position   = {{fw->rect.pos.x + text_pad / 2, fw->rect.pos.y + text_pad / 2}};
   3118 			f32 cursor_offset  = measure_text(*is->font, (s8){is->cursor, text.data}).w;
   3119 			cursor_offset     += text_position.x;
   3120 
   3121 			Rect cursor;
   3122 			cursor.pos  = (v2){{cursor_offset, text_position.y}};
   3123 			cursor.size = (v2){{cursor_width,  text_size.h}};
   3124 
   3125 			v4 cursor_colour = FOCUSED_COLOUR;
   3126 			cursor_colour.a  = CLAMP01(is->cursor_blink_t);
   3127 			v4 text_colour   = v4_lerp(FG_COLOUR, HOVERED_COLOUR, fw->child->hover_t);
   3128 
   3129 			TextSpec text_spec = {.font = is->font, .colour = text_colour};
   3130 			draw_text(text, text_position, &text_spec);
   3131 			DrawRectanglePro(cursor.rl, (Vector2){0}, 0, colour_from_normalized(cursor_colour));
   3132 		} else {
   3133 			draw_ui_view(ui, var, window_rect, mouse, text_spec);
   3134 		}
   3135 	}
   3136 }
   3137 
   3138 function void
   3139 scroll_interaction(Variable *var, f32 delta)
   3140 {
   3141 	switch (var->type) {
   3142 	case VT_B32:{ var->bool32  = !var->bool32; }break;
   3143 	case VT_F32:{ var->real32 += delta;        }break;
   3144 	case VT_I32:{ var->signed32 += delta;      }break;
   3145 	case VT_U32:{ var->unsigned32 += delta;    }break;
   3146 	case VT_SCALED_F32:{ var->scaled_real32.val += delta * var->scaled_real32.scale; }break;
   3147 	case VT_BEAMFORMER_FRAME_VIEW:{
   3148 		BeamformerFrameView *bv = var->generic;
   3149 		bv->threshold.real32 += delta;
   3150 		bv->dirty = 1;
   3151 	} break;
   3152 	case VT_BEAMFORMER_VARIABLE:{
   3153 		BeamformerVariable *bv = &var->beamformer_variable;
   3154 		f32 value  = *bv->store + delta * bv->scroll_scale;
   3155 		*bv->store = CLAMP(value, bv->limits.x, bv->limits.y);
   3156 	}break;
   3157 	case VT_CYCLER:{
   3158 		*var->cycler.state += delta > 0? 1 : -1;
   3159 		*var->cycler.state %= var->cycler.cycle_length;
   3160 	}break;
   3161 	case VT_UI_VIEW:{
   3162 		var->view.rect.pos.h += UI_SCROLL_SPEED * delta;
   3163 		var->view.rect.pos.h  = MAX(0, var->view.rect.pos.h);
   3164 	}break;
   3165 	InvalidDefaultCase;
   3166 	}
   3167 }
   3168 
   3169 function void
   3170 begin_text_input(InputState *is, Rect r, Variable *container, v2 mouse)
   3171 {
   3172 	assert(container->type == VT_UI_TEXT_BOX);
   3173 	Font *font = is->font = is->hot_font;
   3174 	Stream s = {.cap = countof(is->buf), .data = is->buf};
   3175 	stream_append_variable(&s, container->view.child);
   3176 	is->count = s.widx;
   3177 	is->container = container;
   3178 
   3179 	/* NOTE: extra offset to help with putting a cursor at idx 0 */
   3180 	#define TEXT_HALF_CHAR_WIDTH 10
   3181 	f32 hover_p = CLAMP01((mouse.x - r.pos.x) / r.size.w);
   3182 	f32 x_off = TEXT_HALF_CHAR_WIDTH, x_bounds = r.size.w * hover_p;
   3183 	i32 i;
   3184 	for (i = 0; i < is->count && x_off < x_bounds; i++) {
   3185 		/* NOTE: assumes font glyphs are ordered ASCII */
   3186 		i32 idx  = is->buf[i] - 0x20;
   3187 		x_off   += font->glyphs[idx].advanceX;
   3188 		if (font->glyphs[idx].advanceX == 0)
   3189 			x_off += font->recs[idx].width;
   3190 	}
   3191 	is->cursor = i;
   3192 }
   3193 
   3194 function void
   3195 end_text_input(InputState *is, Variable *var)
   3196 {
   3197 	f64 value = parse_f64((s8){.len = is->count, .data = is->buf});
   3198 
   3199 	switch (var->type) {
   3200 	case VT_SCALED_F32:{ var->scaled_real32.val = value; }break;
   3201 	case VT_F32:{        var->real32            = value; }break;
   3202 	case VT_BEAMFORMER_VARIABLE:{
   3203 		BeamformerVariable *bv = &var->beamformer_variable;
   3204 		*bv->store = CLAMP(value / bv->display_scale, bv->limits.x, bv->limits.y);
   3205 		var->hover_t = 0;
   3206 	}break;
   3207 	InvalidDefaultCase;
   3208 	}
   3209 }
   3210 
   3211 function b32
   3212 update_text_input(InputState *is, Variable *var)
   3213 {
   3214 	assert(is->cursor != -1);
   3215 
   3216 	is->cursor_blink_t += is->cursor_blink_scale * dt_for_frame;
   3217 	if (is->cursor_blink_t >= 1.0f) is->cursor_blink_scale = -BLINK_SPEED;
   3218 	if (is->cursor_blink_t <= 0.0f) is->cursor_blink_scale =  BLINK_SPEED;
   3219 
   3220 	var->hover_t -= 2 * HOVER_SPEED * dt_for_frame;
   3221 	var->hover_t  = CLAMP01(var->hover_t);
   3222 
   3223 	/* NOTE: handle multiple input keys on a single frame */
   3224 	for (i32 key = GetCharPressed();
   3225 	     is->count < countof(is->buf) && key > 0;
   3226 	     key = GetCharPressed())
   3227 	{
   3228 		b32 allow_key = (BETWEEN(key, '0', '9') || (key == '.') ||
   3229 		                 (key == '-' && is->cursor == 0));
   3230 		if (allow_key) {
   3231 			mem_move(is->buf + is->cursor + 1,
   3232 			         is->buf + is->cursor,
   3233 			         is->count - is->cursor);
   3234 			is->buf[is->cursor++] = key;
   3235 			is->count++;
   3236 		}
   3237 	}
   3238 
   3239 	is->cursor -= (IsKeyPressed(KEY_LEFT)  || IsKeyPressedRepeat(KEY_LEFT))  && is->cursor > 0;
   3240 	is->cursor += (IsKeyPressed(KEY_RIGHT) || IsKeyPressedRepeat(KEY_RIGHT)) && is->cursor < is->count;
   3241 
   3242 	if ((IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE)) && is->cursor > 0) {
   3243 		is->cursor--;
   3244 		if (is->cursor < countof(is->buf) - 1) {
   3245 			mem_move(is->buf + is->cursor,
   3246 			         is->buf + is->cursor + 1,
   3247 			         is->count - is->cursor - 1);
   3248 		}
   3249 		is->count--;
   3250 	}
   3251 
   3252 	if ((IsKeyPressed(KEY_DELETE) || IsKeyPressedRepeat(KEY_DELETE)) && is->cursor < is->count) {
   3253 		mem_move(is->buf + is->cursor,
   3254 		         is->buf + is->cursor + 1,
   3255 		         is->count - is->cursor - 1);
   3256 		is->count--;
   3257 	}
   3258 
   3259 	b32 result = IsKeyPressed(KEY_ENTER);
   3260 	return result;
   3261 }
   3262 
   3263 function void
   3264 scale_bar_interaction(BeamformerUI *ui, ScaleBar *sb, v2 mouse)
   3265 {
   3266 	Interaction *it = &ui->interaction;
   3267 	b32 mouse_left_pressed  = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
   3268 	b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT);
   3269 	f32 mouse_wheel         = GetMouseWheelMoveV().y;
   3270 
   3271 	if (mouse_left_pressed) {
   3272 		v2 world_mouse = screen_point_to_world_2d(mouse, it->rect.pos,
   3273 		                                          v2_add(it->rect.pos, it->rect.size),
   3274 		                                          (v2){{*sb->min_value, *sb->min_value}},
   3275 		                                          (v2){{*sb->max_value, *sb->max_value}});
   3276 		f32 new_coord = F32_INFINITY;
   3277 		switch (sb->direction) {
   3278 		case SB_LATERAL: new_coord = world_mouse.x; break;
   3279 		case SB_AXIAL:   new_coord = world_mouse.y; break;
   3280 		}
   3281 		if (sb->zoom_starting_coord == F32_INFINITY) {
   3282 			sb->zoom_starting_coord = new_coord;
   3283 		} else {
   3284 			f32 min = sb->zoom_starting_coord;
   3285 			f32 max = new_coord;
   3286 			if (min > max) swap(min, max);
   3287 
   3288 			v2_sll *savepoint = SLLPop(ui->scale_bar_savepoint_freelist);
   3289 			if (!savepoint) savepoint = push_struct(&ui->arena, v2_sll);
   3290 
   3291 			savepoint->v.x = *sb->min_value;
   3292 			savepoint->v.y = *sb->max_value;
   3293 			SLLPush(savepoint, sb->savepoint_stack);
   3294 
   3295 			*sb->min_value = min;
   3296 			*sb->max_value = max;
   3297 
   3298 			sb->zoom_starting_coord = F32_INFINITY;
   3299 		}
   3300 	}
   3301 
   3302 	if (mouse_right_pressed) {
   3303 		v2_sll *savepoint = sb->savepoint_stack;
   3304 		if (savepoint) {
   3305 			*sb->min_value      = savepoint->v.x;
   3306 			*sb->max_value      = savepoint->v.y;
   3307 			sb->savepoint_stack = savepoint->next;
   3308 			SLLPush(savepoint, ui->scale_bar_savepoint_freelist);
   3309 		}
   3310 		sb->zoom_starting_coord = F32_INFINITY;
   3311 	}
   3312 
   3313 	if (mouse_wheel) {
   3314 		*sb->min_value += mouse_wheel * sb->scroll_scale.x;
   3315 		*sb->max_value += mouse_wheel * sb->scroll_scale.y;
   3316 	}
   3317 }
   3318 
   3319 function void
   3320 ui_widget_bring_to_front(Variable *sentinal, Variable *widget)
   3321 {
   3322 	/* TODO(rnp): clean up the linkage so this can be a macro */
   3323 	widget->parent->next = widget->next;
   3324 	widget->next->parent = widget->parent;
   3325 
   3326 	widget->parent = sentinal;
   3327 	widget->next   = sentinal->next;
   3328 	widget->next->parent = widget;
   3329 	sentinal->next = widget;
   3330 }
   3331 
   3332 function void
   3333 ui_view_close(BeamformerUI *ui, Variable *view)
   3334 {
   3335 	switch (view->type) {
   3336 	case VT_UI_MENU:
   3337 	case VT_UI_TEXT_BOX:
   3338 	{
   3339 		UIView *fw = &view->view;
   3340 		if (view->type == VT_UI_MENU) {
   3341 			assert(fw->child->type == VT_GROUP);
   3342 			fw->child->group.expanded  = 0;
   3343 			fw->child->group.container = 0;
   3344 		} else {
   3345 			end_text_input(&ui->text_input_state, fw->child);
   3346 		}
   3347 		view->parent->next = view->next;
   3348 		view->next->parent = view->parent;
   3349 		if (fw->close) SLLPush(fw->close, ui->variable_freelist);
   3350 		SLLPush(view, ui->variable_freelist);
   3351 	}break;
   3352 	case VT_UI_VIEW:{
   3353 		assert(view->parent->type == VT_UI_REGION_SPLIT);
   3354 		Variable *region = view->parent;
   3355 
   3356 		Variable *parent    = region->parent;
   3357 		Variable *remaining = region->region_split.left;
   3358 		if (remaining == view) remaining = region->region_split.right;
   3359 
   3360 		ui_view_free(ui, view);
   3361 
   3362 		assert(parent->type == VT_UI_REGION_SPLIT);
   3363 		if (parent->region_split.left == region) {
   3364 			parent->region_split.left  = remaining;
   3365 		} else {
   3366 			parent->region_split.right = remaining;
   3367 		}
   3368 		remaining->parent = parent;
   3369 
   3370 		SLLPush(region, ui->variable_freelist);
   3371 	}break;
   3372 	InvalidDefaultCase;
   3373 	}
   3374 }
   3375 
   3376 function void
   3377 ui_button_interaction(BeamformerUI *ui, Variable *button)
   3378 {
   3379 	assert(button->type == VT_UI_BUTTON);
   3380 	switch (button->button) {
   3381 	case UI_BID_VIEW_CLOSE:{ ui_view_close(ui, button->parent); }break;
   3382 	case UI_BID_FV_COPY_HORIZONTAL:{
   3383 		ui_copy_frame(ui, button->parent->parent, RSD_HORIZONTAL);
   3384 	}break;
   3385 	case UI_BID_FV_COPY_VERTICAL:{
   3386 		ui_copy_frame(ui, button->parent->parent, RSD_VERTICAL);
   3387 	}break;
   3388 	case UI_BID_GM_OPEN_VIEW_RIGHT:{
   3389 		ui_add_live_frame_view(ui, button->parent->parent, RSD_HORIZONTAL, BeamformerFrameViewKind_Latest);
   3390 	}break;
   3391 	case UI_BID_GM_OPEN_VIEW_BELOW:{
   3392 		ui_add_live_frame_view(ui, button->parent->parent, RSD_VERTICAL, BeamformerFrameViewKind_Latest);
   3393 	}break;
   3394 	}
   3395 }
   3396 
   3397 function void
   3398 ui_begin_interact(BeamformerUI *ui, BeamformerInput *input, b32 scroll)
   3399 {
   3400 	Interaction hot = ui->hot_interaction;
   3401 	if (hot.kind != InteractionKind_None) {
   3402 		if (hot.kind == InteractionKind_Auto) {
   3403 			switch (hot.var->type) {
   3404 			case VT_NULL:{ hot.kind = InteractionKind_Nop; }break;
   3405 			case VT_B32:{ hot.kind  = InteractionKind_Set; }break;
   3406 			case VT_SCALE_BAR:{ hot.kind = InteractionKind_Set; }break;
   3407 			case VT_UI_BUTTON:{ hot.kind = InteractionKind_Button; }break;
   3408 			case VT_GROUP:{ hot.kind = InteractionKind_Set; }break;
   3409 			case VT_UI_TEXT_BOX:
   3410 			case VT_UI_MENU:
   3411 			{
   3412 				if (hot.var->type == VT_UI_MENU) {
   3413 					hot.kind = InteractionKind_Drag;
   3414 				} else {
   3415 					hot.kind = InteractionKind_Text;
   3416 					begin_text_input(&ui->text_input_state, hot.rect, hot.var, input->mouse);
   3417 				}
   3418 				ui_widget_bring_to_front(&ui->floating_widget_sentinal, hot.var);
   3419 			}break;
   3420 			case VT_UI_VIEW:{
   3421 				if (scroll) hot.kind = InteractionKind_Scroll;
   3422 				else        hot.kind = InteractionKind_Nop;
   3423 			}break;
   3424 			case VT_X_PLANE_SHIFT:{
   3425 				assert(hot.var->parent && hot.var->parent->type == VT_BEAMFORMER_FRAME_VIEW);
   3426 				BeamformerFrameView *bv = hot.var->parent->generic;
   3427 				if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
   3428 					XPlaneShift *xp = &hot.var->x_plane_shift;
   3429 					xp->start_point = xp->end_point = bv->hit_test_point;
   3430 					hot.kind = InteractionKind_Drag;
   3431 				} else {
   3432 					if (scroll) {
   3433 						hot.kind = InteractionKind_Scroll;
   3434 						hot.var  = &bv->threshold;
   3435 					} else {
   3436 						hot.kind = InteractionKind_Nop;
   3437 					}
   3438 				}
   3439 			}break;
   3440 			case VT_BEAMFORMER_FRAME_VIEW:{
   3441 				if (scroll) {
   3442 					hot.kind = InteractionKind_Scroll;
   3443 				} else {
   3444 					BeamformerFrameView *bv = hot.var->generic;
   3445 					switch (bv->kind) {
   3446 					case BeamformerFrameViewKind_3DXPlane:{ hot.kind = InteractionKind_Drag; }break;
   3447 					default:{
   3448 						hot.kind = InteractionKind_Nop;
   3449 						switch (++bv->ruler.state) {
   3450 						case RulerState_Start:{
   3451 							hot.kind = InteractionKind_Ruler;
   3452 							v2 r_max = v2_add(hot.rect.pos, hot.rect.size);
   3453 							v2 p = screen_point_to_world_2d(input->mouse, hot.rect.pos, r_max,
   3454 							                                XZ(bv->min_coordinate),
   3455 							                                XZ(bv->max_coordinate));
   3456 							bv->ruler.start = p;
   3457 						}break;
   3458 						case RulerState_Hold:{}break;
   3459 						default:{ bv->ruler.state = RulerState_None; }break;
   3460 						}
   3461 					}break;
   3462 					}
   3463 				}
   3464 			}break;
   3465 			case VT_CYCLER:{
   3466 				if (scroll) hot.kind = InteractionKind_Scroll;
   3467 				else        hot.kind = InteractionKind_Set;
   3468 			}break;
   3469 			case VT_BEAMFORMER_VARIABLE:
   3470 			case VT_F32:
   3471 			case VT_SCALED_F32:
   3472 			{
   3473 				if (scroll) {
   3474 					hot.kind = InteractionKind_Scroll;
   3475 				} else if (hot.var->flags & V_TEXT) {
   3476 					hot.kind = InteractionKind_Text;
   3477 					Variable *w = add_floating_view(ui, &ui->arena, VT_UI_TEXT_BOX,
   3478 					                                hot.rect.pos, hot.var, 0);
   3479 					w->view.rect = hot.rect;
   3480 					begin_text_input(&ui->text_input_state, hot.rect, w, input->mouse);
   3481 				} else {
   3482 					hot.kind = InteractionKind_Drag;
   3483 				}
   3484 			}break;
   3485 			InvalidDefaultCase;
   3486 			}
   3487 		}
   3488 
   3489 		ui->interaction = hot;
   3490 
   3491 		if (ui->interaction.var->flags & V_HIDES_CURSOR) {
   3492 			HideCursor();
   3493 			DisableCursor();
   3494 			/* wtf raylib */
   3495 			SetMousePosition(input->mouse.x, input->mouse.y);
   3496 		}
   3497 	} else {
   3498 		ui->interaction.kind = InteractionKind_Nop;
   3499 	}
   3500 }
   3501 
   3502 function void
   3503 ui_extra_actions(BeamformerUI *ui, Variable *var)
   3504 {
   3505 	switch (var->type) {
   3506 	case VT_CYCLER:{
   3507 		assert(var->parent && var->parent->parent && var->parent->parent->type == VT_UI_VIEW);
   3508 		Variable *view_var = var->parent->parent;
   3509 		UIView   *view     = &view_var->view;
   3510 		switch (view->child->type) {
   3511 		case VT_BEAMFORMER_FRAME_VIEW:{
   3512 			BeamformerFrameView *old = view->child->generic;
   3513 			BeamformerFrameView *new = view->child->generic = ui_beamformer_frame_view_new(ui, &ui->arena);
   3514 			BeamformerFrameViewKind last_kind = (old->kind - 1) % BeamformerFrameViewKind_Count;
   3515 
   3516 			/* NOTE(rnp): log_scale gets released below before its needed */
   3517 			b32 log_scale = old->log_scale->bool32;
   3518 			ui_variable_free_group_items(ui, view->menu);
   3519 
   3520 			ui_beamformer_frame_view_release_subresources(ui, old, last_kind);
   3521 			ui_beamformer_frame_view_convert(ui, &ui->arena, view->child, view->menu, old->kind, old, log_scale);
   3522 			if (new->kind == BeamformerFrameViewKind_Copy && old->frame)
   3523 				ui_beamformer_frame_view_copy_frame(ui, new, old);
   3524 
   3525 			DLLRemove(old);
   3526 			SLLPush(old, ui->view_freelist);
   3527 		}break;
   3528 		InvalidDefaultCase;
   3529 		}
   3530 	}break;
   3531 	InvalidDefaultCase;
   3532 	}
   3533 }
   3534 
   3535 function void
   3536 ui_live_control_update(BeamformerUI *ui, Variable *controls)
   3537 {
   3538 	assert(controls->type == VT_LIVE_CONTROLS_VIEW);
   3539 	BeamformerSharedMemory *sm = ui->shared_memory.region;
   3540 	BeamformerLiveControlsView *lv = controls->generic;
   3541 	atomic_or_u32(&sm->live_imaging_dirty_flags, lv->hot_field_flag);
   3542 }
   3543 
   3544 function void
   3545 ui_end_interact(BeamformerUI *ui, v2 mouse)
   3546 {
   3547 	Interaction *it = &ui->interaction;
   3548 	b32 start_compute = (it->var->flags & V_CAUSES_COMPUTE) != 0;
   3549 	switch (it->kind) {
   3550 	case InteractionKind_Nop:{}break;
   3551 	case InteractionKind_Drag:{
   3552 		switch (it->var->type) {
   3553 		case VT_X_PLANE_SHIFT:{
   3554 			assert(it->var->parent && it->var->parent->type == VT_BEAMFORMER_FRAME_VIEW);
   3555 			XPlaneShift *xp = &it->var->x_plane_shift;
   3556 			BeamformerFrameView *view = it->var->parent->generic;
   3557 			BeamformerViewPlaneTag plane = view_plane_tag_from_x_plane_shift(view, it->var);
   3558 			f32 rotation  = x_plane_rotation_for_view_plane(view, plane);
   3559 			m4 x_rotation = m4_rotation_about_y(rotation);
   3560 			v3 Z = x_rotation.c[2].xyz;
   3561 			f32 delta = v3_dot(Z, v3_sub(xp->end_point, xp->start_point));
   3562 			xp->start_point = xp->end_point;
   3563 
   3564 			BeamformerSharedMemory          *sm = ui->shared_memory.region;
   3565 			BeamformerLiveImagingParameters *li = &sm->live_imaging_parameters;
   3566 			li->image_plane_offsets[plane] += delta;
   3567 			atomic_or_u32(&sm->live_imaging_dirty_flags, BeamformerLiveImagingDirtyFlags_ImagePlaneOffsets);
   3568 		}break;
   3569 		default:{}break;
   3570 		}
   3571 	}break;
   3572 	case InteractionKind_Set:{
   3573 		switch (it->var->type) {
   3574 		case VT_B32:{ it->var->bool32 = !it->var->bool32; }break;
   3575 		case VT_GROUP:{ it->var->group.expanded = !it->var->group.expanded; }break;
   3576 		case VT_SCALE_BAR:{ scale_bar_interaction(ui, &it->var->scale_bar, mouse); }break;
   3577 		case VT_CYCLER:{
   3578 			*it->var->cycler.state += 1;
   3579 			*it->var->cycler.state %= it->var->cycler.cycle_length;
   3580 		}break;
   3581 		InvalidDefaultCase;
   3582 		}
   3583 	}break;
   3584 	case InteractionKind_Menu:{
   3585 		assert(it->var->type == VT_GROUP);
   3586 		VariableGroup *g = &it->var->group;
   3587 		if (g->container) {
   3588 			ui_widget_bring_to_front(&ui->floating_widget_sentinal, g->container);
   3589 		} else {
   3590 			g->container = add_floating_view(ui, &ui->arena, VT_UI_MENU, mouse, it->var, 1);
   3591 		}
   3592 	}break;
   3593 	case InteractionKind_Ruler:{
   3594 		assert(it->var->type == VT_BEAMFORMER_FRAME_VIEW);
   3595 		((BeamformerFrameView *)it->var->generic)->ruler.state = RulerState_None;
   3596 	}break;
   3597 	case InteractionKind_Button:{ ui_button_interaction(ui, it->var); }break;
   3598 	case InteractionKind_Scroll:{ scroll_interaction(it->var, GetMouseWheelMoveV().y); }break;
   3599 	case InteractionKind_Text:{ ui_view_close(ui, ui->text_input_state.container); }break;
   3600 	InvalidDefaultCase;
   3601 	}
   3602 
   3603 	if (start_compute) ui->flush_params = 1;
   3604 	if (it->var->flags & V_UPDATE_VIEW) {
   3605 		Variable *parent = it->var->parent;
   3606 		BeamformerFrameView *frame = parent->generic;
   3607 		/* TODO(rnp): more straight forward way of achieving this */
   3608 		if (parent->type != VT_BEAMFORMER_FRAME_VIEW) {
   3609 			assert(parent->parent->type == VT_UI_VIEW);
   3610 			assert(parent->parent->view.child->type == VT_BEAMFORMER_FRAME_VIEW);
   3611 			frame = parent->parent->view.child->generic;
   3612 		}
   3613 		frame->dirty = 1;
   3614 	}
   3615 
   3616 	if (it->var->flags & V_LIVE_CONTROL)
   3617 		ui_live_control_update(ui, it->var->parent);
   3618 
   3619 	if (it->var->flags & V_HIDES_CURSOR)
   3620 		EnableCursor();
   3621 
   3622 	if (it->var->flags & V_EXTRA_ACTION)
   3623 		ui_extra_actions(ui, it->var);
   3624 
   3625 	ui->interaction = (Interaction){.kind = InteractionKind_None};
   3626 }
   3627 
   3628 function void
   3629 ui_sticky_interaction_check_end(BeamformerUI *ui, v2 mouse)
   3630 {
   3631 	Interaction *it = &ui->interaction;
   3632 	switch (it->kind) {
   3633 	case InteractionKind_Ruler:{
   3634 		if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) || !point_in_rect(mouse, it->rect))
   3635 			ui_end_interact(ui, mouse);
   3636 	}break;
   3637 	case InteractionKind_Text:{
   3638 		Interaction text_box = auto_interaction({{0}}, ui->text_input_state.container);
   3639 		if (!interactions_equal(text_box, ui->hot_interaction))
   3640 			ui_end_interact(ui, mouse);
   3641 	}break;
   3642 	InvalidDefaultCase;
   3643 	}
   3644 }
   3645 
   3646 function void
   3647 ui_interact(BeamformerUI *ui, BeamformerInput *input, Rect window_rect)
   3648 {
   3649 	Interaction *it = &ui->interaction;
   3650 	if (it->kind == InteractionKind_None || interaction_is_sticky(*it)) {
   3651 		ui->hot_interaction = ui->next_interaction;
   3652 
   3653 		b32 mouse_left_pressed  = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
   3654 		b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT);
   3655 		b32 wheel_moved         = GetMouseWheelMoveV().y != 0;
   3656 		if (mouse_right_pressed || mouse_left_pressed || wheel_moved) {
   3657 			if (it->kind != InteractionKind_None)
   3658 				ui_sticky_interaction_check_end(ui, input->mouse);
   3659 			ui_begin_interact(ui, input, wheel_moved);
   3660 		}
   3661 	}
   3662 
   3663 	switch (it->kind) {
   3664 	case InteractionKind_Nop:{ it->kind = InteractionKind_None; }break;
   3665 	case InteractionKind_None:{}break;
   3666 	case InteractionKind_Text:{
   3667 		if (update_text_input(&ui->text_input_state, it->var))
   3668 			ui_end_interact(ui, input->mouse);
   3669 	}break;
   3670 	case InteractionKind_Ruler:{
   3671 		assert(it->var->type == VT_BEAMFORMER_FRAME_VIEW);
   3672 		BeamformerFrameView *bv = it->var->generic;
   3673 		v2 r_max = v2_add(it->rect.pos, it->rect.size);
   3674 		v2 mouse = clamp_v2_rect(input->mouse, it->rect);
   3675 		bv->ruler.end = screen_point_to_world_2d(mouse, it->rect.pos, r_max,
   3676 		                                         XZ(bv->min_coordinate),
   3677 		                                         XZ(bv->max_coordinate));
   3678 	}break;
   3679 	case InteractionKind_Drag:{
   3680 		if (!IsMouseButtonDown(MOUSE_BUTTON_LEFT) && !IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) {
   3681 			ui_end_interact(ui, input->mouse);
   3682 		} else {
   3683 			v2 ws     = window_rect.size;
   3684 			v2 dMouse = v2_sub(input->mouse, input->last_mouse);
   3685 
   3686 			switch (it->var->type) {
   3687 			case VT_BEAMFORMER_VARIABLE:{
   3688 				BeamformerVariable *bv = &it->var->beamformer_variable;
   3689 				/* TODO(rnp): vertical sliders? */
   3690 				f32 mouse_frac = CLAMP01((input->mouse.x - it->rect.pos.x) / it->rect.size.w);
   3691 				*bv->store     = bv->limits.x + mouse_frac * (bv->limits.y - bv->limits.x);
   3692 			}break;
   3693 			case VT_X_PLANE_SHIFT:{
   3694 				assert(it->var->parent && it->var->parent->type == VT_BEAMFORMER_FRAME_VIEW);
   3695 				v2 mouse = clamp_v2_rect(input->mouse, it->rect);
   3696 				XPlaneShift *xp = &it->var->x_plane_shift;
   3697 				ray mouse_ray = ray_for_x_plane_view(ui, it->var->parent->generic,
   3698 				                                     normalized_p_in_rect(it->rect, mouse, 0));
   3699 				/* NOTE(rnp): project start point onto ray */
   3700 				v3 s = v3_sub(xp->start_point, mouse_ray.origin);
   3701 				v3 r = v3_sub(mouse_ray.direction, mouse_ray.origin);
   3702 				f32 scale     = v3_dot(s, r) / v3_magnitude_squared(r);
   3703 				xp->end_point = v3_add(mouse_ray.origin, v3_scale(r, scale));
   3704 			}break;
   3705 			case VT_BEAMFORMER_FRAME_VIEW:{
   3706 				BeamformerFrameView *bv = it->var->generic;
   3707 				switch (bv->kind) {
   3708 				case BeamformerFrameViewKind_3DXPlane:{
   3709 					bv->rotation += dMouse.x / ws.w;
   3710 					if (bv->rotation > 1.0f) bv->rotation -= 1.0f;
   3711 					if (bv->rotation < 0.0f) bv->rotation += 1.0f;
   3712 				}break;
   3713 				InvalidDefaultCase;
   3714 				}
   3715 			}break;
   3716 			case VT_UI_MENU:{
   3717 				v2 *pos = &ui->interaction.var->view.rect.pos;
   3718 				*pos = clamp_v2_rect(v2_add(*pos, dMouse), window_rect);
   3719 			}break;
   3720 			case VT_UI_REGION_SPLIT:{
   3721 				f32 min_fraction = 0;
   3722 				dMouse = v2_mul(dMouse, (v2){{1.0f / ws.w, 1.0f / ws.h}});
   3723 				RegionSplit *rs = &ui->interaction.var->region_split;
   3724 				switch (rs->direction) {
   3725 				case RSD_VERTICAL: {
   3726 					min_fraction  = (UI_SPLIT_HANDLE_THICK + 0.5 * UI_REGION_PAD) / ws.h;
   3727 					rs->fraction += dMouse.y;
   3728 				} break;
   3729 				case RSD_HORIZONTAL: {
   3730 					min_fraction  = (UI_SPLIT_HANDLE_THICK + 0.5 * UI_REGION_PAD) / ws.w;
   3731 					rs->fraction += dMouse.x;
   3732 				} break;
   3733 				}
   3734 				rs->fraction = CLAMP(rs->fraction, min_fraction, 1 - min_fraction);
   3735 			}break;
   3736 			default:{}break;
   3737 			}
   3738 			if (it->var->flags & V_LIVE_CONTROL)
   3739 				ui_live_control_update(ui, it->var->parent);
   3740 		}
   3741 	} break;
   3742 	default:{ ui_end_interact(ui, input->mouse); }break;
   3743 	}
   3744 
   3745 	ui->next_interaction = (Interaction){.kind = InteractionKind_None};
   3746 }
   3747 
   3748 function void
   3749 ui_init(BeamformerCtx *ctx, Arena store)
   3750 {
   3751 	BeamformerUI *ui = ctx->ui;
   3752 	if (!ui) {
   3753 		ui = ctx->ui = push_struct(&store, typeof(*ui));
   3754 		ui->arena = store;
   3755 		ui->frame_view_render_context = &ctx->frame_view_render_context;
   3756 		ui->unit_cube_model = ctx->csctx.unit_cube_model;
   3757 		ui->shared_memory   = ctx->shared_memory;
   3758 		ui->beamformer_context = ctx;
   3759 
   3760 		/* TODO(rnp): build these into the binary */
   3761 		/* TODO(rnp): better font, this one is jank at small sizes */
   3762 		ui->font       = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 28, 0, 0);
   3763 		ui->small_font = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 20, 0, 0);
   3764 
   3765 		ui->floating_widget_sentinal.parent = &ui->floating_widget_sentinal;
   3766 		ui->floating_widget_sentinal.next   = &ui->floating_widget_sentinal;
   3767 
   3768 		Variable *split = ui->regions = add_ui_split(ui, 0, &ui->arena, s8("UI Root"), 0.36,
   3769 		                                             RSD_HORIZONTAL, ui->font);
   3770 		split->region_split.left = add_ui_split(ui, split, &ui->arena, s8(""), 0.475,
   3771 		                                        RSD_VERTICAL, ui->font);
   3772 
   3773 		split = split->region_split.right = add_ui_split(ui, split, &ui->arena, s8(""), 0.70,
   3774 		                                                 RSD_HORIZONTAL, ui->font);
   3775 		{
   3776 			split->region_split.left  = add_beamformer_frame_view(ui, split, &ui->arena,
   3777 			                                                      BeamformerFrameViewKind_Latest, 0, 0);
   3778 			split->region_split.right = add_live_controls_view(ui, split, &ui->arena);
   3779 		}
   3780 		split = split->parent;
   3781 
   3782 		split = split->region_split.left;
   3783 		split->region_split.left  = add_beamformer_parameters_view(split, ctx);
   3784 		split->region_split.right = add_ui_split(ui, split, &ui->arena, s8(""), 0.22,
   3785 		                                         RSD_VERTICAL, ui->font);
   3786 		split = split->region_split.right;
   3787 
   3788 		split->region_split.left  = add_compute_progress_bar(split, ctx);
   3789 		split->region_split.right = add_compute_stats_view(ui, split, &ui->arena, ctx);
   3790 
   3791 		ctx->ui_read_params = 1;
   3792 
   3793 		/* NOTE(rnp): shrink variable size once this fires */
   3794 		assert(ui->arena.beg - (u8 *)ui < KB(64));
   3795 	}
   3796 }
   3797 
   3798 function void
   3799 validate_ui_parameters(BeamformerUIParameters *p)
   3800 {
   3801 	if (p->output_min_coordinate[0] > p->output_max_coordinate[0])
   3802 		swap(p->output_min_coordinate[0], p->output_max_coordinate[0]);
   3803 	if (p->output_min_coordinate[2] > p->output_max_coordinate[2])
   3804 		swap(p->output_min_coordinate[2], p->output_max_coordinate[2]);
   3805 }
   3806 
   3807 function void
   3808 draw_ui(BeamformerCtx *ctx, BeamformerInput *input, BeamformerFrame *frame_to_draw, BeamformerViewPlaneTag frame_plane)
   3809 {
   3810 	BeamformerUI *ui = ctx->ui;
   3811 	BeamformerSharedMemory *sm = ctx->shared_memory.region;
   3812 
   3813 	ui->latest_plane[BeamformerViewPlaneTag_Count] = frame_to_draw;
   3814 	ui->latest_plane[frame_plane]                  = frame_to_draw;
   3815 
   3816 	/* TODO(rnp): there should be a better way of detecting this */
   3817 	if (ctx->ui_read_params) {
   3818 		mem_copy(&ui->params, &sm->parameters.output_min_coordinate, sizeof(ui->params));
   3819 		ui->flush_params    = 0;
   3820 		ctx->ui_read_params = 0;
   3821 	}
   3822 
   3823 	/* NOTE: process interactions first because the user interacted with
   3824 	 * the ui that was presented last frame */
   3825 	Rect window_rect = {.size = {.w = ctx->window_size.w, .h = ctx->window_size.h}};
   3826 	ui_interact(ui, input, window_rect);
   3827 
   3828 	if (ui->flush_params) {
   3829 		i32 lock = BeamformerSharedMemoryLockKind_Parameters;
   3830 		validate_ui_parameters(&ui->params);
   3831 		if (ctx->os.shared_memory_region_lock(&ctx->shared_memory, sm->locks, lock, 0)) {
   3832 			mem_copy(&sm->parameters_ui, &ui->params, sizeof(ui->params));
   3833 			ui->flush_params = 0;
   3834 			atomic_or_u32(&sm->dirty_regions, (1 << (lock - 1)));
   3835 			b32 dispatch = ctx->os.shared_memory_region_lock(&ctx->shared_memory, sm->locks,
   3836 			                                                 BeamformerSharedMemoryLockKind_DispatchCompute,
   3837 			                                                 0);
   3838 			sm->start_compute_from_main |= dispatch & ctx->latest_frame->ready_to_present;
   3839 			ctx->os.shared_memory_region_unlock(&ctx->shared_memory, sm->locks, lock);
   3840 		}
   3841 	}
   3842 
   3843 	/* NOTE(rnp): can't render to a different framebuffer in the middle of BeginDrawing()... */
   3844 	update_frame_views(ui, window_rect);
   3845 
   3846 	BeginDrawing();
   3847 		f32 one = 1;
   3848 		glClearNamedFramebufferfv(0, GL_COLOR, 0, BG_COLOUR.E);
   3849 		glClearNamedFramebufferfv(0, GL_DEPTH, 0, &one);
   3850 
   3851 		draw_ui_regions(ui, window_rect, input->mouse);
   3852 		draw_floating_widgets(ui, window_rect, input->mouse);
   3853 	EndDrawing();
   3854 }