ogl_beamforming

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

ui.c (149530B)


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