ogl_beamforming

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

ui.c (152918B)


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