ogl_beamforming

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

ui.c (144342B)


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