ogl_beamforming

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

ui.c (182515B)


      1 /* See LICENSE for license details. */
      2 /* TODO(rnp):
      3  * [ ]: bug: flickering x-scroll bar on switch from ComputeBarGraph to other
      4  * [ ]: word scan for text input
      5  * [ ]: animation state
      6  * [ ]: tooltips
      7  * [ ]: extra copy view settings
      8  * [ ]: refactor: all drag overlay floating elements can be children of the drag_root.
      9  *      as long as we layout before chaining them on there won't be an issue.
     10  * [ ]: refactor: can the scroll container just use the ViewScroll flags like the tab bar?
     11  * [ ]: refactor: it would be nice to have some table building helpers
     12  *
     13  * [ ]: refactor: cross plane view for non XZ/YZ planes. math needs to be cleaned up
     14  *      to support this.
     15  *  - model transform needs to first rotate so that Z is normal, then scale, the rotate from Z to Y.
     16  *  - ideally the hardcoded +0.25f rotation for YZ should just be a consequence of the math
     17  * [ ]: command window
     18  * [ ]: 3D data view
     19  *    - add extra view controls, change view without recompute
     20  *      - to start just have starting plane/normal, plane uvs, rotation, and offset
     21  *    - confirmation on recompute
     22  * [ ]: rich highlighting for parameters -> X-Plane link
     23  *
     24  * [ ]: multi-os windows
     25  * [ ]: ui color configuration at runtime
     26  */
     27 
     28 #include "assets/generated/assets.c"
     29 
     30 #define NIL_COLOUR             (v4){{0.76f, 0.00f, 0.65f, 1.0f}}
     31 #define BG_COLOUR              (v4){{0.15f, 0.12f, 0.13f, 1.0f}}
     32 #define FG_COLOUR              (v4){{0.92f, 0.88f, 0.78f, 1.0f}}
     33 #define FOCUSED_COLOUR         (v4){{0.86f, 0.28f, 0.21f, 1.0f}}
     34 #define HOVERED_COLOUR         (v4){{0.11f, 0.50f, 0.59f, 1.0f}}
     35 #define SELECTION_COLOUR       (v4){{0.07f, 0.37f, 0.90f, 0.5f}}
     36 #define RULER_COLOUR           (v4){{1.00f, 0.70f, 0.00f, 1.0f}}
     37 #define BORDER_COLOUR          v4_lerp(FG_COLOUR, BG_COLOUR, 0.85f)
     38 #define NODE_SPLIT_COLOUR      (v4){{0.6f, 0.6f, 0.6f, 0.5f}}
     39 
     40 #define FRAME_VIEW_BB_COLOUR          (v4){{0.92f, 0.88f, 0.78f, 1.0f}}
     41 #define FRAME_VIEW_BB_FRACTION        0.007f
     42 #define FRAME_VIEW_RENDER_TARGET_SIZE 1024, 1024
     43 
     44 #define MENU_PLUS_COLOUR       (v4){{0.33f, 0.42f, 1.00f, 1.00f}}
     45 #define MENU_CLOSE_COLOUR      FOCUSED_COLOUR
     46 
     47 #define UI_NODE_PAD         8.f
     48 #define UI_BORDER_THICK     4.f
     49 
     50 #define UI_HASH_TABLE_COUNT 4096
     51 
     52 read_only global v4 g_colour_palette[] = {
     53 	{{0.32f, 0.20f, 0.50f, 1.00f}},
     54 	{{0.14f, 0.39f, 0.61f, 1.00f}},
     55 	{{0.61f, 0.14f, 0.25f, 1.00f}},
     56 	{{0.20f, 0.60f, 0.24f, 1.00f}},
     57 	{{0.80f, 0.60f, 0.20f, 1.00f}},
     58 	{{0.15f, 0.51f, 0.74f, 1.00f}},
     59 };
     60 
     61 #define HOVER_SPEED            5.0f
     62 #define BLINK_SPEED            1.5f
     63 
     64 #define TABLE_CELL_PAD_HEIGHT  2.0f
     65 #define TABLE_CELL_PAD_WIDTH   8.0f
     66 
     67 #define RULER_TEXT_PAD          6.0f
     68 #define RULER_TICK_LENGTH      20.0f
     69 
     70 #define UI_SPLIT_HANDLE_THICK  5.0f
     71 #define UI_REGION_PAD          32.0f
     72 
     73 /* TODO(rnp) smooth scroll */
     74 #define UI_SCROLL_SPEED 12.0f
     75 
     76 #define LISTING_LINE_PAD    6.0f
     77 #define TITLE_BAR_PAD       6.0f
     78 
     79 typedef enum {
     80 	UINodeFlag_MouseClickable            = 1ull << 0,
     81 	UINodeFlag_KeyboardClickable         = 1ull << 1,
     82 	UINodeFlag_DropSite                  = 1ull << 2,
     83 	UINodeFlag_ClickToFocus              = 1ull << 3,
     84 	UINodeFlag_Scroll                    = 1ull << 4,
     85 	UINodeFlag_FocusHot                  = 1ull << 5,
     86 	UINodeFlag_FocusActive               = 1ull << 6,
     87 	UINodeFlag_FocusHotDisabled          = 1ull << 7,
     88 	UINodeFlag_FocusActiveDisabled       = 1ull << 8,
     89 	UINodeFlag_Disabled                  = 1ull << 9,
     90 
     91 	UINodeFlag_FloatingX                 = 1ull << 10,
     92 	UINodeFlag_FloatingY                 = 1ull << 11,
     93 	UINodeFlag_FixedWidth                = 1ull << 12,
     94 	UINodeFlag_FixedHeight               = 1ull << 13,
     95 	UINodeFlag_AllowOverflowX            = 1ull << 14,
     96 	UINodeFlag_AllowOverflowY            = 1ull << 15,
     97 
     98 	// NOTE(rnp): for scrollable containers
     99 	UINodeFlag_ViewScrollX               = 1ull << 16,
    100 	UINodeFlag_ViewScrollY               = 1ull << 17,
    101 
    102 	UINodeFlag_DrawDropShadow            = 1ull << 18,
    103 	UINodeFlag_DrawBackgroundBlur        = 1ull << 19,
    104 	UINodeFlag_DrawBackground            = 1ull << 20,
    105 	UINodeFlag_DrawBorder                = 1ull << 21,
    106 	UINodeFlag_DrawText                  = 1ull << 22,
    107 	UINodeFlag_DrawHotEffects            = 1ull << 23,
    108 	UINodeFlag_DrawActiveEffects         = 1ull << 24,
    109 	UINodeFlag_DrawOverlay               = 1ull << 25,
    110 	UINodeFlag_Clip                      = 1ull << 26,
    111 	UINodeFlag_DisableTextTrunc          = 1ull << 27,
    112 	UINodeFlag_DisableFocusBorder        = 1ull << 28,
    113 	UINodeFlag_DisableFocusOverlay       = 1ull << 29,
    114 
    115 	UINodeFlag_TextInput                 = 1ull << 30,
    116 	UINodeFlag_TextInputNumeric          = 1ull << 31,
    117 	UINodeFlag_TextInputClearOnStart     = 1ull << 32,
    118 
    119 	UINodeFlag_CustomDraw                = 1ull << 33,
    120 
    121 	// TODO(rnp): hack: when text is not drawn with raylib do something smarter
    122 	UINodeFlag_IconText                  = 1ull << 34,
    123 
    124 	UINodeFlag_Clickable           = UINodeFlag_MouseClickable|UINodeFlag_KeyboardClickable,
    125 	UINodeFlag_Floating            = UINodeFlag_FloatingX|UINodeFlag_FloatingY,
    126 	UINodeFlag_FixedSize           = UINodeFlag_FixedWidth|UINodeFlag_FixedHeight,
    127 	UINodeFlag_AllowOverflow       = UINodeFlag_AllowOverflowX|UINodeFlag_AllowOverflowY,
    128 	UINodeFlag_DisableFocusEffects = UINodeFlag_DisableFocusBorder|UINodeFlag_DisableFocusOverlay,
    129 	UINodeFlag_ViewScroll          = UINodeFlag_ViewScrollX|UINodeFlag_ViewScrollY,
    130 } UINodeFlags;
    131 
    132 typedef struct UINodeFlagsNode UINodeFlagsNode;
    133 struct UINodeFlagsNode {UINodeFlagsNode *next; UINodeFlags v;};
    134 
    135 typedef struct Axis2Node Axis2Node;
    136 struct Axis2Node {Axis2Node *next; Axis2 v;};
    137 
    138 typedef enum {
    139 	UISizeKind_Nil,
    140 	UISizeKind_Pixels,
    141 	UISizeKind_TextContent,
    142 	UISizeKind_PercentOfParent,
    143 	UISizeKind_ChildrenSum,
    144 } UISizeKind;
    145 
    146 typedef struct {
    147 	UISizeKind kind;
    148 	f32        value;
    149 	f32        strictness;
    150 } UISize;
    151 
    152 typedef struct UISizeNode UISizeNode;
    153 struct UISizeNode {UISizeNode *next; UISize v;};
    154 
    155 typedef enum {
    156 	UIAlign_Left,
    157 	UIAlign_Right,
    158 	UIAlign_Center,
    159 	UIAlign_Count,
    160 } UIAlign;
    161 
    162 typedef struct UIAlignNode UIAlignNode;
    163 struct UIAlignNode {UIAlignNode *next; UIAlign v;};
    164 
    165 typedef struct {u64 value;} UINodeKey;
    166 
    167 typedef struct UINode UINode;
    168 
    169 #define UI_CUSTOM_DRAW_FUNCTION(name) void name(UINode *node, Rect node_rect)
    170 typedef UI_CUSTOM_DRAW_FUNCTION(UICustomDrawFunction);
    171 
    172 struct UINode {
    173 	UINode *parent;
    174 	UINode *first_child;
    175 	UINode *last_child;
    176 	UINode *previous_sibling;
    177 	UINode *next_sibling;
    178 
    179 	u32     child_count;
    180 
    181 	UINodeFlags flags;
    182 	str8        string;
    183 	// NOTE(rnp): desired sizing info from build step
    184 	union {
    185 		struct {
    186 			UISize semantic_width;
    187 			UISize semantic_height;
    188 		};
    189 		UISize semantic_size[Axis2_Count];
    190 	};
    191 
    192 	union {
    193 		struct {
    194 			UIAlign alignment_x;
    195 			UIAlign alignment_y;
    196 		};
    197 		UIAlign alignment[Axis2_Count];
    198 	};
    199 
    200 	UIAlign    text_alignment;
    201 
    202 	Axis2      child_layout_axis;
    203 	f32        font_size;
    204 
    205 	u64        last_frame_active_index;
    206 	UINodeKey  key;
    207 	UINode    *hash_prev;
    208 	UINode    *hash_next;
    209 
    210 	// NOTE(rnp): recomputed every frame before drawing. also
    211 	// used on next frame for mouse collision detection.
    212 	f32  computed_position[Axis2_Count];
    213 	f32  computed_size[Axis2_Count];
    214 
    215 	v2   text_size;
    216 
    217 	// NOTE(rnp): persistent data
    218 	f32 active_t;
    219 	f32 hot_t;
    220 
    221 	v2  view_scroll_offset;
    222 
    223 	v4  bg_colour;
    224 
    225 	v4  text_colour;
    226 	v4  text_outline_colour;
    227 	f32 text_outline_thickness;
    228 
    229 	v4  border_colour;
    230 	f32 border_thickness;
    231 
    232 	UICustomDrawFunction *custom_draw_function;
    233 	void                 *custom_draw_context;
    234 };
    235 
    236 typedef struct {UINode *first, *last;} UINodeHashBucket;
    237 
    238 typedef struct UIParentNode UIParentNode;
    239 struct UIParentNode {UIParentNode *next; UINode *v;};
    240 
    241 typedef enum {
    242 	UIMouseButtonKind_Left,
    243 	UIMouseButtonKind_Middle,
    244 	UIMouseButtonKind_Right,
    245 	UIMouseButtonKind_Count,
    246 } UIMouseButtonKind;
    247 
    248 typedef enum {
    249 	UISignalFlag_LeftPressed          = (1 << 0),
    250 	UISignalFlag_MiddlePressed        = (1 << 1),
    251 	UISignalFlag_RightPressed         = (1 << 2),
    252 
    253 	UISignalFlag_LeftDragging         = (1 << 3),
    254 	UISignalFlag_MiddleDragging       = (1 << 4),
    255 	UISignalFlag_RightDragging        = (1 << 5),
    256 
    257 	UISignalFlag_LeftDoubleDragging   = (1 << 6),
    258 	UISignalFlag_MiddleDoubleDragging = (1 << 7),
    259 	UISignalFlag_RightDoubleDragging  = (1 << 8),
    260 
    261 	UISignalFlag_LeftTripleDragging   = (1 << 9),
    262 	UISignalFlag_MiddleTripleDragging = (1 << 10),
    263 	UISignalFlag_RightTripleDragging  = (1 << 11),
    264 
    265 	UISignalFlag_LeftReleased         = (1 << 12),
    266 	UISignalFlag_MiddleReleased       = (1 << 13),
    267 	UISignalFlag_RightReleased        = (1 << 14),
    268 
    269 	UISignalFlag_LeftClicked          = (1 << 15),
    270 	UISignalFlag_MiddleClicked        = (1 << 16),
    271 	UISignalFlag_RightClicked         = (1 << 17),
    272 
    273 	UISignalFlag_LeftDoubleClicked    = (1 << 18),
    274 	UISignalFlag_MiddleDoubleClicked  = (1 << 19),
    275 	UISignalFlag_RightDoubleClicked   = (1 << 20),
    276 
    277 	UISignalFlag_LeftTripleClicked    = (1 << 21),
    278 	UISignalFlag_MiddleTripleClicked  = (1 << 22),
    279 	UISignalFlag_RightTripleClicked   = (1 << 23),
    280 
    281 	UISignalFlag_ScrolledX            = (1 << 24),
    282 	UISignalFlag_ScrolledY            = (1 << 25),
    283 
    284 	UISignalFlag_KeyboardPressed      = (1 << 26),
    285 
    286 	UISignalFlag_Hovering             = (1 << 27),
    287 
    288 	UISignalFlag_TextCommit           = (1 << 28),
    289 
    290 	UISignalFlag_Scrolled             = UISignalFlag_ScrolledX|UISignalFlag_ScrolledY,
    291 	UISignalFlag_Pressed              = UISignalFlag_LeftPressed|UISignalFlag_KeyboardPressed,
    292 	UISignalFlag_Released             = UISignalFlag_LeftReleased,
    293 	UISignalFlag_Clicked              = UISignalFlag_LeftClicked|UISignalFlag_KeyboardPressed,
    294 	UISignalFlag_DoubleClicked        = UISignalFlag_LeftDoubleClicked,
    295 	UISignalFlag_TripleClicked        = UISignalFlag_LeftTripleClicked,
    296 	UISignalFlag_Dragging             = UISignalFlag_LeftDragging,
    297 } UISignalFlags;
    298 
    299 typedef struct {
    300 	UINode        *node;
    301 	v2             scroll;
    302 	str8           string;
    303 	UISignalFlags  flags;
    304 } UISignal;
    305 
    306 typedef struct {
    307 	UINodeKey node_key;
    308 	UINodeKey next_node_key;
    309 	UINodeKey last_node_key;
    310 
    311 	i16       cursor;
    312 	i16       mark;
    313 	i16       count;
    314 	i16       last_count;
    315 	b32       numeric;
    316 	b32       changed;
    317 	// TODO(rnp): animation key
    318 	BeamformerUIBlinker blinker;
    319 	u8        buffer[256];
    320 	u8        last_buffer[256];
    321 } UITextInputState;
    322 
    323 typedef struct F32Node F32Node;
    324 struct F32Node {F32Node *next; f32 v;};
    325 
    326 typedef struct V4Node V4Node;
    327 struct V4Node {V4Node *next; v4 v;};
    328 
    329 #define UI_STACK_LIST \
    330 	X(Axis2Node,       child_layout_axis,      Axis2,       0) \
    331 	X(F32Node,         font_size,              f32,         0) \
    332 	X(F32Node,         border_thickness,       f32,         UI_BORDER_THICK) \
    333 	X(F32Node,         text_outline_thickness, f32,         0) \
    334 	X(UINodeFlagsNode, flags,                  UINodeFlags, 0) \
    335 	X(UIParentNode,    parent,                 UINode *,    (&ui_node_nil)) \
    336 	X(UISizeNode,      semantic_height,        UISize,      {0}) \
    337 	X(UISizeNode,      semantic_width,         UISize,      {0}) \
    338 	X(UIAlignNode,     alignment_y,            UIAlign,     0) \
    339 	X(UIAlignNode,     alignment_x,            UIAlign,     0) \
    340 	X(UIAlignNode,     text_alignment,         UIAlign,     UIAlign_Left) \
    341 	X(V4Node,          text_colour,            v4,          FG_COLOUR) \
    342 	X(V4Node,          text_outline_colour,    v4,          NIL_COLOUR) \
    343 	X(V4Node,          border_colour,          v4,          NIL_COLOUR) \
    344 	X(V4Node,          bg_colour,              v4,          NIL_COLOUR) \
    345 
    346 
    347 typedef struct {
    348 	u64   current_frame_index;
    349 	Arena arena;
    350 
    351 	v2    current_mouse;
    352 	v2    last_mouse;
    353 	u64   input_consumed[countof(((BeamformerInput *)0)->event_queue) / 64];
    354 	static_assert(countof(((BeamformerInput *)0)->event_queue) % 64 == 0, "");
    355 
    356 	Font font;
    357 	Font small_font;
    358 
    359 	BeamformerFrameView *view_first;
    360 	BeamformerFrameView *view_last;
    361 	BeamformerFrameView *view_freelist;
    362 
    363 	VulkanHandle    pipelines[BeamformerShaderKind_RenderCount];
    364 
    365 	OSHandle        render_semaphores_export[2];
    366 	VulkanHandle    render_semaphores[2];
    367 	u32             render_semaphores_gl[2];
    368 
    369 	GPUImage        render_3d_image;
    370 	GPUImage        render_3d_depth_image;
    371 	RenderModel     unit_cube_model;
    372 
    373 	BeamformerFrame latest_plane[BeamformerViewPlaneTag_Count];
    374 
    375 	BeamformerUIParameters parameters;
    376 	b32                    flush_parameters;
    377 	u32 selected_parameter_block;
    378 
    379 	// TODO(rnp): this should be per parameter block
    380 	f32 off_axis_position;
    381 	f32 beamform_plane;
    382 
    383 	BeamformerUIPanel *tree;
    384 	BeamformerUIPanel *tree_node_freelist;
    385 
    386 	// NOTE(rnp): context menu
    387 	UINode            *context_menu_root;
    388 	UINodeKey          context_menu_anchor_key;
    389 	UINodeKey          context_menu_next_anchor_key;
    390 	BeamformerUIPanel *context_menu_panel;
    391 	BeamformerUIPanel *context_menu_next_panel;
    392 	f32                context_menu_open_t;
    393 	b32                context_menu_state_changed;
    394 
    395 	// NOTE(rnp): drag info
    396 	UINodeKey          drop_target_key;  // alway a stable node
    397 	UINode            *drop_target_node; // may point to a transient node
    398 	UINode            *drag_root;
    399 	UINode            *drag_overlay_root;
    400 	UINode            *drag_overlay_edges_root;
    401 	UINode            *drag_overlay_tab_root;
    402 	BeamformerUIPanel *drag_panel;
    403 	f32                drag_open_t;
    404 	b32                drag_end;
    405 
    406 	// NOTE(rnp): User Interaction
    407 	UINodeKey        hot_node_key;
    408 	UINodeKey        active_node_key[UIMouseButtonKind_Count];
    409 	// TODO(rnp): click timestamp history (double/triple press)
    410 
    411 	// NOTE(rnp): Builder State
    412 	UINode          *node_freelist;
    413 	UINode          *root_node;
    414 	Arena            build_arenas[2];
    415 	TempArena        build_arena_savepoints[2];
    416 	// NOTE(rnp): Builder Stacks
    417 	#define X(type, name, ...) struct {type *top; type *free; u64 count;} name##_node_stack;
    418 	UI_STACK_LIST
    419 	#undef X
    420 
    421 	UINodeHashBucket node_hash_table[UI_HASH_TABLE_COUNT];
    422 
    423 	UITextInputState text_input_state;
    424 } BeamformerUI;
    425 
    426 typedef enum {
    427 	TF_NONE     = 0,
    428 	TF_ROTATED  = 1 << 0,
    429 	TF_LIMITED  = 1 << 1,
    430 	TF_OUTLINED = 1 << 2,
    431 } TextFlags;
    432 
    433 typedef enum {
    434 	TextAlignment_Center,
    435 	TextAlignment_Left,
    436 	TextAlignment_Right,
    437 } TextAlignment;
    438 
    439 typedef struct {
    440 	Font  *font;
    441 	Rect  limits;
    442 	v4    colour;
    443 	v4    outline_colour;
    444 	f32   outline_thick;
    445 	f32   rotation;
    446 	TextAlignment align;
    447 	TextFlags     flags;
    448 } TextSpec;
    449 
    450 global BeamformerUI    *ui_context;
    451 global BeamformerInput *beamformer_input;
    452 
    453 read_only global UINode ui_node_nil = {
    454 	.parent           = &ui_node_nil,
    455 	.first_child      = &ui_node_nil,
    456 	.last_child       = &ui_node_nil,
    457 	.previous_sibling = &ui_node_nil,
    458 	.next_sibling     = &ui_node_nil,
    459 };
    460 
    461 #define X(type, name, _t, impl) read_only global type ui_##name##_node_nil = {.v = impl};
    462 UI_STACK_LIST
    463 #undef X
    464 
    465 #define ui_node_is_nil(n) ((n) == 0 || (n) == &ui_node_nil)
    466 #define ui_build_arena()  (ui_context->build_arenas + (ui_context->current_frame_index % countof(ui_context->build_arenas)))
    467 
    468 #define UIStackPushBody(name_upper, name_lower, type, new_value) \
    469 	name_upper *node = SLLPop(ui_context->name_lower##_node_stack.free, next); \
    470 	if (!node) node = push_struct_no_zero(ui_build_arena(), name_upper); \
    471 	node->v = new_value; \
    472 	type result = ui_context->name_lower##_node_stack.top->v; \
    473 	SLLStackPush(ui_context->name_lower##_node_stack.top, node, next); \
    474 	ui_context->name_lower##_node_stack.count++; \
    475 	return result
    476 
    477 #define UIStackPopBody(name_upper, name_lower, type) \
    478 	name_upper *node = ui_context->name_lower##_node_stack.top; \
    479 	type result = node->v; \
    480 	if (node != &ui_##name_lower##_node_nil) { \
    481 		node = SLLPop(ui_context->name_lower##_node_stack.top, next); \
    482 		SLLStackPush(ui_context->name_lower##_node_stack.free, node, next); \
    483 	} \
    484 	return result
    485 
    486 #define UIAlign(v)                DeferLoop(ui_push_alignment(UIAlign_##v), ui_pop_alignment())
    487 #define UIAxisAlign(axis, v)      DeferLoop(ui_push_axis_alignment(axis, UIAlign_##v), ui_pop_axis_alignment(axis))
    488 #define UIAxisSize(axis, v)       DeferLoop(ui_push_axis_size(axis, v), ui_pop_axis_size(axis))
    489 #define UIBorderColour(v)         DeferLoop(ui_push_border_colour(v), ui_pop_border_colour())
    490 #define UIBorderThickness(v)      DeferLoop(ui_push_border_thickness(v), ui_pop_border_thickness())
    491 #define UIBGColour(v)             DeferLoop(ui_push_bg_colour(v), ui_pop_bg_colour())
    492 #define UIChildLayoutAxis(v)      DeferLoop(ui_push_child_layout_axis(v), ui_pop_child_layout_axis())
    493 #define UIFlags(v)                DeferLoop(ui_push_flags(v), ui_pop_flags())
    494 #define UIFontSize(v)             DeferLoop(ui_push_font_size(v), ui_pop_font_size())
    495 #define UIParent(v)               DeferLoop(ui_push_parent(v), ui_pop_parent())
    496 #define UIPrefHeight(v)           DeferLoop(ui_push_semantic_height(v), ui_pop_semantic_height())
    497 #define UIPrefWidth(v)            DeferLoop(ui_push_semantic_width(v), ui_pop_semantic_width())
    498 #define UISize(v)                 DeferLoop(ui_push_size(v), ui_pop_size())
    499 #define UITextAlign(v)            DeferLoop(ui_push_text_alignment(UIAlign_##v), ui_pop_text_alignment())
    500 #define UITextOutlineColour(v)    DeferLoop(ui_push_text_outline_colour(v), ui_pop_text_outline_colour())
    501 #define UITextOutlineThickness(v) DeferLoop(ui_push_text_outline_thickness(v), ui_pop_text_outline_thickness())
    502 #define UITextColour(v)           DeferLoop(ui_push_text_colour(v), ui_pop_text_colour())
    503 
    504 #define UIScroll(axis)            DeferLoop(ui_scroll_begin(axis), ui_scroll_end())
    505 
    506 #define X(type, name, value_type, ...) \
    507 	function value_type ui_push_##name(value_type v) {UIStackPushBody(type, name, value_type, v);} \
    508 	function value_type ui_pop_##name(void)          {UIStackPopBody(type, name, value_type);} \
    509 	function value_type ui_top_##name(void)          {return ui_context->name##_node_stack.top->v;}
    510 UI_STACK_LIST
    511 #undef X
    512 
    513 #define ui_size(k, v, s) (UISize){.kind = UISizeKind_##k, .value = (v), .strictness = (s)}
    514 #define ui_em(value, strictness)         ui_size(Pixels, (value) * ui_top_font_size(), (strictness))
    515 #define ui_px(value, strictness)         ui_size(Pixels, (value), (strictness))
    516 #define ui_pct(value, strictness)        ui_size(PercentOfParent, (value), (strictness))
    517 #define ui_children_sum(strictness)      ui_size(ChildrenSum, 0.f, (strictness))
    518 #define ui_text_dim(padding, strictness) ui_size(TextContent, (padding), (strictness))
    519 
    520 #define ui_node_key_zero() (UINodeKey){0}
    521 
    522 #define ui_spacer(flags) ui_build_node_from_key(flags, ui_node_key_zero())
    523 #define ui_padw(v) UIPrefWidth(ui_px(v, 1.f))  ui_spacer(0)
    524 #define ui_padh(v) UIPrefHeight(ui_px(v, 1.f)) ui_spacer(0)
    525 #define ui_pads(v) UISize(ui_px(v, 1.f))       ui_spacer(0)
    526 
    527 #define ui_dragging(s)     (!!((s).flags & UISignalFlag_Dragging))
    528 #define ui_released(s)     (!!((s).flags & UISignalFlag_Released))
    529 #define ui_pressed(s)      (!!((s).flags & UISignalFlag_Pressed))
    530 #define ui_scrolled(s)     (!!((s).flags & UISignalFlag_Scrolled))
    531 
    532 #define ui_context_menu(p) ((p) == ui_context->context_menu_panel)
    533 
    534 function UIAlign
    535 ui_push_axis_alignment(Axis2 axis, UIAlign v)
    536 {
    537 	UIAlign result = 0;
    538 	switch (axis) {
    539 	case Axis2_X:{result = ui_push_alignment_x(v);}break;
    540 	case Axis2_Y:{result = ui_push_alignment_y(v);}break;
    541 	InvalidDefaultCase;
    542 	}
    543 	return result;
    544 }
    545 
    546 function UIAlign
    547 ui_pop_axis_alignment(Axis2 axis)
    548 {
    549 	UIAlign result = 0;
    550 	switch (axis) {
    551 	case Axis2_X:{result = ui_pop_alignment_x();}break;
    552 	case Axis2_Y:{result = ui_pop_alignment_y();}break;
    553 	InvalidDefaultCase;
    554 	}
    555 	return result;
    556 }
    557 
    558 function UIAlign
    559 ui_push_alignment(UIAlign v)
    560 {
    561 	UIAlign result = ui_push_axis_alignment(ui_top_child_layout_axis(), v);
    562 	return result;
    563 }
    564 
    565 function UIAlign
    566 ui_pop_alignment(void)
    567 {
    568 	UIAlign result = ui_pop_axis_alignment(ui_top_child_layout_axis());
    569 	return result;
    570 }
    571 
    572 function UISize
    573 ui_push_axis_size(Axis2 axis, UISize v)
    574 {
    575 	UISize result = {0};
    576 	switch (axis) {
    577 	case Axis2_X:{result = ui_push_semantic_width(v); }break;
    578 	case Axis2_Y:{result = ui_push_semantic_height(v);}break;
    579 	InvalidDefaultCase;
    580 	}
    581 	return result;
    582 }
    583 
    584 function UISize
    585 ui_pop_axis_size(Axis2 axis)
    586 {
    587 	UISize result = {0};
    588 	switch (axis) {
    589 	case Axis2_X:{result = ui_pop_semantic_width(); }break;
    590 	case Axis2_Y:{result = ui_pop_semantic_height();}break;
    591 	InvalidDefaultCase;
    592 	}
    593 	return result;
    594 }
    595 
    596 function UISize
    597 ui_push_size(UISize v)
    598 {
    599 	UISize result = ui_push_axis_size(ui_top_child_layout_axis(), v);
    600 	return result;
    601 }
    602 
    603 function UISize
    604 ui_pop_size(void)
    605 {
    606 	UISize result = ui_pop_axis_size(ui_top_child_layout_axis());
    607 	return result;
    608 }
    609 
    610 #define ui_node_key_nil(k) (ui_node_key_equal((k), ui_node_key_zero()))
    611 #define ui_node_hot(n)     (ui_node_key_equal((n)->key, ui_context->hot_node_key))
    612 function b32
    613 ui_node_key_equal(UINodeKey a, UINodeKey b)
    614 {
    615 	b32 result = a.value == b.value;
    616 	return result;
    617 }
    618 
    619 function UINodeKey
    620 ui_node_ancestor_key(void)
    621 {
    622 	UINode *node = ui_top_parent();
    623 	while (!ui_node_is_nil(node) && ui_node_key_equal(node->key, ui_node_key_zero()))
    624 		node = node->parent;
    625 	UINodeKey result = node->key;
    626 	return result;
    627 }
    628 
    629 function Rect
    630 ui_node_rect(UINode *node)
    631 {
    632 	Rect result = {0};
    633 	result.size = (v2){{node->computed_size[0], node->computed_size[1]}};
    634 	result.pos  = (v2){{node->computed_position[0], node->computed_position[1]}};
    635 	return result;
    636 }
    637 
    638 function f32
    639 ui_alignment_correction(UIAlign alignment, f32 delta)
    640 {
    641 	f32 result = 0;
    642 	switch (alignment) {
    643 	InvalidDefaultCase;
    644 	case UIAlign_Left:{  result = 0;           }break;
    645 	case UIAlign_Center:{result = 0.5f * delta;}break;
    646 	case UIAlign_Right:{ result = delta;       }break;
    647 	}
    648 	return result;
    649 }
    650 
    651 function v2
    652 ui_node_text_position(UINode *node)
    653 {
    654 	Rect r = ui_node_rect(node);
    655 	v2 result = r.pos;
    656 	result.x += ui_alignment_correction(node->text_alignment, r.size.x - node->text_size.x);
    657 	result.y += (r.size.y - node->text_size.y) / 2.f;
    658 	return result;
    659 }
    660 
    661 function void
    662 ui_disable_cursor(void)
    663 {
    664 	HideCursor();
    665 	DisableCursor();
    666 	/* wtf raylib */
    667 	SetMousePosition((i32)ui_context->current_mouse.x, (i32)ui_context->current_mouse.y);
    668 }
    669 
    670 function void
    671 ui_enable_cursor(void)
    672 {
    673 	EnableCursor();
    674 }
    675 
    676 function Vector2
    677 rl_v2(v2 a)
    678 {
    679 	Vector2 result = {a.x, a.y};
    680 	return result;
    681 }
    682 
    683 function Rectangle
    684 rl_rect(Rect a)
    685 {
    686 	Rectangle result = {a.pos.x, a.pos.y, a.size.w, a.size.h};
    687 	return result;
    688 }
    689 
    690 function f32
    691 beamformer_ui_blinker_update(BeamformerUIBlinker *b, f32 scale)
    692 {
    693 	b->t += b->scale * dt_for_frame;
    694 	if (b->t >= 1.0f) b->scale = -scale;
    695 	if (b->t <= 0.0f) b->scale =  scale;
    696 	f32 result = b->t;
    697 	return result;
    698 }
    699 
    700 function v2
    701 measure_glyph(Font font, u32 glyph)
    702 {
    703 	assert(glyph >= 0x20);
    704 	v2 result = {.y = (f32)font.baseSize};
    705 	/* NOTE: assumes font glyphs are ordered ASCII */
    706 	result.x = (f32)font.glyphs[glyph - 0x20].advanceX;
    707 	if (result.x == 0)
    708 		result.x = (font.recs[glyph - 0x20].width + (f32)font.glyphs[glyph - 0x20].offsetX);
    709 	return result;
    710 }
    711 
    712 function v2
    713 measure_text_tight(Font font, str8 text)
    714 {
    715 	v2 result = {0};
    716 	for (i64 i = 0; i < text.length; i++) {
    717 		assert(text.data[i] >= 0x20);
    718 		u8 glyph = text.data[i] - 0x20;
    719 		result.x += font.recs[glyph].width;
    720 		result.y  = Max(font.recs[glyph].height, result.y);
    721 	}
    722 	return result;
    723 }
    724 
    725 function v2
    726 measure_text(Font font, str8 text)
    727 {
    728 	v2 result = {.y = (f32)font.baseSize};
    729 	for (i64 i = 0; i < text.length; i++)
    730 		result.x += measure_glyph(font, text.data[i]).x;
    731 	return result;
    732 }
    733 
    734 function str8
    735 clamp_text_to_width(Font font, str8 text, f32 limit)
    736 {
    737 	str8 result = text;
    738 	f32  width  = 0;
    739 	for (i64 i = 0; i < text.length; i++) {
    740 		f32 next = measure_glyph(font, text.data[i]).w;
    741 		if (width + next > limit) {
    742 			result.length = i;
    743 			break;
    744 		}
    745 		width += next;
    746 	}
    747 	return result;
    748 }
    749 
    750 function Texture
    751 make_raylib_texture(BeamformerFrameView *v)
    752 {
    753 	Texture result;
    754 	result.id      = v->texture;
    755 	result.width   = v->colour_image.width;
    756 	result.height  = v->colour_image.height;
    757 	result.mipmaps = v->colour_image.mip_map_levels;
    758 	result.format  = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8;
    759 	return result;
    760 }
    761 
    762 function str8
    763 push_acquisition_kind(Arena *arena, BeamformerAcquisitionKind kind, u32 transmit_count, BeamformerContrastMode contrast_mode)
    764 {
    765 	str8 name           = str8("Invalid");
    766 	b32 fixed_transmits = 0;
    767 	if Between(kind, 0, BeamformerAcquisitionKind_Count - 1) {
    768 		name            = beamformer_acquisition_kind_strings[kind];
    769 		fixed_transmits = beamformer_acquisition_kind_has_fixed_transmits[kind];
    770 	}
    771 
    772 	Stream sb = arena_stream(*arena);
    773 	stream_append_s8(&sb, s8_from_str8(name));
    774 	if (!fixed_transmits) {
    775 		stream_append_byte(&sb, '-');
    776 		stream_append_u64(&sb, transmit_count);
    777 	}
    778 
    779 	if (contrast_mode != BeamformerContrastMode_None)
    780 		stream_append_s8s(&sb, s8(" ("), s8_from_str8(beamformer_contrast_mode_strings[contrast_mode]), s8(")"));
    781 
    782 	s8 result = arena_stream_commit(arena, &sb);
    783 	return str8_from_s8(result);
    784 }
    785 
    786 function void
    787 resize_frame_view(BeamformerFrameView *view, uv2 dim)
    788 {
    789 	if ValidHandle(view->export_handle) os_release_handle(view->export_handle);
    790 
    791 	glDeleteMemoryObjectsEXT(1, &view->memory_object);
    792 	glCreateMemoryObjectsEXT(1, &view->memory_object);
    793 
    794 	glDeleteTextures(1, &view->texture);
    795 	glCreateTextures(GL_TEXTURE_2D, 1, &view->texture);
    796 
    797 	/* TODO(rnp): add some ID for the specific view here */
    798 	s8 label = s8("Frame View Texture");
    799 	vk_image_allocate(&view->colour_image, dim.w, dim.h, 1, 1, VulkanImageUsage_Colour,
    800 	                  VulkanUsageFlag_ImageSampling, &view->export_handle, label);
    801 
    802 	glMemoryObjectParameterivEXT(view->memory_object, GL_DEDICATED_MEMORY_OBJECT_EXT, (GLint []){1});
    803 
    804 	if (OS_WINDOWS) {
    805 		glImportMemoryWin32HandleEXT(view->memory_object, view->colour_image.memory_size,
    806 		                             GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, (void *)view->export_handle.value[0]);
    807 		// NOTE(rnp): w32 does not transfer ownership from handle back to driver
    808 	} else {
    809 		glImportMemoryFdEXT(view->memory_object, view->colour_image.memory_size,
    810 		                    GL_HANDLE_TYPE_OPAQUE_FD_EXT, view->export_handle.value[0]);
    811 		view->export_handle.value[0] = OSInvalidHandleValue;
    812 	}
    813 
    814 	glTextureStorageMem2DEXT(view->texture, view->colour_image.mip_map_levels, GL_RGBA8,
    815 	                         view->colour_image.width, view->colour_image.height,
    816 	                         view->memory_object, 0);
    817 
    818 	/* NOTE(rnp): work around raylib's janky texture sampling */
    819 	v4 border_colour = {{0, 0, 0, 1}};
    820 	if (view->kind != BeamformerFrameViewKind_Copy) border_colour = (v4){0};
    821 	glTextureParameteri(view->texture, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    822 	glTextureParameteri(view->texture, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    823 	glTextureParameterfv(view->texture, GL_TEXTURE_BORDER_COLOR, border_colour.E);
    824 	/* TODO(rnp): better choice when depth component is included */
    825 	glTextureParameteri(view->texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    826 	glTextureParameteri(view->texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    827 
    828 	glObjectLabel(GL_TEXTURE, view->texture, (i32)label.len, (char *)label.data);
    829 }
    830 
    831 function void
    832 beamformer_ui_frame_view_release_subresources(BeamformerFrameView *bv, BeamformerFrameViewKind kind)
    833 {
    834 	if (kind == BeamformerFrameViewKind_Copy)
    835 		vk_buffer_release(&bv->copy_buffer);
    836 }
    837 
    838 function void
    839 beamformer_ui_frame_view_copy_frame(BeamformerFrameView *new, BeamformerFrameView *old)
    840 {
    841 	memory_copy(&new->frame, &old->frame, sizeof(old->frame));
    842 
    843 	iv3 points     = new->frame.points;
    844 	i64 frame_size = points.x * points.y * points.z * beamformer_data_kind_byte_size[new->frame.data_kind];
    845 
    846 	Stream sb = arena_stream(ui_context->arena);
    847 	stream_append_s8(&sb, s8("Frame Copy ["));
    848 	stream_append_hex_u64(&sb, new->frame.id);
    849 	stream_append_s8(&sb, s8("]"));
    850 	stream_append_byte(&sb, 0);
    851 
    852 	GPUBufferAllocateInfo allocate_info = {
    853 		.size  = frame_size,
    854 		.flags = VulkanUsageFlag_TransferDestination,
    855 		.label = stream_to_str8(&sb),
    856 	};
    857 	vk_buffer_allocate(&new->copy_buffer, &allocate_info);
    858 
    859 	GPUBuffer *backlog = beamformer_context->compute_context.backlog.buffer;
    860 	VulkanHandle cmd = vk_command_begin(VulkanTimeline_Compute);
    861 	vk_command_wait_timeline(cmd, VulkanTimeline_Compute, old->frame.timeline_valid_value);
    862 	vk_command_copy_buffer(cmd, &new->copy_buffer, backlog, old->frame.buffer_offset, frame_size);
    863 	new->frame.timeline_valid_value = vk_command_end(cmd, (VulkanHandle){0}, (VulkanHandle){0});
    864 }
    865 
    866 function BeamformerFrameView *
    867 beamformer_ui_frame_view_new(BeamformerFrameViewKind kind)
    868 {
    869 	BeamformerFrameView *old    = (BeamformerFrameView *)beamformer_registers()->frame_view;
    870 	BeamformerFrameView *result = SLLPopFreelist(ui_context->view_freelist);
    871 	if (!result) result = push_struct_no_zero(&ui_context->arena, typeof(*result));
    872 	zero_struct(result);
    873 	DLLInsertLast(0, ui_context->view_first, ui_context->view_last, result, next, prev);
    874 
    875 	result->export_handle.value[0] = OSInvalidHandleValue;
    876 
    877 	result->kind  = kind;
    878 	result->dirty = 1;
    879 
    880 	result->log_scale     = old? old->log_scale     : 0;
    881 	result->dynamic_range = old? old->dynamic_range : 50.0f;
    882 	result->threshold     = old? old->threshold     : 55.0f;
    883 	result->gamma         = old? old->gamma         : 1.0f;
    884 
    885 	/* TODO(rnp): this is quite dumb. what we actually want is to render directly
    886 	 * into the view region with the appropriate size for that region (scissor) */
    887 	resize_frame_view(result, (uv2){{FRAME_VIEW_RENDER_TARGET_SIZE}});
    888 
    889 	switch (kind) {
    890 	default:{
    891 		b32 copy = kind == BeamformerFrameViewKind_Copy;
    892 		result->scale_bar_active[0] = copy ? old->scale_bar_active[0] : 1;
    893 		result->scale_bar_active[1] = copy ? old->scale_bar_active[1] : 1;
    894 	}break;
    895 	case BeamformerFrameViewKind_3DXPlane:{
    896 		glTextureParameteri(result->texture, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    897 		glTextureParameteri(result->texture, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    898 		result->demo             = 1;
    899 		result->plane_drag_index = -1;
    900 		result->plane_active[BeamformerViewPlaneTag_XZ] = 1;
    901 		result->plane_active[BeamformerViewPlaneTag_YZ] = 1;
    902 	}break;
    903 	}
    904 
    905 	if (kind == BeamformerFrameViewKind_Copy) {
    906 		assert(old != 0);
    907 		beamformer_ui_frame_view_copy_frame(result, old);
    908 	}
    909 
    910 	if (kind == BeamformerFrameViewKind_Latest)
    911 		result->view_plane = BeamformerViewPlaneTag_Count;
    912 
    913 	return result;
    914 }
    915 
    916 function v3
    917 x_plane_display_size(BeamformerFrame *frame)
    918 {
    919 	v3 result = {0};
    920 	v2 min_2d, max_2d;
    921 	plane_corners_from_transform(frame->voxel_transform, &min_2d, &max_2d);
    922 	result.xy = v2_sub(max_2d, min_2d);
    923 	result.x  = Max(1e-3f, result.x);
    924 	result.y  = Max(1e-3f, result.y);
    925 	result.z  = Max(1e-3f, result.z);
    926 	return result;
    927 }
    928 
    929 function f32
    930 x_plane_rotation_for_view_plane(BeamformerFrameView *view, BeamformerViewPlaneTag tag)
    931 {
    932 	f32 result = view->rotation;
    933 	if (tag == BeamformerViewPlaneTag_YZ)
    934 		result += 0.25f;
    935 	return result;
    936 }
    937 
    938 function v3
    939 x_plane_position(BeamformerFrame *frame)
    940 {
    941 	v2 min_2d, max_2d;
    942 	plane_corners_from_transform(frame->voxel_transform, &min_2d, &max_2d);
    943 	f32 y_min = min_2d.y;
    944 	f32 y_max = max_2d.y;
    945 	v3 result = {.y = y_min + (y_max - y_min) / 2};
    946 	return result;
    947 }
    948 
    949 function v3
    950 x_plane_offset_position(BeamformerFrameView *view, BeamformerFrame *frame, BeamformerViewPlaneTag tag)
    951 {
    952 	BeamformerLiveImagingParameters *li = &beamformer_context->shared_memory->live_imaging_parameters;
    953 	m4 x_rotation = m4_rotation_about_y(x_plane_rotation_for_view_plane(view, tag));
    954 	v3 Z = x_rotation.c[2].xyz;
    955 	v3 offset = v3_scale(Z, li->image_plane_offsets[tag]);
    956 	v3 result = v3_add(x_plane_position(frame), offset);
    957 	return result;
    958 }
    959 
    960 function v3
    961 x_plane_camera(BeamformerFrame *frame)
    962 {
    963 	v3 size   = x_plane_display_size(frame);
    964 	v3 target = x_plane_position(frame);
    965 	f32 dist  = v2_magnitude(size.xy);
    966 	v3 result = v3_add(target, (v3){{dist, -0.5f * size.y * tan_f32(50.0f * PI / 180.0f), dist}});
    967 	return result;
    968 }
    969 
    970 function m4
    971 x_plane_view_matrix(BeamformerFrame *frame, v3 camera)
    972 {
    973 	m4 result = camera_look_at(camera, x_plane_position(frame));
    974 	return result;
    975 }
    976 
    977 function m4
    978 x_plane_projection_matrix(f32 aspect)
    979 {
    980 	m4 result = perspective_projection(10e-3f, 500e-3f, 45.0f * PI / 180.0f, aspect);
    981 	return result;
    982 }
    983 
    984 function ray
    985 x_plane_raycast(BeamformerFrameView *view, BeamformerFrame *frame, v2 uv)
    986 {
    987 	assert(view->kind == BeamformerFrameViewKind_3DXPlane);
    988 	ray result  = {.origin = x_plane_camera(frame)};
    989 	v4 ray_clip = {{uv.x, uv.y, -1.0f, 1.0f}};
    990 
    991 	/* TODO(rnp): combine these so we only do one matrix inversion */
    992 	m4 proj_m   = x_plane_projection_matrix((f32)view->colour_image.width / (f32)view->colour_image.height);
    993 	m4 view_m   = x_plane_view_matrix(frame, result.origin);
    994 	m4 proj_inv = m4_inverse(proj_m);
    995 	m4 view_inv = m4_inverse(view_m);
    996 
    997 	v4 ray_eye  = {.z = -1};
    998 	ray_eye.x   = v4_dot(m4_row(proj_inv, 0), ray_clip);
    999 	ray_eye.y   = v4_dot(m4_row(proj_inv, 1), ray_clip);
   1000 	result.direction = v3_normalize(m4_mul_v4(view_inv, ray_eye).xyz);
   1001 
   1002 	return result;
   1003 }
   1004 
   1005 function void
   1006 render_single_xplane(BeamformerFrameView *view, BeamformerFrame *frame, v3 translate, f32 rotation_turns,
   1007                      VulkanHandle command, BeamformerRenderBeamformedPushConstants *pc, m4 vp_m, b32 drag_plane)
   1008 {
   1009 	GPUBuffer *beamformed_buffer = beamformer_context->compute_context.backlog.buffer;
   1010 	pc->input_data   = frame->timeline_valid_value ? beamformed_buffer->gpu_pointer + frame->buffer_offset : 0;
   1011 	pc->input_size_x = frame->points.x;
   1012 	pc->input_size_y = frame->points.y;
   1013 	pc->input_size_z = frame->points.z;
   1014 	pc->data_kind    = frame->data_kind;
   1015 	pc->mvp_matrix   = m4_mul(vp_m, y_aligned_volume_transform(x_plane_display_size(frame), translate, rotation_turns));
   1016 
   1017 	vk_command_wait_timeline(command, VulkanTimeline_Compute, frame->timeline_valid_value);
   1018 	vk_command_push_constants(command, 0, sizeof(*pc), pc);
   1019 	vk_command_draw(command, &ui_context->unit_cube_model.model);
   1020 
   1021 	v3 xp_delta = v3_sub(view->hit_test_point, view->hit_start_point);
   1022 	if (drag_plane && !f32_equal(v3_magnitude_squared(xp_delta), 0)) {
   1023 		m4 x_rotation = m4_rotation_about_y(rotation_turns);
   1024 		v3 Z = x_rotation.c[2].xyz;
   1025 		v3 f = v3_scale(Z, v3_dot(Z, xp_delta));
   1026 
   1027 		pc->mvp_matrix = m4_mul(vp_m, y_aligned_volume_transform(x_plane_display_size(frame), v3_add(f, translate), rotation_turns));
   1028 		pc->bounding_box_colour   = HOVERED_COLOUR;
   1029 		pc->bounding_box_fraction = 1.0f;
   1030 		pc->input_data            = 0;
   1031 
   1032 		vk_command_push_constants(command, 0, sizeof(*pc), pc);
   1033 		vk_command_draw(command, &ui_context->unit_cube_model.model);
   1034 	}
   1035 }
   1036 
   1037 function void
   1038 render_3d_xplane(BeamformerFrameView *view, VulkanHandle command, BeamformerRenderBeamformedPushConstants *pc)
   1039 {
   1040 	if (view->demo) {
   1041 		view->rotation += dt_for_frame * 0.125f;
   1042 		if (view->rotation > 1.f) view->rotation -= 1.f;
   1043 	}
   1044 
   1045 	u32 largest_plane_index = 0;
   1046 	f32 largest_magnitude = 0.f;
   1047 	for EachElement(view->plane_active, plane) if (view->plane_active[plane]) {
   1048 		BeamformerFrame *frame = ui_context->latest_plane + plane;
   1049 		f32 m = v3_magnitude_squared(x_plane_display_size(frame));
   1050 		if (largest_magnitude < m) {
   1051 			largest_magnitude   = m;
   1052 			largest_plane_index = plane;
   1053 		}
   1054 	}
   1055 
   1056 	BeamformerFrame *frame = ui_context->latest_plane + largest_plane_index;
   1057 	m4 projection = x_plane_projection_matrix((f32)view->colour_image.width / (f32)view->colour_image.height);
   1058 	m4 view_m     = camera_look_at(x_plane_camera(frame), x_plane_position(frame));
   1059 	m4 vp_m       = m4_mul(projection, view_m);
   1060 
   1061 	for EachElement(view->plane_active, plane) {
   1062 		frame = ui_context->latest_plane + plane;
   1063 		if (view->plane_active[plane] && frame->timeline_valid_value) {
   1064 			pc->bounding_box_fraction = FRAME_VIEW_BB_FRACTION;
   1065 			pc->bounding_box_colour   = v4_lerp(FG_COLOUR, HOVERED_COLOUR, view->hot_t[plane]);
   1066 			f32 rotation  = x_plane_rotation_for_view_plane(view, plane);
   1067 			v3  translate = x_plane_offset_position(view, frame, plane);
   1068 			render_single_xplane(view, frame, translate, rotation, command, pc, vp_m, (i32)plane == view->plane_drag_index);
   1069 		}
   1070 	}
   1071 }
   1072 
   1073 function void
   1074 render_2d_plane(BeamformerFrameView *view, VulkanHandle command, BeamformerRenderBeamformedPushConstants *pc)
   1075 {
   1076 	m4 view_m     = m4_identity();
   1077 	m4 model      = m4_scale((v3){{2.0f, 2.0f, 0.0f}});
   1078 	m4 projection = orthographic_projection(0, 1, 1, 1);
   1079 
   1080 	GPUBuffer *beamformed_buffer = beamformer_context->compute_context.backlog.buffer;
   1081 	pc->mvp_matrix   = m4_mul(m4_mul(model, view_m), projection);
   1082 	pc->input_data   = beamformed_buffer->gpu_pointer + view->frame.buffer_offset;
   1083 	pc->input_size_x = view->frame.points.x;
   1084 	pc->input_size_y = view->frame.points.y;
   1085 	pc->input_size_z = view->frame.points.z;
   1086 	pc->data_kind    = view->frame.data_kind;
   1087 
   1088 	vk_command_wait_timeline(command, VulkanTimeline_Compute, view->frame.timeline_valid_value);
   1089 	vk_command_push_constants(command, 0, sizeof(*pc), pc);
   1090 	vk_command_draw(command, &ui_context->unit_cube_model.model);
   1091 }
   1092 
   1093 function b32
   1094 view_update(BeamformerUI *ui, BeamformerFrameView *view)
   1095 {
   1096 	if (view->kind == BeamformerFrameViewKind_Latest) {
   1097 		BeamformerFrame *frame;
   1098 		if (view->view_plane  == BeamformerViewPlaneTag_Count)
   1099 			frame = beamformer_frame_from_index(beamformer_registers()->frame);
   1100 		else
   1101 			frame = ui->latest_plane + view->view_plane;
   1102 
   1103 		view->dirty |= view->frame.timeline_valid_value != frame->timeline_valid_value;
   1104 		memory_copy(&view->frame, frame, sizeof(view->frame));
   1105 	}
   1106 
   1107 	/* TODO(rnp): x-z or y-z */
   1108 	// TODO(rnp): how to track this now? use pipeline handle value?
   1109 	view->dirty |= beamformer_context->render_shader_updated;
   1110 	view->dirty |= view->kind == BeamformerFrameViewKind_3DXPlane;
   1111 
   1112 	b32 result = view->dirty;
   1113 	return result;
   1114 }
   1115 
   1116 function void
   1117 update_frame_views(BeamformerUI *ui, Rect window)
   1118 {
   1119 	for (BeamformerFrameView *view = ui->view_first; view; view = view->next) {
   1120 		if (view_update(ui, view)) {
   1121 			BeamformerRenderBeamformedPushConstants pc = {
   1122 				.bounding_box_colour = FRAME_VIEW_BB_COLOUR,
   1123 				.db_cutoff           = view->log_scale ? view->dynamic_range : 0,
   1124 				.threshold           = view->threshold,
   1125 				.gamma               = view->gamma,
   1126 				.positions           = ui->unit_cube_model.model.gpu_pointer,
   1127 				.normals             = ui->unit_cube_model.model.gpu_pointer + ui->unit_cube_model.normals_offset,
   1128 			};
   1129 
   1130 			//start_renderdoc_capture();
   1131 
   1132 			glSignalSemaphoreEXT(ui->render_semaphores_gl[0], 0, 0, 1, &view->texture, (GLenum []){GL_NONE});
   1133 
   1134 			VulkanHandle cmd = vk_command_begin(VulkanTimeline_Graphics);
   1135 			vk_command_bind_pipeline(cmd, ui->pipelines[BeamformerShaderKind_RenderBeamformed - BeamformerShaderKind_RenderFirst]);
   1136 			vk_command_begin_rendering(cmd, &ui->render_3d_image, &ui->render_3d_depth_image, &view->colour_image);
   1137 			vk_command_viewport(cmd, view->colour_image.width, view->colour_image.height, 0, 0, 0.0f, 1.0f);
   1138 			vk_command_scissor(cmd, view->colour_image.width, view->colour_image.height, 0, 0);
   1139 			if (view->kind == BeamformerFrameViewKind_3DXPlane) {
   1140 				render_3d_xplane(view, cmd, &pc);
   1141 			} else {
   1142 				render_2d_plane(view, cmd, &pc);
   1143 			}
   1144 			vk_command_end_rendering(cmd);
   1145 			vk_command_end(cmd, ui->render_semaphores[0], ui->render_semaphores[1]);
   1146 
   1147 			glWaitSemaphoreEXT(ui->render_semaphores_gl[1], 0, 0, 1, &view->texture, (GLenum[]){GL_LAYOUT_COLOR_ATTACHMENT_EXT});
   1148 
   1149 			//end_renderdoc_capture();
   1150 			view->dirty = 0;
   1151 		}
   1152 	}
   1153 }
   1154 
   1155 function Color
   1156 colour_from_normalized(v4 rgba)
   1157 {
   1158 	Color result = {.r = (u8)(rgba.r * 255.0f), .g = (u8)(rgba.g * 255.0f),
   1159 	                .b = (u8)(rgba.b * 255.0f), .a = (u8)(rgba.a * 255.0f)};
   1160 	return result;
   1161 }
   1162 
   1163 function void
   1164 draw_text_tight(Font font, str8 text, v2 pos, Color colour)
   1165 {
   1166 	v2 off = v2_floor(pos);
   1167 	for (i64 i = 0; i < text.length; i++) {
   1168 		/* NOTE: assumes font glyphs are ordered ASCII */
   1169 		i32 idx = text.data[i] - 0x20;
   1170 		Rectangle dst = {
   1171 			off.x, off.y,
   1172 			font.recs[idx].width,
   1173 			font.recs[idx].height,
   1174 		};
   1175 		Rectangle src = {
   1176 			font.recs[idx].x,
   1177 			font.recs[idx].y,
   1178 			font.recs[idx].width,
   1179 			font.recs[idx].height,
   1180 		};
   1181 		DrawTexturePro(font.texture, src, dst, (Vector2){0}, 0, colour);
   1182 
   1183 		off.x += (f32)font.recs[idx].width;
   1184 	}
   1185 }
   1186 
   1187 function v2
   1188 draw_text_base(Font font, str8 text, v2 pos, Color colour)
   1189 {
   1190 	v2 off = v2_floor(pos);
   1191 	f32 glyph_pad = (f32)font.glyphPadding;
   1192 	for (i64 i = 0; i < text.length; i++) {
   1193 		/* NOTE: assumes font glyphs are ordered ASCII */
   1194 		i32 idx = text.data[i] - 0x20;
   1195 		Rectangle dst = {
   1196 			off.x + (f32)font.glyphs[idx].offsetX - glyph_pad,
   1197 			off.y + (f32)font.glyphs[idx].offsetY - glyph_pad,
   1198 			font.recs[idx].width  + 2.0f * glyph_pad,
   1199 			font.recs[idx].height + 2.0f * glyph_pad
   1200 		};
   1201 		Rectangle src = {
   1202 			font.recs[idx].x - glyph_pad,
   1203 			font.recs[idx].y - glyph_pad,
   1204 			font.recs[idx].width  + 2.0f * glyph_pad,
   1205 			font.recs[idx].height + 2.0f * glyph_pad
   1206 		};
   1207 		DrawTexturePro(font.texture, src, dst, (Vector2){0}, 0, colour);
   1208 
   1209 		off.x += (f32)font.glyphs[idx].advanceX;
   1210 		if (font.glyphs[idx].advanceX == 0)
   1211 			off.x += font.recs[idx].width;
   1212 	}
   1213 	v2 result = {{off.x - pos.x, (f32)font.baseSize}};
   1214 	return result;
   1215 }
   1216 
   1217 /* NOTE(rnp): expensive but of the available options in raylib this gives the best results */
   1218 function v2
   1219 draw_outlined_text(str8 text, v2 pos, TextSpec *ts)
   1220 {
   1221 	f32 ow = ts->outline_thick;
   1222 	Color outline = colour_from_normalized(ts->outline_colour);
   1223 	Color colour  = colour_from_normalized(ts->colour);
   1224 	draw_text_base(*ts->font, text, v2_sub(pos, (v2){{ ow,  ow}}), outline);
   1225 	draw_text_base(*ts->font, text, v2_sub(pos, (v2){{ ow, -ow}}), outline);
   1226 	draw_text_base(*ts->font, text, v2_sub(pos, (v2){{-ow,  ow}}), outline);
   1227 	draw_text_base(*ts->font, text, v2_sub(pos, (v2){{-ow, -ow}}), outline);
   1228 
   1229 	v2 result = draw_text_base(*ts->font, text, pos, colour);
   1230 
   1231 	return result;
   1232 }
   1233 
   1234 function v2
   1235 draw_text(str8 text, v2 pos, TextSpec *ts)
   1236 {
   1237 	if (ts->flags & TF_ROTATED) {
   1238 		rlPushMatrix();
   1239 		rlTranslatef(pos.x, pos.y, 0);
   1240 		rlRotatef(ts->rotation, 0, 0, 1);
   1241 		pos = (v2){0};
   1242 	}
   1243 
   1244 	v2 result   = measure_text(*ts->font, text);
   1245 	/* TODO(rnp): the size of this should be stored for each font */
   1246 	str8 ellipsis = str8("...");
   1247 	b32 clamped = ts->flags & TF_LIMITED && result.w > ts->limits.size.w;
   1248 	if (clamped) {
   1249 		f32 ellipsis_width = measure_text(*ts->font, ellipsis).x;
   1250 		if (ellipsis_width < ts->limits.size.w) {
   1251 			text = clamp_text_to_width(*ts->font, text, ts->limits.size.w - ellipsis_width);
   1252 		} else {
   1253 			text.length     = 0;
   1254 			ellipsis.length = 0;
   1255 		}
   1256 	}
   1257 
   1258 	Color colour = colour_from_normalized(ts->colour);
   1259 	if (ts->flags & TF_OUTLINED) result.x = draw_outlined_text(text, pos, ts).x;
   1260 	else                         result.x = draw_text_base(*ts->font, text, pos, colour).x;
   1261 
   1262 	if (clamped) {
   1263 		pos.x += result.x;
   1264 		if (ts->flags & TF_OUTLINED) result.x += draw_outlined_text(ellipsis, pos, ts).x;
   1265 		else                         result.x += draw_text_base(*ts->font, ellipsis, pos,
   1266 		                                                        colour).x;
   1267 	}
   1268 
   1269 	if (ts->flags & TF_ROTATED) rlPopMatrix();
   1270 
   1271 	return result;
   1272 }
   1273 
   1274 function b32
   1275 point_in_rect(v2 p, Rect r)
   1276 {
   1277 	v2  end    = v2_add(r.pos, r.size);
   1278 	b32 result = Between(p.x, r.pos.x, end.x) & Between(p.y, r.pos.y, end.y);
   1279 	return result;
   1280 }
   1281 
   1282 function v3
   1283 world_point_from_plane_uv(m4 world, v2 uv)
   1284 {
   1285 	v3 U   = world.c[0].xyz;
   1286 	v3 V   = world.c[1].xyz;
   1287 	v3 min = world.c[3].xyz;
   1288 	v3 result =  v3_add(v3_add(v3_scale(U, uv.x), v3_scale(V, uv.y)), min);
   1289 	return result;
   1290 }
   1291 
   1292 function v2
   1293 screen_point_to_world_2d(v2 p, v2 screen_min, v2 screen_max, v2 world_min, v2 world_max)
   1294 {
   1295 	v2 pixels_to_m = v2_div(v2_sub(world_max, world_min), v2_sub(screen_max, screen_min));
   1296 	v2 result      = v2_add(v2_mul(v2_sub(p, screen_min), pixels_to_m), world_min);
   1297 	return result;
   1298 }
   1299 
   1300 function v2
   1301 world_point_to_screen_2d(v2 p, v2 world_min, v2 world_max, v2 screen_min, v2 screen_max)
   1302 {
   1303 	v2 m_to_pixels = v2_div(v2_sub(screen_max, screen_min), v2_sub(world_max, world_min));
   1304 	v2 result      = v2_add(v2_mul(v2_sub(p, world_min), m_to_pixels), screen_min);
   1305 	return result;
   1306 }
   1307 
   1308 function void
   1309 draw_view_ruler(BeamformerFrameView *view, Arena a, Rect view_rect, TextSpec ts)
   1310 {
   1311 	// TODO(rnp): merge this into draw function, tons of duplicate code
   1312 	v2 vr_max_p = v2_add(view_rect.pos, view_rect.size);
   1313 
   1314 	v3 U   = view->frame.voxel_transform.c[0].xyz;
   1315 	v3 V   = view->frame.voxel_transform.c[1].xyz;
   1316 	v3 min = view->frame.voxel_transform.c[3].xyz;
   1317 
   1318 	v3 end = view->ruler.end;
   1319 	if (view->ruler.state != RulerState_Hold)
   1320 		end = world_point_from_plane_uv(view->frame.voxel_transform, rect_uv(ui_context->current_mouse, view_rect));
   1321 
   1322 	v2 start_uv = plane_uv(v3_sub(view->ruler.start, min), U, V);
   1323 	v2 end_uv   = plane_uv(v3_sub(end,               min), U, V);
   1324 
   1325 	v2 start_p  = v2_add(view_rect.pos, v2_mul(start_uv, view_rect.size));
   1326 	v2 end_p    = v2_add(view_rect.pos, v2_mul(end_uv,   view_rect.size));
   1327 
   1328 	b32 start_in_bounds = point_in_rect(start_p, view_rect);
   1329 	b32 end_in_bounds   = point_in_rect(end_p,   view_rect);
   1330 
   1331 	// TODO(rnp): this should be a ray intersection not a clamp
   1332 	start_p = clamp_v2_rect(start_p, view_rect);
   1333 	end_p   = clamp_v2_rect(end_p, view_rect);
   1334 
   1335 	Color rl_colour = colour_from_normalized(ts.colour);
   1336 	DrawLineEx(rl_v2(end_p), rl_v2(start_p), 2, rl_colour);
   1337 	if (start_in_bounds) DrawCircleV(rl_v2(start_p), 3, rl_colour);
   1338 	if (end_in_bounds)   DrawCircleV(rl_v2(end_p),   3, rl_colour);
   1339 
   1340 	Stream buf = arena_stream(a);
   1341 	stream_append_f64(&buf, 1e3 * v3_magnitude(v3_sub(end, view->ruler.start)), 100);
   1342 	stream_append_s8(&buf, s8(" mm"));
   1343 
   1344 	s8 s = stream_to_s8(&buf);
   1345 	v2 txt_p = start_p;
   1346 	v2 txt_s = measure_text(*ts.font, str8_from_s8(s));
   1347 	v2 pixel_delta = v2_sub(start_p, end_p);
   1348 	if (pixel_delta.y < 0) txt_p.y -= txt_s.y;
   1349 	if (pixel_delta.x < 0) txt_p.x -= txt_s.x;
   1350 	if (txt_p.x < view_rect.pos.x) txt_p.x = view_rect.pos.x;
   1351 	if (txt_p.x + txt_s.x > vr_max_p.x) txt_p.x -= (txt_p.x + txt_s.x) - vr_max_p.x;
   1352 
   1353 	draw_text(str8_from_s8(s), txt_p, &ts);
   1354 }
   1355 
   1356 function void
   1357 ui_event_consume(BeamformerInput *input, BeamformerInputEvent *current)
   1358 {
   1359 	BeamformerUI *ui = ui_context;
   1360 	BeamformerInputEvent *last = input->event_queue + input->event_count - 1;
   1361 	if Between(current, input->event_queue, last) {
   1362 		u64 index = current - input->event_queue;
   1363 		u64 bin   = index / (sizeof(ui->input_consumed[0]) * 8);
   1364 		u64 bit   = index % (sizeof(ui->input_consumed[0]) * 8);
   1365 		ui->input_consumed[bin] |= (1 << bit);
   1366 	}
   1367 }
   1368 
   1369 function BeamformerInputEvent *
   1370 ui_event_next(BeamformerInput *input, BeamformerInputEvent *current)
   1371 {
   1372 	BeamformerUI *ui = ui_context;
   1373 	BeamformerInputEvent *result = 0, *last = input->event_queue + input->event_count - 1;
   1374 
   1375 	current++;
   1376 	current = Max(current, input->event_queue);
   1377 
   1378 	for (; !result && Between(current, input->event_queue, last); current++) {
   1379 		u64 index = current - input->event_queue;
   1380 		u64 bin   = index / (sizeof(ui->input_consumed[0]) * 8);
   1381 		u64 bit   = index % (sizeof(ui->input_consumed[0]) * 8);
   1382 
   1383 		if (!(ui->input_consumed[bin] & (1 << bit)) &&
   1384 		    (current->kind == BeamformerInputEventKind_ButtonPress   ||
   1385 		     current->kind == BeamformerInputEventKind_ButtonRelease ||
   1386 		     current->kind == BeamformerInputEventKind_MouseScroll))
   1387 		{
   1388 			result = current;
   1389 		}
   1390 	}
   1391 	return result;
   1392 }
   1393 
   1394 function UINode *
   1395 ui_node_from_key(UINodeKey key)
   1396 {
   1397 	UINodeHashBucket *hb     = ui_context->node_hash_table + (key.value % UI_HASH_TABLE_COUNT);
   1398 	UINode           *result = &ui_node_nil;
   1399 
   1400 	for (UINode *b = hb->first; !ui_node_is_nil(b); b = b->hash_next) {
   1401 		if (ui_node_key_equal(b->key, key)) {
   1402 			result = b;
   1403 			break;
   1404 		}
   1405 	}
   1406 
   1407 	return result;
   1408 }
   1409 
   1410 function str8
   1411 ui_draw_part_from_key_string(str8 string)
   1412 {
   1413 	str8 result = string;
   1414 	i64 index = str8_find_needle(string, str8("##"), 0);
   1415 	if (index < string.length)
   1416 		result.length = index;
   1417 	return result;
   1418 }
   1419 
   1420 function str8
   1421 ui_hash_part_from_key_string(str8 string)
   1422 {
   1423 	str8 result = string;
   1424 	// NOTE(rnp): for xxx###yyy only use the ###yyy otherwise the whole string is hashed
   1425 	i64 index = str8_find_needle(string, str8("###"), 0);
   1426 	if (index < string.length)
   1427 		result = str8_skip(string, index);
   1428 	return result;
   1429 }
   1430 
   1431 function UINodeKey
   1432 ui_key_from_string(str8 string, UINodeKey seed)
   1433 {
   1434 	UINodeKey result = {0};
   1435 	if (string.length > 0) {
   1436 		str8 hash_string = ui_hash_part_from_key_string(string);
   1437 		result.value     = u64_hash_from_str8_seed(hash_string, seed.value);
   1438 	}
   1439 	return result;
   1440 }
   1441 
   1442 function Font
   1443 ui_font_for_node(UINode *node)
   1444 {
   1445 	Font result = node->font_size > 28.0f ? ui_context->font : ui_context->small_font;
   1446 	return result;
   1447 }
   1448 
   1449 function b32
   1450 ui_number_conversion_f64(str8 s, f64 *out_value)
   1451 {
   1452 	b32 result = 0;
   1453 	NumberConversion number = number_from_str8(s);
   1454 	if (number.result == NumberConversionResult_Success) {
   1455 		result     = 1;
   1456 		if (number.kind == NumberConversionKind_Float)
   1457 			*out_value = number.F64;
   1458 		else
   1459 			*out_value = (f64)number.S64;
   1460 	}
   1461 	return result;
   1462 }
   1463 
   1464 function iv2
   1465 ui_text_input_cursor_range(void)
   1466 {
   1467 	UITextInputState *tis = &ui_context->text_input_state;
   1468 	iv2 range;
   1469 	range.x = Min(tis->cursor, tis->mark);
   1470 	range.y = Max(tis->cursor, tis->mark);
   1471 	return range;
   1472 }
   1473 
   1474 function str8
   1475 ui_text_input_string(void)
   1476 {
   1477 	UITextInputState *tis = &ui_context->text_input_state;
   1478 	str8 result = {.data = tis->buffer, .length = tis->count};
   1479 	return result;
   1480 }
   1481 
   1482 function str8
   1483 ui_text_input_last_string(void)
   1484 {
   1485 	UITextInputState *tis = &ui_context->text_input_state;
   1486 	str8 result = {.data = tis->last_buffer, .length = tis->last_count};
   1487 	return result;
   1488 }
   1489 
   1490 function Rect
   1491 ui_text_input_rect(void)
   1492 {
   1493 	Rect result = ui_node_rect(ui_node_from_key(ui_context->text_input_state.node_key));
   1494 	f32 text_box_slop = 4.0f;
   1495 	result.pos.x  -= text_box_slop;
   1496 	result.size.x += 2 * text_box_slop;
   1497 	return result;
   1498 }
   1499 
   1500 function i32
   1501 ui_text_input_index_from_point(f32 point)
   1502 {
   1503 	i32 result = 0;
   1504 
   1505 	// TODO(rnp): visible range, extended virtual rect which exactly fits the visible text
   1506 	UITextInputState *tis = &ui_context->text_input_state;
   1507 	Rect r = ui_text_input_rect();
   1508 
   1509 	Font font = ui_font_for_node(ui_node_from_key(tis->node_key));
   1510 
   1511 	/* NOTE: extra offset to help with putting a cursor at idx 0 */
   1512 	f32 pct   = Clamp01((point - r.pos.x) / r.size.w);
   1513 	f32 x_off = 10.0f, x_bounds = r.size.w * pct;
   1514 	for (; result < tis->count && x_off < x_bounds; result++) {
   1515 		/* NOTE: assumes font glyphs are ordered ASCII */
   1516 		i32 idx  = tis->buffer[result] - 0x20;
   1517 		x_off   += (f32)font.glyphs[idx].advanceX;
   1518 		if (font.glyphs[idx].advanceX == 0)
   1519 			x_off += font.recs[idx].width;
   1520 	}
   1521 
   1522 	return result;
   1523 }
   1524 
   1525 function void
   1526 ui_text_input_end(void)
   1527 {
   1528 	UITextInputState *tis = &ui_context->text_input_state;
   1529 
   1530 	UINode *next_node     = ui_node_from_key(tis->next_node_key);
   1531 	str8 new_input_string = str8("");
   1532 	if ((next_node->flags & UINodeFlag_TextInputClearOnStart) == 0)
   1533 		new_input_string = ui_draw_part_from_key_string(next_node->string);
   1534 
   1535 	tis->cursor = tis->mark = 0;
   1536 	tis->last_count = tis->count;
   1537 	tis->count      = Min(new_input_string.length, countof(tis->buffer));
   1538 	tis->numeric    = (next_node->flags & UINodeFlag_TextInputNumeric) != 0;
   1539 	memory_copy(tis->last_buffer, tis->buffer, tis->last_count);
   1540 	memory_copy(tis->buffer, new_input_string.data, tis->count);
   1541 
   1542 	tis->last_node_key = tis->node_key;
   1543 	tis->node_key      = ui_node_key_zero();
   1544 }
   1545 
   1546 function void
   1547 ui_text_input_insert(str8 text)
   1548 {
   1549 	UITextInputState *tis = &ui_context->text_input_state;
   1550 	iv2 cursor_range       = ui_text_input_cursor_range();
   1551 	i64 bytes_after_cursor = tis->count - cursor_range.y;
   1552 	i64 remaining_length   = ((i32)countof(tis->buffer) - cursor_range.x) - bytes_after_cursor;
   1553 	i64 truncated_length   = Min(remaining_length, text.length);
   1554 
   1555 	memory_move(tis->buffer + cursor_range.x + truncated_length,
   1556 	            tis->buffer + cursor_range.y, bytes_after_cursor);
   1557 	memory_copy(tis->buffer + cursor_range.x, text.data, truncated_length);
   1558 
   1559 	tis->count -= cursor_range.y - cursor_range.x;
   1560 	tis->count += truncated_length;
   1561 	tis->cursor = tis->mark = cursor_range.x + truncated_length;
   1562 }
   1563 
   1564 function b32
   1565 ui_text_input_update(BeamformerInput *input)
   1566 {
   1567 	UITextInputState *tis = &ui_context->text_input_state;
   1568 
   1569 	Arena  scratch = *ui_build_arena();
   1570 	Stream sb = arena_stream(scratch);
   1571 
   1572 	enum {
   1573 		DeltaPicksSide  = (1 << 0),
   1574 		WordScan        = (1 << 1),
   1575 		Delete          = (1 << 2),
   1576 		KeepMark        = (1 << 3),
   1577 		Copy            = (1 << 4),
   1578 		Paste           = (1 << 5),
   1579 	};
   1580 
   1581 	i32 delta = 0;
   1582 	u32 flags = 0;
   1583 
   1584 	b32 result = 0;
   1585 
   1586 	// NOTE(rnp): first pass, non uniform inputs
   1587 	for (BeamformerInputEvent *event = ui_event_next(input, 0);
   1588 	     event;
   1589 	     event = ui_event_next(input, event))
   1590 	{
   1591 		b32 taken = 0;
   1592 
   1593 		BeamformerInputModifiers mods = event->modifiers;
   1594 		if (event->kind == BeamformerInputEventKind_ButtonPress) {
   1595 			if (mods & BeamformerInputModifier_Control)
   1596 				flags |= WordScan;
   1597 
   1598 			if (mods & BeamformerInputModifier_Shift)
   1599 				flags |= KeepMark;
   1600 
   1601 			switch (event->button_id) {
   1602 			default:{}break;
   1603 			case BeamformerButtonID_Escape:
   1604 			case BeamformerButtonID_Enter:
   1605 			{
   1606 				taken  = 1;
   1607 				result = 1;
   1608 			}break;
   1609 
   1610 			case BeamformerButtonID_A: if (mods & BeamformerInputModifier_Control) {
   1611 				tis->cursor = 0;
   1612 				tis->mark   = tis->count;
   1613 				taken       = 1;
   1614 			}break;
   1615 
   1616 			case BeamformerButtonID_C: if (mods & BeamformerInputModifier_Control) {
   1617 				flags |= Copy;
   1618 				taken  = 1;
   1619 			}break;
   1620 
   1621 			case BeamformerButtonID_V: if (mods & BeamformerInputModifier_Control) {
   1622 				flags |= Paste;
   1623 				taken  = 1;
   1624 			}break;
   1625 
   1626 			case BeamformerButtonID_X: if (mods & BeamformerInputModifier_Control) {
   1627 				flags |= Copy|Delete|KeepMark;
   1628 				taken  = 1;
   1629 			}break;
   1630 
   1631 			case BeamformerButtonID_Backspace:{
   1632 				delta -= 1;
   1633 				flags |= Delete|KeepMark;
   1634 				taken  = 1;
   1635 			}break;
   1636 
   1637 			case BeamformerButtonID_Delete:{
   1638 				delta += 1;
   1639 				flags |= Delete|KeepMark;
   1640 				taken  = 1;
   1641 			}break;
   1642 
   1643 			case BeamformerButtonID_Left:{
   1644 				delta -= 1;
   1645 				flags |= DeltaPicksSide;
   1646 				taken  = 1;
   1647 			}break;
   1648 
   1649 			case BeamformerButtonID_Right:{
   1650 				delta += 1;
   1651 				flags |= DeltaPicksSide;
   1652 				taken  = 1;
   1653 			}break;
   1654 
   1655 			}
   1656 
   1657 			if (!taken && event->codepoint) {
   1658 				u32 cp = event->codepoint;
   1659 				taken = !tis->numeric || (Between(cp, '0', '9') || (cp == '.') || (cp == '-' && tis->cursor == 0));
   1660 				if (taken) stream_append_codepoint(&sb, event->codepoint);
   1661 			}
   1662 		}
   1663 
   1664 		if (taken) ui_event_consume(input, event);
   1665 	}
   1666 
   1667 	if (flags & Paste) {
   1668 		str8 string;
   1669 		string.data = os_get_clipboard_text(&string.length);
   1670 		for (i64 it = 0; it < string.length; it++) {
   1671 			u8 cp = string.data[it];
   1672 			if (!tis->numeric || (Between(cp, '0', '9') || (cp == '.') || (cp == '-' && tis->cursor == 0)))
   1673 				stream_append_byte(&sb, cp);
   1674 		}
   1675 	}
   1676 
   1677 	if (flags & Copy) {
   1678 		str8 string = ui_text_input_string();
   1679 		os_set_clipboard_text(string.data, string.length);
   1680 	}
   1681 
   1682 	if ((flags & Delete) && tis->mark != tis->cursor)
   1683 		delta = 0;
   1684 
   1685 	// TODO(rnp): word selection
   1686 	tis->mark += delta;
   1687 	tis->mark  = Clamp(tis->mark, 0, tis->count);
   1688 
   1689 	if (!(flags & KeepMark) && delta) {
   1690 		i32 new_cursor = tis->mark;
   1691 		if (flags & DeltaPicksSide) {
   1692 			if (delta < 0) new_cursor = Min(tis->mark, tis->cursor);
   1693 			if (delta > 0) new_cursor = Max(tis->mark, tis->cursor);
   1694 		}
   1695 		tis->mark = tis->cursor = new_cursor;
   1696 	}
   1697 
   1698 	if ((flags & Delete) || sb.widx)
   1699 		ui_text_input_insert(stream_to_str8(&sb));
   1700 
   1701 	if (flags || delta || sb.widx)
   1702 		tis->blinker.t = 1.0;
   1703 
   1704 	return result;
   1705 }
   1706 
   1707 function void
   1708 ui_context_menu_close(void)
   1709 {
   1710 	ui_context->context_menu_next_anchor_key = ui_node_key_zero();
   1711 	ui_context->context_menu_state_changed   = 1;
   1712 	ui_context->context_menu_next_panel      = 0;
   1713 }
   1714 
   1715 function void
   1716 ui_context_menu_open(UINodeKey anchor_node_key, BeamformerUIPanel *panel)
   1717 {
   1718 	if (ui_node_key_equal(ui_context->context_menu_anchor_key, anchor_node_key)) {
   1719 		ui_context_menu_close();
   1720 	} else {
   1721 		ui_context->context_menu_next_anchor_key = anchor_node_key;
   1722 		ui_context->context_menu_next_panel      = panel;
   1723 		ui_context->context_menu_state_changed   = 1;
   1724 		ui_context->context_menu_open_t          = 0;
   1725 	}
   1726 }
   1727 
   1728 function void
   1729 ui_drag_end(void)
   1730 {
   1731 	if ((beamformer_registers()->split_left_tree != beamformer_registers()->split_right_tree) &&
   1732 	     ui_context->drag_panel)
   1733 	{
   1734 		beamformer_command(beamformer_command_infos[BeamformerCommandKind_SplitTree].string,
   1735 		                   .tree_node = (u64)ui_context->drag_panel);
   1736 	} else if (beamformer_registers()->drop_target_tree && ui_context->drag_panel) {
   1737 		beamformer_command(beamformer_command_infos[BeamformerCommandKind_MoveTab].string,
   1738 		                   .tree_node = (u64)ui_context->drag_panel);
   1739 	}
   1740 	ui_context->drag_panel = 0;
   1741 	ui_context->drag_end   = 0;
   1742 }
   1743 
   1744 function void
   1745 ui_drag_begin(BeamformerUIPanel *panel)
   1746 {
   1747 	if (!ui_context->drag_panel) {
   1748 		ui_context->drag_panel  = panel;
   1749 		ui_context->drag_open_t = 0;
   1750 		ui_context->drop_target_key = ui_node_key_zero();
   1751 		beamformer_registers()->drop_target_tree = 0;
   1752 	}
   1753 }
   1754 
   1755 function v2
   1756 ui_node_final_position(UINode *node)
   1757 {
   1758 	v2 result = ui_node_rect(node).pos;
   1759 	for (UINode *p = node->parent; !ui_node_is_nil(p); p = p->parent)
   1760 		if (p->flags & UINodeFlag_ViewScroll)
   1761 			result = v2_sub(result, p->view_scroll_offset);
   1762 	return result;
   1763 }
   1764 
   1765 function UISignal
   1766 ui_signal_from_node(UINode *node)
   1767 {
   1768 	BeamformerUI    *ui    = ui_context;
   1769 	BeamformerInput *input = beamformer_input;
   1770 
   1771 	UISignal result = {.node = node};
   1772 	Rect nr = ui_node_rect(node);
   1773 
   1774 	// NOTE(rnp): use the last mouse as this matches what the user saw when they positioned
   1775 	v2 mouse = ui->last_mouse;
   1776 
   1777 	// NOTE(rnp): apply offset
   1778 	nr.pos = ui_node_final_position(node);
   1779 
   1780 	// NOTE(rnp): apply clipping
   1781 	for (UINode *p = node->parent; !ui_node_is_nil(p); p = p->parent)
   1782 		if (p->flags & UINodeFlag_Clip)
   1783 			nr = rect_intersect(nr, ui_node_rect(p));
   1784 
   1785 	// NOTE(rnp): filter when node is under context menu
   1786 	b32 context_menu_descendent = 0;
   1787 	for (UINode *p = node->parent; !ui_node_is_nil(p); p = p->parent)
   1788 		if (p == ui->context_menu_root)
   1789 			context_menu_descendent = 1;
   1790 
   1791 	Rect filter_rect = {0};
   1792 	if (!context_menu_descendent && !ui_node_key_nil(ui->context_menu_anchor_key))
   1793 		filter_rect = ui_node_rect(ui->context_menu_root);
   1794 
   1795 	b32 disabled = (node->flags & UINodeFlag_Disabled) != 0;
   1796 	b32 collides = point_in_rect(mouse, nr) && !point_in_rect(mouse, filter_rect);
   1797 
   1798 	result.flags |= collides * UISignalFlag_Hovering;
   1799 
   1800 	if (!disabled)
   1801 	for (BeamformerInputEvent *event = ui_event_next(input, 0);
   1802 	     event;
   1803 	     event = ui_event_next(input, event))
   1804 	{
   1805 		b32 taken   = 0;
   1806 		b32 press   = event->kind == BeamformerInputEventKind_ButtonPress;
   1807 		b32 release = event->kind == BeamformerInputEventKind_ButtonRelease;
   1808 		b32 event_is_mouse = (press || release) && (
   1809 		                     event->button_id == BeamformerButtonID_MouseLeft   ||
   1810 		                     event->button_id == BeamformerButtonID_MouseRight  ||
   1811 		                     event->button_id == BeamformerButtonID_MouseMiddle ||
   1812 		                     (0));
   1813 		UIMouseButtonKind mouse_button = (event->button_id == BeamformerButtonID_MouseLeft   ? UIMouseButtonKind_Left :
   1814 		                                  event->button_id == BeamformerButtonID_MouseRight  ? UIMouseButtonKind_Right :
   1815 		                                  event->button_id == BeamformerButtonID_MouseMiddle ? UIMouseButtonKind_Middle :
   1816 		                                  UIMouseButtonKind_Left);
   1817 
   1818 		if ((node->flags & UINodeFlag_MouseClickable) && event_is_mouse && press && collides) {
   1819 			ui->hot_node_key                  = node->key;
   1820 			ui->active_node_key[mouse_button] = node->key;
   1821 
   1822 			// TODO(rnp): store timestamp
   1823 			// TODO(rnp): check with timestamp for double/triple click
   1824 
   1825 			result.flags |= UISignalFlag_LeftPressed << mouse_button;
   1826 
   1827 			taken = 1;
   1828 		}
   1829 
   1830 		// NOTE(rnp): release, applies whenever this node is active regardless of in bounds or not.
   1831 		if ((node->flags & UINodeFlag_MouseClickable) && event_is_mouse && release &&
   1832 		     ui_node_key_equal(ui->active_node_key[mouse_button], node->key))
   1833 		{
   1834 			ui->hot_node_key                  = ui_node_key_zero();
   1835 			ui->active_node_key[mouse_button] = ui_node_key_zero();
   1836 			result.flags |= UISignalFlag_LeftReleased << mouse_button;
   1837 
   1838 			taken = 1;
   1839 		}
   1840 
   1841 		// NOTE(rnp): custom scroll handling
   1842 		if (node->flags & UINodeFlag_Scroll && event->kind == BeamformerInputEventKind_MouseScroll && collides) {
   1843 			v2 delta = {{event->scroll.x, event->scroll.y}};
   1844 			// TODO(rnp): glfw doesn't pass these through
   1845 			if (event->modifiers & BeamformerInputModifier_Shift)
   1846 				swap(delta.x, delta.y);
   1847 			result.scroll = v2_add(result.scroll, delta);
   1848 
   1849 			taken = 1;
   1850 		}
   1851 
   1852 		// NOTE(rnp): scrollable container handling
   1853 		if (node->flags & UINodeFlag_ViewScroll && collides) {
   1854 			v2 delta = {{event->scroll.x, event->scroll.y}};
   1855 			// TODO(rnp): glfw doesn't pass these through
   1856 			if (event->modifiers & BeamformerInputModifier_Shift)
   1857 				swap(delta.x, delta.y);
   1858 
   1859 			// NOTE(rnp): if the view only has scroll in one direction we ignore the delta's direction
   1860 
   1861 			if ((node->flags & UINodeFlag_ViewScrollX) == 0) {
   1862 				if f32_equal(delta.y, 0)
   1863 					delta.y = delta.x;
   1864 				delta.x = 0;
   1865 			}
   1866 
   1867 			if ((node->flags & UINodeFlag_ViewScrollY) == 0) {
   1868 				if f32_equal(delta.x, 0)
   1869 					delta.x = delta.y;
   1870 				delta.y = 0;
   1871 			}
   1872 
   1873 			node->view_scroll_offset = v2_add(node->view_scroll_offset, v2_scale(delta, -10.f));
   1874 			taken = 1;
   1875 		}
   1876 
   1877 		if (taken) ui_event_consume(input, event);
   1878 	}
   1879 
   1880 	// NOTE(rnp): single click dragging
   1881 	if (node->flags & UINodeFlag_MouseClickable) {
   1882 		for EachEnumValue(UIMouseButtonKind, k) {
   1883 			if (ui_node_key_equal(ui->active_node_key[k], node->key) ||
   1884 	        result.flags & (UISignalFlag_LeftPressed << k))
   1885 			{
   1886 				result.flags |= (UISignalFlag_LeftDragging << k);
   1887 			}
   1888 		}
   1889 	}
   1890 
   1891 	// NOTE(rnp): drop handling
   1892 	if (node->flags & UINodeFlag_DropSite && collides
   1893 	    && ui_node_key_equal(ui->drop_target_key, ui_node_key_zero()))
   1894 	{
   1895 		ui->drop_target_key = node->key;
   1896 	}
   1897 
   1898 	if (node->flags & UINodeFlag_DropSite && !collides
   1899 	    && ui_node_key_equal(ui->drop_target_key, node->key))
   1900 	{
   1901 		ui->drop_target_key = ui_node_key_zero();
   1902 	}
   1903 
   1904 	// TODO(rnp): double click dragging
   1905 
   1906 	// TODO(rnp): triple click dragging
   1907 
   1908 	result.flags |= (!f32_equal(0, result.scroll.x) * UISignalFlag_ScrolledX);
   1909 	result.flags |= (!f32_equal(0, result.scroll.y) * UISignalFlag_ScrolledY);
   1910 
   1911 	if (node->flags & UINodeFlag_MouseClickable && collides &&
   1912 	    (ui_node_key_nil(ui->hot_node_key) || ui_node_key_equal(ui->hot_node_key, node->key)) &&
   1913 	    (ui_node_key_nil(ui->active_node_key[UIMouseButtonKind_Left])   || ui_node_key_equal(ui->active_node_key[UIMouseButtonKind_Left],   node->key)) &&
   1914 	    (ui_node_key_nil(ui->active_node_key[UIMouseButtonKind_Middle]) || ui_node_key_equal(ui->active_node_key[UIMouseButtonKind_Middle], node->key)) &&
   1915 	    (ui_node_key_nil(ui->active_node_key[UIMouseButtonKind_Right])  || ui_node_key_equal(ui->active_node_key[UIMouseButtonKind_Right],  node->key)))
   1916 	{
   1917 		ui->hot_node_key = node->key;
   1918 	}
   1919 
   1920 	if (node->flags & UINodeFlag_ViewScroll) {
   1921 		v2  offset  = node->view_scroll_offset;
   1922 		f32 clamp_x = Max(0, node->computed_size[Axis2_X] - node->parent->computed_size[Axis2_X]);
   1923 		f32 clamp_y = Max(0, node->computed_size[Axis2_Y] - node->parent->computed_size[Axis2_Y]);
   1924 		node->view_scroll_offset.x = Max(0, Sign(offset.x) * Min(Abs(offset.x), clamp_x));
   1925 		node->view_scroll_offset.y = Max(0, Sign(offset.y) * Min(Abs(offset.y), clamp_y));
   1926 	}
   1927 
   1928 	// NOTE(rnp): activate text input
   1929 	if (ui_pressed(result) && !ui_node_key_equal(ui->text_input_state.node_key, node->key)) {
   1930 		ui->text_input_state.changed       = 1;
   1931 		ui->text_input_state.next_node_key = node->flags & UINodeFlag_TextInput ? node->key : ui_node_key_zero();
   1932 	}
   1933 
   1934 	// NOTE(rnp): signal ended text input
   1935 	if (node->flags & UINodeFlag_TextInput &&
   1936 	    ui_node_key_equal(ui->text_input_state.last_node_key, node->key))
   1937 	{
   1938 		result.flags  |= UISignalFlag_TextCommit;
   1939 		result.string  = (str8){.length = ui->text_input_state.last_count,
   1940 		                        .data   = ui->text_input_state.last_buffer};
   1941 	}
   1942 
   1943 	if (ui_pressed(result) && !context_menu_descendent)
   1944 		ui_context_menu_close();
   1945 
   1946 	if (!disabled) {
   1947 		b32 hot = ui_node_key_equal(ui->hot_node_key, node->key);
   1948 		if (hot) node->hot_t += HOVER_SPEED * dt_for_frame;
   1949 		else     node->hot_t -= HOVER_SPEED * dt_for_frame;
   1950 		node->hot_t = Clamp01(node->hot_t);
   1951 	}
   1952 
   1953 	return result;
   1954 }
   1955 
   1956 function UINode *
   1957 ui_build_node_from_key(UINodeFlags flags, UINodeKey key)
   1958 {
   1959 	UINode *result = ui_node_from_key(key);
   1960 
   1961 	b32 first_frame = ui_node_is_nil(result);
   1962 	b32 transient   = ui_node_key_equal(key, ui_node_key_zero());
   1963 
   1964 	assert(first_frame || result->last_frame_active_index != ui_context->current_frame_index);
   1965 
   1966 	if (first_frame) {
   1967 		result = transient ? 0 : ui_context->node_freelist;
   1968 		if (!ui_node_is_nil(result)) {
   1969 			SLLStackPop(ui_context->node_freelist, next_sibling);
   1970 		} else {
   1971 			result = push_struct_no_zero(transient ? ui_build_arena() : &ui_context->arena, UINode);
   1972 		}
   1973 		zero_struct(result);
   1974 	}
   1975 
   1976 	// NOTE(rnp): reassigned per frame
   1977 	{
   1978 		result->parent = result->first_child = result->last_child = &ui_node_nil;
   1979 		result->next_sibling = result->previous_sibling = &ui_node_nil;
   1980 		result->child_count = 0;
   1981 	}
   1982 
   1983 	if (first_frame && !transient) {
   1984 		UINodeHashBucket *hb = ui_context->node_hash_table + (key.value % UI_HASH_TABLE_COUNT);
   1985 		DLLInsert(&ui_node_nil, hb->first, hb->last, result, hash_next, hash_prev);
   1986 	}
   1987 
   1988 	#define X(type, name, value_type, ...) result->name = ui_top_##name();
   1989 	UI_STACK_LIST
   1990 	#undef X
   1991 
   1992 	result->last_frame_active_index = ui_context->current_frame_index;
   1993 	result->key = key;
   1994 	result->flags |= flags;
   1995 
   1996 	if (!ui_node_is_nil(result->parent)) {
   1997 		DLLInsertLast(&ui_node_nil, result->parent->first_child, result->parent->last_child,
   1998 		              result, next_sibling, previous_sibling);
   1999 		result->parent->child_count++;
   2000 	}
   2001 
   2002 	return result;
   2003 }
   2004 
   2005 function UINode *
   2006 ui_node_from_string(UINodeFlags flags, str8 string)
   2007 {
   2008 	UINode *result = ui_build_node_from_key(flags, ui_key_from_string(string, ui_node_ancestor_key()));
   2009 	if (flags & UINodeFlag_DrawText) {
   2010 		if (ui_node_key_equal(ui_context->text_input_state.node_key, result->key))
   2011 			result->string = ui_text_input_string();
   2012 		else if (ui_node_key_equal(ui_context->text_input_state.last_node_key, result->key))
   2013 			result->string = ui_text_input_last_string();
   2014 		else
   2015 			result->string = string;
   2016 	}
   2017 	return result;
   2018 }
   2019 
   2020 function print_format(2, 3) UINode *
   2021 ui_node_from_stringf(UINodeFlags flags, const char *format, ...)
   2022 {
   2023 	va_list args;
   2024 	va_start(args, format);
   2025 	str8 string = push_str8_fv(ui_build_arena(), format, args);
   2026 	va_end(args);
   2027 	UINode *result = ui_node_from_string(flags, string);
   2028 	return result;
   2029 }
   2030 
   2031 typedef struct {
   2032 	f32 percent;
   2033 } UIDrawSliderData;
   2034 
   2035 function UI_CUSTOM_DRAW_FUNCTION(ui_custom_draw_slider)
   2036 {
   2037 	UIDrawSliderData *data = node->custom_draw_context;
   2038 
   2039 	f32  pct             = data->percent;
   2040 	f32  border_thick    = 3.0f;
   2041 	f32  bar_height_frac = 0.8f;
   2042 	v2   bar_size        = {{6.0f, bar_height_frac * node_rect.size.y}};
   2043 
   2044 	Rect inner  = rect_shrink_centered(node_rect, (v2){{2.0f * border_thick, // NOTE(rnp): raylib jank
   2045 	                                                    Max(0, 2.0f * (node_rect.size.y - bar_size.y))}});
   2046 	Rect filled = inner;
   2047 	filled.size.w *= pct;
   2048 
   2049 	Rect bar;
   2050 
   2051 	bar.pos  = v2_add(node_rect.pos, (v2){{pct * (node_rect.size.w - bar_size.w),
   2052 	                                       (1 - bar_height_frac) * 0.5f * node_rect.size.y}});
   2053 	bar.size = bar_size;
   2054 	v4 bar_colour = v4_lerp(FG_COLOUR, FOCUSED_COLOUR, node->hot_t);
   2055 
   2056 	DrawRectangleRec(rl_rect(filled), colour_from_normalized(node->bg_colour));
   2057 	DrawRectangleRoundedLinesEx(rl_rect(inner), 0.2f, 0, border_thick, BLACK);
   2058 	DrawRectangleRounded(rl_rect(bar), 0.6f, 1, colour_from_normalized(bar_colour));
   2059 }
   2060 
   2061 function UISignal
   2062 ui_slider(f32 percent, str8 tag)
   2063 {
   2064 	UINode *slider = ui_node_from_string(UINodeFlag_Clickable|
   2065 	                                     UINodeFlag_Scroll|
   2066 	                                     UINodeFlag_CustomDraw, tag);
   2067 	// TODO(rnp): don't need custom draw for this when individual borders can be specified
   2068 	slider->custom_draw_function = ui_custom_draw_slider;
   2069 	slider->custom_draw_context  = push_struct(ui_build_arena(), UIDrawSliderData);
   2070 	UIDrawSliderData *data = slider->custom_draw_context;
   2071 	data->percent = percent;
   2072 
   2073 	UISignal result = ui_signal_from_node(slider);
   2074 	return result;
   2075 }
   2076 
   2077 function print_format(2, 3) UISignal
   2078 ui_sliderf(f32 percent, const char *format, ...)
   2079 {
   2080 	va_list args;
   2081 	va_start(args, format);
   2082 	str8 string = push_str8_fv(ui_build_arena(), format, args);
   2083 	va_end(args);
   2084 	UISignal result = ui_slider(percent, string);
   2085 	return result;
   2086 }
   2087 
   2088 function UISignal
   2089 ui_button(str8 string)
   2090 {
   2091 	UINode *node = ui_node_from_string(UINodeFlag_Clickable|
   2092 	                                   UINodeFlag_DrawBackground|
   2093 	                                   UINodeFlag_DrawBorder|
   2094 	                                   UINodeFlag_DrawText|
   2095 	                                   UINodeFlag_DrawHotEffects|
   2096 	                                   UINodeFlag_DrawActiveEffects,
   2097 	                                   string);
   2098 	UISignal result = ui_signal_from_node(node);
   2099 	return result;
   2100 }
   2101 
   2102 function print_format(1, 2) UISignal
   2103 ui_buttonf(const char *format, ...)
   2104 {
   2105 	va_list args;
   2106 	va_start(args, format);
   2107 	str8 string = push_str8_fv(ui_build_arena(), format, args);
   2108 	va_end(args);
   2109 	UISignal result = ui_button(string);
   2110 	return result;
   2111 }
   2112 
   2113 function UISignal
   2114 ui_toggle_button(b32 state, str8 string)
   2115 {
   2116 	UINode *node, *outer;
   2117 
   2118 	UIAxisAlign(Axis2_Y, Center)
   2119 	UIAxisAlign(Axis2_X, Center)
   2120 	UIParent(ui_spacer(0))
   2121 	{
   2122 		UIPrefHeight(ui_pct(0.75f, 1.f))
   2123 		UIPrefWidth(ui_pct(0.75f, 1.f))
   2124 		UIBorderThickness(2.f)
   2125 		UIBorderColour(FG_COLOUR)
   2126 		outer = ui_node_from_string(UINodeFlag_Clickable|UINodeFlag_DrawBorder,
   2127 		                            push_str8_from_parts(ui_build_arena(), str8(""), string, str8("_outer")));
   2128 
   2129 		UIParent(outer)
   2130 		UIPrefHeight(ui_pct(0.46f, 1.f))
   2131 		UIPrefWidth(ui_pct(0.46f, 1.f))
   2132 		UIBGColour(state ? FG_COLOUR : (v4){0})
   2133 		{
   2134 			node = ui_node_from_string(UINodeFlag_DrawBackground|
   2135 			                           UINodeFlag_DrawHotEffects|
   2136 			                           UINodeFlag_DrawActiveEffects,
   2137 			                           string);
   2138 			node->hot_t = outer->hot_t;
   2139 		}
   2140 	}
   2141 
   2142 	UISignal result = ui_signal_from_node(outer);
   2143 	return result;
   2144 }
   2145 
   2146 function print_format(2, 3) UISignal
   2147 ui_toggle_buttonf(b32 state, const char *format, ...)
   2148 {
   2149 	va_list args;
   2150 	va_start(args, format);
   2151 	str8 string = push_str8_fv(ui_build_arena(), format, args);
   2152 	va_end(args);
   2153 	UISignal result = ui_toggle_button(state, string);
   2154 	return result;
   2155 }
   2156 
   2157 function UISignal
   2158 ui_label(str8 string)
   2159 {
   2160 	UINode *node = ui_node_from_string(UINodeFlag_DrawText, string);
   2161 	UISignal result = ui_signal_from_node(node);
   2162 	return result;
   2163 }
   2164 
   2165 function print_format(1, 2) UISignal
   2166 ui_labelf(const char *format, ...)
   2167 {
   2168 	va_list args;
   2169 	va_start(args, format);
   2170 	str8 string = push_str8_fv(ui_build_arena(), format, args);
   2171 	va_end(args);
   2172 	UISignal result = ui_label(string);
   2173 	return result;
   2174 }
   2175 
   2176 function UISignal
   2177 ui_label_button(str8 string)
   2178 {
   2179 	UINode *node = ui_node_from_string(UINodeFlag_DrawText|
   2180 	                                   UINodeFlag_Clickable|
   2181 	                                   UINodeFlag_DrawHotEffects|
   2182 	                                   UINodeFlag_DrawActiveEffects,
   2183 	                                   string);
   2184 	UISignal result = ui_signal_from_node(node);
   2185 	return result;
   2186 }
   2187 
   2188 function print_format(1, 2) UISignal
   2189 ui_label_buttonf(const char *format, ...)
   2190 {
   2191 	va_list args;
   2192 	va_start(args, format);
   2193 	str8 string = push_str8_fv(ui_build_arena(), format, args);
   2194 	va_end(args);
   2195 	UISignal result = ui_label_button(string);
   2196 	return result;
   2197 }
   2198 
   2199 function UISignal
   2200 ui_text_box(str8 string)
   2201 {
   2202 	UINode *node = ui_node_from_string(UINodeFlag_TextInput|
   2203 	                                   UINodeFlag_DrawText|
   2204 	                                   UINodeFlag_Clickable|
   2205 	                                   UINodeFlag_DrawHotEffects|
   2206 	                                   UINodeFlag_DrawActiveEffects,
   2207 	                                   string);
   2208 	UISignal result = ui_signal_from_node(node);
   2209 	return result;
   2210 }
   2211 
   2212 function print_format(1, 2) UISignal
   2213 ui_text_boxf(const char *format, ...)
   2214 {
   2215 	va_list args;
   2216 	va_start(args, format);
   2217 	str8 string = push_str8_fv(ui_build_arena(), format, args);
   2218 	va_end(args);
   2219 	UISignal result = ui_text_box(string);
   2220 	return result;
   2221 }
   2222 
   2223 function b32
   2224 ui_tweak_f32_compute_variable(UISignal signal, f32 *value, f32 text_scale, f32 scroll_scale, v2 limits)
   2225 {
   2226 	b32 result = 0;
   2227 	if (signal.flags) {
   2228 		f64 new_value = *value;
   2229 		if (signal.flags & UISignalFlag_TextCommit && ui_number_conversion_f64(signal.string, &new_value))
   2230 			new_value *= text_scale;
   2231 
   2232 		if (signal.flags & UISignalFlag_ScrolledY)
   2233 			new_value += scroll_scale * signal.scroll.y;
   2234 
   2235 		new_value = Clamp(new_value, limits.x, limits.y);
   2236 
   2237 		result = !f32_equal(*value, (f32)new_value);
   2238 		*value = (f32)new_value;
   2239 	}
   2240 	return result;
   2241 }
   2242 
   2243 typedef struct {
   2244 	v2 uv_start;
   2245 	v2 uv_end;
   2246 	BeamformerFrameView *view;
   2247 } BeamformerCustomDrawFrameViewData;
   2248 
   2249 function UI_CUSTOM_DRAW_FUNCTION(beamformer_custom_draw_frame_view)
   2250 {
   2251 	// TODO(rnp): we should always just draw inline, requires no raylib
   2252 	BeamformerCustomDrawFrameViewData *data = node->custom_draw_context;
   2253 	BeamformerFrameView *view = data->view;
   2254 	Rectangle tex_r = {
   2255 		data->uv_start.x * view->colour_image.width,
   2256 		data->uv_start.y * view->colour_image.height,
   2257 		data->uv_end.x   * view->colour_image.width,
   2258 		data->uv_end.y   * view->colour_image.height,
   2259 	};
   2260 	NPatchInfo tex_np = { tex_r, 0, 0, 0, 0, NPATCH_NINE_PATCH };
   2261 	DrawTextureNPatch(make_raylib_texture(view), tex_np, rl_rect(node_rect), (Vector2){0}, 0, WHITE);
   2262 
   2263 	TextSpec text_spec = {.font = &ui_context->small_font, .flags = TF_LIMITED|TF_OUTLINED,
   2264 	                      .colour = RULER_COLOUR, .outline_thick = 1, .outline_colour.a = 1,
   2265 	                      .limits.size.x = node_rect.size.w};
   2266 	if (view->kind != BeamformerFrameViewKind_3DXPlane && view->ruler.state != RulerState_None)
   2267 		draw_view_ruler(view, *ui_build_arena(), node_rect, text_spec);
   2268 }
   2269 
   2270 function b32
   2271 ui_rebuild_das_transform(u32 parameter_block, i32 dimension, v3 min, v3 max)
   2272 {
   2273 	BeamformerUI *ui = ui_context;
   2274 
   2275 	b32 result = 0;
   2276 	m4 new_transform = m4_identity();
   2277 
   2278 	BeamformerParameterBlock *pb = beamformer_parameter_block(beamformer_context->shared_memory, parameter_block);
   2279 
   2280 	m4 das_transform = pb->parameters.das_voxel_transform;
   2281 
   2282 	switch (dimension) {
   2283 	case 1:{new_transform = das_transform_1d(min, max);}break;
   2284 	case 3:{new_transform = das_transform_3d(min, max);}break;
   2285 	case 2:{
   2286 		v3 U = v3_normalize(das_transform.c[0].xyz);
   2287 		v3 V = v3_normalize(das_transform.c[1].xyz);
   2288 		v3 N = cross(V, U);
   2289 
   2290 		v2 min_2d = {{min.E[0], min.E[1]}};
   2291 		v2 max_2d = {{max.E[0], max.E[1]}};
   2292 
   2293 		new_transform = das_transform_2d_with_normal(N, min_2d, max_2d, 0);
   2294 
   2295 		v3 rotation_axis = cross(v3_normalize(new_transform.c[0].xyz), N);
   2296 
   2297 		m4 R = m4_rotation_about_axis(rotation_axis, ui->beamform_plane);
   2298 		m4 T = m4_translation(v3_scale(m4_mul_v3(R, N), ui->off_axis_position));
   2299 
   2300 		new_transform = m4_mul(T, m4_mul(R, new_transform));
   2301 	}break;
   2302 	}
   2303 
   2304 	new_transform = m4_mul(new_transform, m4_inverse(das_transform));
   2305 
   2306 	BeamformerComputePlan *cp = beamformer_context->compute_context.compute_plans[parameter_block];
   2307 	if (cp) {
   2308 		result |= !m4_equal(new_transform, cp->ui_voxel_transform);
   2309 		memory_copy(cp->ui_voxel_transform.E, new_transform.E, sizeof(new_transform));
   2310 	}
   2311 
   2312 	if (result) {
   2313 		mark_parameter_block_region_dirty(beamformer_context->shared_memory, parameter_block,
   2314 		                                  BeamformerParameterBlockRegion_Parameters);
   2315 	}
   2316 
   2317 	return result;
   2318 }
   2319 
   2320 function void
   2321 ui_scroll_begin(Axis2 scroll_axis)
   2322 {
   2323 	UINode *outer, *inner, *clip, *child;
   2324 
   2325 	UIChildLayoutAxis(Axis2_Y)
   2326 	UIParent(ui_spacer(0))
   2327 	{
   2328 		UIChildLayoutAxis(Axis2_X)
   2329 		UIFontSize(30.f)
   2330 		outer = ui_node_from_string(UINodeFlag_Scroll, str8("###scroll_box"));
   2331 
   2332 		ui_padh(UI_NODE_PAD);
   2333 	}
   2334 
   2335 	UIParent(outer)
   2336 	{
   2337 		ui_padw(UI_NODE_PAD);
   2338 		UIChildLayoutAxis(Axis2_Y)
   2339 		inner = ui_node_from_string(0, str8("###scroll_inner"));
   2340 	}
   2341 
   2342 	UINodeFlags axis_flags;
   2343 	switch (scroll_axis) {
   2344 	InvalidDefaultCase;
   2345 	case Axis2_Count:{axis_flags = UINodeFlag_ViewScroll; }break;
   2346 	case Axis2_X:{    axis_flags = UINodeFlag_ViewScrollX;}break;
   2347 	case Axis2_Y:{    axis_flags = UINodeFlag_ViewScrollY;}break;
   2348 	}
   2349 	UIParent(inner)
   2350 	clip = ui_node_from_string(axis_flags|
   2351 	                           UINodeFlag_Clip|
   2352 	                           UINodeFlag_AllowOverflow|
   2353 	                           0, str8("###scroll_clip"));
   2354 
   2355 	UIParent(clip)
   2356 	{
   2357 		UIPrefWidth(ui_children_sum(1.f))
   2358 		UIPrefHeight(ui_children_sum(1.f))
   2359 		child = ui_node_from_string(0, str8("###scroll_child"));
   2360 	}
   2361 
   2362 	ui_push_parent(child);
   2363 }
   2364 
   2365 function void
   2366 ui_scroll_end(void)
   2367 {
   2368 	BeamformerUI *ui = ui_context;
   2369 	UINode *child = ui_pop_parent();
   2370 	UINode *clip  = child->parent;
   2371 	UINode *inner = clip->parent;
   2372 	UINode *outer = inner->parent;
   2373 
   2374 	v2 scroll_offset  = clip->view_scroll_offset;
   2375 
   2376 	str8 labels[2][2] = {
   2377 		[Axis2_X] = {str8_comp("<"), str8_comp(">")},
   2378 		[Axis2_Y] = {str8_comp("^"), str8_comp("v")},
   2379 	};
   2380 
   2381 	f32 btn_size = (f32)ui_font_for_node(outer).baseSize;
   2382 
   2383 	UINode *axis_parents[] = {[Axis2_X] = inner, [Axis2_Y] = outer};
   2384 	for EachElement(axis_parents, axis)
   2385 	if (clip->flags & (UINodeFlag_ViewScrollX << axis))
   2386 	UIParent(axis_parents[axis])
   2387 	{
   2388 		b32 build_scrollbar = 2.f * btn_size < clip->computed_size[axis] &&
   2389 		                      child->computed_size[axis] > clip->computed_size[axis];
   2390 
   2391 		// NOTE(rnp): vertical scroll bar shares padding on bottom with horizontal
   2392 		// scroll bar so padding was already pushed, if we aren't drawing the horizontal
   2393 		// scroll bar we need to avoid a double pad
   2394 		if (axis == Axis2_Y || build_scrollbar) {
   2395 			UIChildLayoutAxis(axis2_flip(axis))
   2396 			ui_pads(UI_NODE_PAD);
   2397 		}
   2398 
   2399 		if (build_scrollbar)
   2400 		UIAxisSize(axis2_flip(axis), ui_px(12.f, 1.f))
   2401 		UIChildLayoutAxis(axis)
   2402 		UIParent(axis_parents[axis])
   2403 		{
   2404 			UINode *parent = axis_parents[axis];
   2405 			f32 d_size     = child->computed_size[axis] - clip->computed_size[axis];
   2406 			f32 used_pct   = clip->computed_size[axis] / child->computed_size[axis];
   2407 			f32 rem_pct    = 1.f - used_pct;
   2408 			f32 before_pct = rem_pct - (d_size - scroll_offset.E[axis]) / child->computed_size[axis];
   2409 			f32 after_pct  = rem_pct - before_pct;
   2410 
   2411 			UINode *scroll_container;
   2412 			UIAxisAlign(axis2_flip(axis), Center)
   2413 			UIAxisSize(axis, ui_px(parent->computed_size[axis], 1.f))
   2414 			scroll_container = ui_spacer(0);
   2415 
   2416 			UIAxisSize(axis2_flip(axis), ui_pct(1.f, 0.5f))
   2417 			UIFontSize(outer->font_size)
   2418 			UIParent(scroll_container)
   2419 			{
   2420 				UISignal signal;
   2421 				// TODO(rnp): icons
   2422 				UIFlags(UINodeFlag_IconText)
   2423 				UIAxisSize(axis2_flip(axis), ui_text_dim(1.f, 1.f))
   2424 				UIAxisSize(axis, ui_text_dim(1.f, 1.f))
   2425 				signal = ui_label_button(labels[axis][0]);
   2426 				if (signal.flags & UISignalFlag_LeftPressed) {
   2427 					// TODO(rnp): handle repeat
   2428 					scroll_offset.E[axis] -= btn_size * 0.5f;
   2429 				}
   2430 
   2431 				ui_pads(3.f);
   2432 
   2433 				UISignalFlags bar_flags = 0;
   2434 
   2435 				UIBorderColour((v4){0})
   2436 				UIFlags(UINodeFlag_Clickable|UINodeFlag_DrawBorder|UINodeFlag_DrawHotEffects)
   2437 				UIAxisSize(axis, ui_pct(before_pct, 0.5f))
   2438 				bar_flags |= ui_signal_from_node(ui_node_from_string(0, str8("###before"))).flags;
   2439 
   2440 				UIBGColour(FG_COLOUR)
   2441 				UIAxisSize(axis, ui_pct(used_pct, 0.5f))
   2442 				UIFlags(UINodeFlag_Clickable|UINodeFlag_DrawBackground|UINodeFlag_DrawHotEffects)
   2443 				signal = ui_signal_from_node(ui_node_from_string(0, str8("###used")));
   2444 				bar_flags |= signal.flags;
   2445 
   2446 				UIBorderColour((v4){0})
   2447 				UIFlags(UINodeFlag_Clickable|UINodeFlag_DrawBorder|UINodeFlag_DrawHotEffects)
   2448 				UIAxisSize(axis, ui_pct(after_pct , 0.5f))
   2449 				bar_flags |= ui_signal_from_node(ui_node_from_string(0, str8("###after"))).flags;
   2450 
   2451 				if (bar_flags & (UISignalFlag_Dragging|UISignalFlag_Pressed)) {
   2452 					f32 off_pct = rect_uv(ui->last_mouse, ui_node_rect(clip)).E[axis] - 0.5f * used_pct;
   2453 					scroll_offset.E[axis] = Clamp01(off_pct) * child->computed_size[axis];
   2454 				}
   2455 
   2456 				ui_pads(3.f);
   2457 
   2458 				UIFlags(UINodeFlag_IconText)
   2459 				UIAxisSize(axis2_flip(axis), ui_text_dim(1.f, 1.f))
   2460 				UIAxisSize(axis, ui_text_dim(1.f, 1.f))
   2461 				signal = ui_label_button(labels[axis][1]);
   2462 				if (signal.flags & UISignalFlag_LeftPressed) {
   2463 					// TODO(rnp): handle repeat
   2464 					scroll_offset.E[axis] += btn_size * 0.5f;
   2465 				}
   2466 			}
   2467 
   2468 			// NOTE(rnp): vertical scroll bar needs padding next to it but must share
   2469 			// padding on the bottom with the horizontal scrollbar
   2470 			if (axis == Axis2_Y) ui_padw(UI_NODE_PAD);
   2471 		}
   2472 	}
   2473 
   2474 	// TODO(rnp): view scroll is being added to ViewScroll node in ui_signal_from_node maybe we are ignoring it?
   2475 	UISignal signal = ui_signal_from_node(outer);
   2476 	scroll_offset = v2_sub(scroll_offset, v2_scale(signal.scroll, btn_size * 0.5f));
   2477 
   2478 	scroll_offset.x = Max(0, Min(scroll_offset.x, child->computed_size[Axis2_X] - clip->computed_size[Axis2_X]));
   2479 	scroll_offset.y = Max(0, Min(scroll_offset.y, child->computed_size[Axis2_Y] - clip->computed_size[Axis2_Y]));
   2480 	clip->view_scroll_offset = scroll_offset;
   2481 }
   2482 
   2483 typedef struct {
   2484 	Axis2 axis;
   2485 	f32   start_value;
   2486 	f32   end_value;
   2487 	u32   segments;
   2488 } UIDrawScaleBarData;
   2489 
   2490 function UI_CUSTOM_DRAW_FUNCTION(ui_custom_draw_scale_bar)
   2491 {
   2492 	UIDrawScaleBarData *info = node->custom_draw_context;
   2493 
   2494 	b32 draw_plus = Sign(info->end_value) != Sign(info->start_value);
   2495 
   2496 	Font font        = ui_font_for_node(node);
   2497 	v2   start_point = node_rect.pos;
   2498 	v2   end_point   = node_rect.pos;
   2499 
   2500 	if (info->axis == Axis2_Y) start_point.y += node_rect.size.y;
   2501 	else                       end_point.x   += node_rect.size.x;
   2502 
   2503 	end_point = v2_sub(end_point, start_point);
   2504 
   2505 	rlPushMatrix();
   2506 	rlTranslatef(start_point.x, start_point.y, 0);
   2507 	rlRotatef(atan2_f32(end_point.y, end_point.x) * 180 / PI, 0, 0, 1);
   2508 
   2509 	Stream buf = arena_stream(*ui_build_arena());
   2510 	f32 inc       = v2_magnitude(end_point) / (f32)info->segments;
   2511 	f32 value_inc = (info->end_value - info->start_value) / (f32)info->segments;
   2512 	f32 value     = info->start_value;
   2513 
   2514 	v2 sp = {0}, ep = {.y = RULER_TICK_LENGTH};
   2515 	v2 tp = {{(f32)font.baseSize / 2.0f, ep.y + RULER_TEXT_PAD}};
   2516 
   2517 	TextSpec text_spec = {.font = &font, .rotation = 90.0f, .colour = node->text_colour, .flags = TF_ROTATED};
   2518 	if (node->flags & UINodeFlag_DrawHotEffects)
   2519 		text_spec.colour = v4_lerp(text_spec.colour, HOVERED_COLOUR, node->hot_t);
   2520 
   2521 	Color rl_txt_colour = colour_from_normalized(node->text_colour);
   2522 	for (u32 j = 0; j <= info->segments; j++) {
   2523 		DrawLineEx(rl_v2(sp), rl_v2(ep), 4.f, rl_txt_colour);
   2524 
   2525 		stream_reset(&buf, 0);
   2526 		if (draw_plus && value > 0) stream_append_byte(&buf, '+');
   2527 		stream_append_f64(&buf, value, Abs(value_inc) < 1 ? 100 : 10);
   2528 		stream_append_s8(&buf, s8("mm"));
   2529 		draw_text(stream_to_str8(&buf), tp, &text_spec);
   2530 
   2531 		value += value_inc;
   2532 		sp.x  += inc;
   2533 		ep.x  += inc;
   2534 		tp.x  += inc;
   2535 	}
   2536 
   2537 	rlPopMatrix();
   2538 }
   2539 
   2540 function UISignal
   2541 ui_build_scale_bar(Axis2 axis, v2 min, v2 max)
   2542 {
   2543 	Font font = ui_font_for_node(ui_top_parent());
   2544 	f32  label_size = measure_text(font, str8("-288.88mm")).w;
   2545 
   2546 	UISignal result;
   2547 	UIAxisSize(axis2_flip(axis), ui_px(RULER_TICK_LENGTH + RULER_TEXT_PAD + label_size, 1.f))
   2548 	UIFlags(UINodeFlag_Clickable|UINodeFlag_Scroll|UINodeFlag_DrawHotEffects|UINodeFlag_CustomDraw)
   2549 	{
   2550 		UINode *node = ui_node_from_string(0, str8("###scale_bar"));
   2551 		result = ui_signal_from_node(node);
   2552 
   2553 		UIDrawScaleBarData *info = push_struct(ui_build_arena(), UIDrawScaleBarData);
   2554 		node->custom_draw_function = ui_custom_draw_scale_bar;
   2555 		node->custom_draw_context  = info;
   2556 
   2557 		Rect tick_rect = ui_node_rect(node);
   2558 		if (tick_rect.size.E[axis] > 0) {
   2559 			info->axis        = axis;
   2560 			info->segments    = (u32)(tick_rect.size.E[axis] / (1.5f * font.baseSize));
   2561 			info->start_value = min.E[axis] * 1e3;
   2562 			info->end_value   = max.E[axis] * 1e3;
   2563 			if (axis == Axis2_Y) swap(info->start_value, info->end_value);
   2564 		}
   2565 	}
   2566 	return result;
   2567 }
   2568 
   2569 function void
   2570 ui_build_frame_view_overlay(UINode *frame_view, BeamformerFrameView *view, v2 min_2d, v2 max_2d)
   2571 {
   2572 	BeamformerUI *ui = ui_context;
   2573 	UIParent(frame_view)
   2574 	UIChildLayoutAxis(Axis2_X)
   2575 	UIPrefHeight(ui_children_sum(1.f))
   2576 	UIPrefWidth(ui_pct(1.f, 0.5f))
   2577 	UITextOutlineColour((v4){.a = 1.f})
   2578 	UITextOutlineThickness(1.f)
   2579 	UITextColour(RULER_COLOUR)
   2580 	{
   2581 		ui_padh(UI_NODE_PAD);
   2582 
   2583 		if (view->kind != BeamformerFrameViewKind_3DXPlane)
   2584 		UIFontSize(30.f)
   2585 		UIParent(ui_spacer(0))
   2586 		{
   2587 			ui_spacer(0);
   2588 
   2589 			UIPrefHeight(ui_text_dim(1.f, 1.f))
   2590 			UIPrefWidth(ui_text_dim(1.f, 1.f))
   2591 			ui_label(push_acquisition_kind(ui_build_arena(), view->frame.acquisition_kind,
   2592 			                               view->frame.compound_count, view->frame.contrast_mode));
   2593 
   2594 			ui_padw(2.f * UI_NODE_PAD);
   2595 		}
   2596 
   2597 		UIPrefHeight(ui_pct(1.f, 0.5f)) ui_spacer(0);
   2598 
   2599 		UIFontSize(24.f)
   2600 		UIAxisAlign(Axis2_Y, Right)
   2601 		UIParent(ui_spacer(0))
   2602 		{
   2603 			ui_padw(2.f * UI_NODE_PAD);
   2604 
   2605 			UINode *label_column, *value_column, *unit_column;
   2606 			UIAxisAlign(Axis2_X, Left)
   2607 			UIAxisAlign(Axis2_Y, Left)
   2608 			UIPrefWidth(ui_children_sum(1.f))
   2609 			UIParent(ui_spacer(0))
   2610 			UIChildLayoutAxis(Axis2_Y)
   2611 			{
   2612 				label_column = ui_node_from_string(0, str8("###labels"));
   2613 				ui_padw(UI_NODE_PAD);
   2614 				value_column = ui_node_from_string(0, str8("###values"));
   2615 				ui_padw(UI_NODE_PAD);
   2616 				unit_column  = ui_node_from_string(0, str8("###units"));
   2617 			}
   2618 
   2619 			UIPrefWidth(ui_text_dim(1.f, 1.f))
   2620 			UIPrefHeight(ui_text_dim(1.f, 1.f))
   2621 			{
   2622 				if (view->log_scale) {
   2623 					UIParent(label_column) ui_label(str8("Dynamic Range:"));
   2624 					UIParent(unit_column)  ui_label(str8("[dB]"));
   2625 					UIParent(value_column)
   2626 					UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric)
   2627 					{
   2628 						UISignal signal = ui_text_boxf("%0.2f###dynamic_range", view->dynamic_range);
   2629 						view->dirty |= ui_tweak_f32_compute_variable(signal, &view->dynamic_range, 1.f, 0.5f, V2_INFINITY);
   2630 					}
   2631 				}
   2632 
   2633 				// TODO(rnp): ui_em after text height matches correctly
   2634 				f32 spacer_height;
   2635 				UIParent(label_column) spacer_height = ui_label(str8("Gamma:")).node->computed_size[Axis2_Y];
   2636 				UIParent(unit_column)  ui_padh(spacer_height);
   2637 				UIParent(value_column)
   2638 				UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric)
   2639 				{
   2640 					UISignal signal = ui_text_boxf("%0.2f###gamma", view->gamma);
   2641 					view->dirty |= ui_tweak_f32_compute_variable(signal, &view->gamma, 1.f, 0.025f, V2_INFINITY);
   2642 				}
   2643 
   2644 				UIParent(label_column) spacer_height = ui_label(str8("Threshold:")).node->computed_size[Axis2_Y];
   2645 				UIParent(unit_column)  ui_padh(spacer_height);
   2646 				UIParent(value_column)
   2647 				UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric)
   2648 				{
   2649 					UISignal signal = ui_text_boxf("%0.2f###threshold", view->threshold);
   2650 					view->dirty |= ui_tweak_f32_compute_variable(signal, &view->threshold, 1.f, 1.f, V2_INFINITY);
   2651 				}
   2652 			}
   2653 
   2654 			UIPrefWidth(ui_pct(1.f, 0.5f)) ui_spacer(0);
   2655 
   2656 			Rect nr = ui_node_rect(frame_view);
   2657 			if (view->kind != BeamformerFrameViewKind_3DXPlane && point_in_rect(ui->last_mouse, nr) && ui->drag_panel == 0) {
   2658 				b32 is_1d = iv3_dimension(view->frame.points) == 1;
   2659 				v2 world = screen_point_to_world_2d(ui->last_mouse, nr.pos, v2_add(nr.pos, nr.size),
   2660 				                                    min_2d, max_2d);
   2661 				world = v2_scale(world, 1e3f);
   2662 				if (is_1d) world.y = ((nr.pos.y + nr.size.y) - ui->last_mouse.y) / nr.size.y;
   2663 
   2664 				UIPrefWidth(ui_text_dim(1.f, 1.f))
   2665 				UIPrefHeight(ui_text_dim(1.f, 1.f))
   2666 				ui_labelf("{%0.2f%s, %0.2f}", world.x, is_1d ? " mm" : "", world.y);
   2667 			}
   2668 
   2669 			ui_padw(2.f * UI_NODE_PAD);
   2670 		}
   2671 
   2672 		ui_padh(UI_NODE_PAD);
   2673 	}
   2674 }
   2675 
   2676 function void
   2677 ui_build_3d_xplane_context_menu(BeamformerFrameView *view)
   2678 {
   2679 	UINode *label_column, *button_column;
   2680 	UIParent(ui_context->context_menu_root)
   2681 	UIChildLayoutAxis(Axis2_X)
   2682 	UIPrefHeight(ui_children_sum(1.f))
   2683 	UIPrefWidth(ui_children_sum(1.f))
   2684 	UIParent(ui_spacer(0))
   2685 	UIChildLayoutAxis(Axis2_Y)
   2686 	{
   2687 		ui_padw(UI_NODE_PAD);
   2688 		UIAxisAlign(Axis2_X, Left)   label_column  = ui_node_from_string(0, str8("###labels"));
   2689 		ui_padw(UI_NODE_PAD * 2.f);
   2690 		UIAxisAlign(Axis2_X, Center)
   2691 			button_column = ui_node_from_string(0, str8("###buttons"));
   2692 		ui_padw(UI_NODE_PAD);
   2693 	}
   2694 
   2695 	UIPrefHeight(ui_text_dim(1.1f, 1.f))
   2696 	UIPrefWidth(ui_text_dim(1.f, 1.f))
   2697 	{
   2698 		{
   2699 			f32 row_height;
   2700 			UIParent(label_column)
   2701 				row_height = ui_label(str8("Log Scale")).node->computed_size[Axis2_Y];
   2702 
   2703 			UIParent(button_column)
   2704 			// TODO(rnp): ui_em(1.f, 1.f) once font size matches directly
   2705 			UIPrefHeight(ui_px(row_height, 1.f))
   2706 			UIPrefWidth(ui_px(row_height, 1.f))
   2707 			{
   2708 				UISignal signal = ui_toggle_button(view->log_scale, str8("###log_scale"));
   2709 				if ui_pressed(signal) {
   2710 					view->log_scale = !view->log_scale;
   2711 					view->dirty     = 1;
   2712 				}
   2713 			}
   2714 		}
   2715 
   2716 		{
   2717 			f32 row_height;
   2718 			UIParent(label_column)
   2719 				row_height = ui_label(str8("Demo Mode")).node->computed_size[Axis2_Y];
   2720 
   2721 			UIParent(button_column)
   2722 			// TODO(rnp): ui_em(1.f, 1.f) once font size matches directly
   2723 			UIPrefHeight(ui_px(row_height, 1.f))
   2724 			UIPrefWidth(ui_px(row_height, 1.f))
   2725 			{
   2726 				UISignal signal = ui_toggle_button(view->demo, str8("###demo_mode"));
   2727 				if ui_pressed(signal)
   2728 					view->demo = !view->demo;
   2729 			}
   2730 		}
   2731 
   2732 		UIParent(label_column)
   2733 		{
   2734 			f32 row_height = ui_label(str8("Planes:")).node->computed_size[Axis2_Y];
   2735 			// TODO(rnp): ui_em(1.f, 1.f) once font size matches directly
   2736 			UIParent(button_column) ui_padh(row_height);
   2737 		}
   2738 		for EachElement(view->plane_active, plane) {
   2739 			f32 row_height;
   2740 			UIParent(label_column)
   2741 			{
   2742 				str8 label = push_str8_from_parts(ui_build_arena(), str8(""), str8("    "),
   2743 				                                  beamformer_view_plane_tag_strings[plane]);
   2744 				row_height = ui_label(label).node->computed_size[Axis2_Y];
   2745 			}
   2746 
   2747 			UIParent(button_column)
   2748 			UIPrefHeight(ui_px(row_height, 1.f))
   2749 			UIPrefWidth(ui_px(row_height, 1.f))
   2750 			{
   2751 				UISignal signal = ui_toggle_button(view->plane_active[plane],
   2752 				                                   beamformer_view_plane_tag_strings[plane]);
   2753 				if ui_pressed(signal)
   2754 					view->plane_active[plane] = !view->plane_active[plane];
   2755 			}
   2756 		}
   2757 	}
   2758 }
   2759 
   2760 function void
   2761 ui_build_3d_xplane_frame_view(UINode *container, BeamformerFrameView *view)
   2762 {
   2763 	assert(view->kind == BeamformerFrameViewKind_3DXPlane);
   2764 	Rect display_rect = ui_node_rect(container);
   2765 	Rect vr = rect_shrink_centered(display_rect, (v2){{UI_NODE_PAD, UI_NODE_PAD}});
   2766 
   2767 	f32 aspect = (f32)view->colour_image.width / (f32)view->colour_image.height;
   2768 	if (aspect > 1.0f) vr.size.w = vr.size.h;
   2769 	else               vr.size.h = vr.size.w;
   2770 
   2771 	if (vr.size.w > display_rect.size.w) {
   2772 		vr.size.w -= (vr.size.w - display_rect.size.w);
   2773 		vr.size.h  = vr.size.w / aspect;
   2774 	} else if (vr.size.h > display_rect.size.h) {
   2775 		vr.size.h -= (vr.size.h - display_rect.size.h);
   2776 		vr.size.w  = vr.size.h * aspect;
   2777 	}
   2778 
   2779 	// TODO(rnp): probably we don't need frame_top in this path
   2780 	UINode *frame_top, *frame_view;
   2781 	UIParent(container)
   2782 	{
   2783 		ui_padh(UI_NODE_PAD);
   2784 
   2785 		UIChildLayoutAxis(Axis2_X)
   2786 		UIPrefHeight(ui_children_sum(1.f))
   2787 		UIPrefWidth(ui_children_sum(1.f))
   2788 		frame_top = ui_node_from_string(0, str8("###frame_view_top"));
   2789 
   2790 		UIParent(frame_top)
   2791 		UIPrefHeight(ui_px(vr.size.h, 1.f))
   2792 		{
   2793 			UIChildLayoutAxis(Axis2_Y)
   2794 			UIPrefWidth(ui_px(vr.size.w, 1.f))
   2795 			frame_view = ui_node_from_string(UINodeFlag_Clickable|
   2796 			                                 UINodeFlag_CustomDraw|
   2797 			                                 UINodeFlag_Clip|
   2798 			                                 UINodeFlag_Scroll|
   2799 			                                 0, str8("###frame_view"));
   2800 			frame_view->custom_draw_function = beamformer_custom_draw_frame_view;
   2801 			frame_view->custom_draw_context  = push_struct(ui_build_arena(), BeamformerCustomDrawFrameViewData);
   2802 			{
   2803 				BeamformerCustomDrawFrameViewData *data = frame_view->custom_draw_context;
   2804 				data->uv_start = (v2){0};
   2805 				data->uv_end   = (v2){{1.f, 1.f}};
   2806 				data->view     = view;
   2807 			}
   2808 
   2809 			ui_build_frame_view_overlay(frame_view, view, (v2){0}, (v2){0});
   2810 		}
   2811 	}
   2812 
   2813 	UISignal signal = ui_signal_from_node(frame_view);
   2814 	if (ui_tweak_f32_compute_variable(signal, &view->threshold, 1.f, 1.f, V2_INFINITY))
   2815 		view->dirty = 1;
   2816 
   2817 	f32 test[countof(view->plane_active)]       = {0};
   2818 	ray mouse_rays[countof(view->plane_active)] = {0};
   2819 	v2  mouse_uv = rect_uv_ndc(ui_context->last_mouse, vr);
   2820 
   2821 	i32 hovered_plane = -1;
   2822 	if ui_node_hot(frame_view) {
   2823 		for EachElement(test, it) if (view->plane_active[it]) {
   2824 			BeamformerFrame *frame = ui_context->latest_plane + it;
   2825 			v2 min_2d, max_2d;
   2826 			plane_corners_from_transform(frame->voxel_transform, &min_2d, &max_2d);
   2827 			v3  x_size     = v3_scale(x_plane_display_size(frame), 0.5f);
   2828 			m4  x_rotation = m4_rotation_about_y(x_plane_rotation_for_view_plane(view, it));
   2829 			v3  x_position = x_plane_offset_position(view, frame, it);
   2830 			mouse_rays[it] = x_plane_raycast(view, frame, mouse_uv);
   2831 			test[it]       = obb_raycast(x_rotation, x_size, x_position, mouse_rays[it]);
   2832 		}
   2833 
   2834 		f32 min_valid_t = inf32();
   2835 		for EachElement(test, it) {
   2836 			if (view->plane_active[it] && Between(test[it], 0, min_valid_t)) {
   2837 				hovered_plane = (i32)it;
   2838 				min_valid_t = test[it];
   2839 			}
   2840 		}
   2841 	}
   2842 
   2843 	if ui_pressed(signal) {
   2844 		view->plane_drag_index = hovered_plane;
   2845 		if (hovered_plane != -1) {
   2846 			v3 origin = mouse_rays[hovered_plane].origin;
   2847 			v3 p      = v3_scale(mouse_rays[hovered_plane].direction, test[hovered_plane]);
   2848 			view->hit_start_point = view->hit_test_point = v3_add(origin, p);
   2849 		}
   2850 	}
   2851 
   2852 	b32 active = ui_node_key_equal(ui_context->active_node_key[UIMouseButtonKind_Left], frame_view->key);
   2853 	for EachElement(view->hot_t, it) {
   2854 		b32 hot = active ? (view->plane_drag_index == (i32)it) : (hovered_plane == (i32)it);
   2855 		if (hot) view->hot_t[it] += HOVER_SPEED * dt_for_frame;
   2856 		else     view->hot_t[it] -= HOVER_SPEED * dt_for_frame;
   2857 		view->hot_t[it] = Clamp01(view->hot_t[it]);
   2858 	}
   2859 
   2860 	if ui_dragging(signal) {
   2861 		ui_disable_cursor();
   2862 		// TODO(rnp): hide mouse
   2863 		if (view->plane_drag_index != -1) {
   2864 			/* NOTE(rnp): project start point onto ray */
   2865 			BeamformerFrame *frame = ui_context->latest_plane + view->plane_drag_index;
   2866 			ray mouse_ray = x_plane_raycast(view, frame, rect_uv_ndc(clamp_v2_rect(ui_context->last_mouse, vr), vr));
   2867 			v3  s         = v3_sub(view->hit_start_point, mouse_ray.origin);
   2868 			v3  r         = v3_sub(mouse_ray.direction, mouse_ray.origin);
   2869 			f32 scale     = v3_dot(s, r) / v3_magnitude_squared(r);
   2870 			view->hit_test_point = v3_add(mouse_ray.origin, v3_scale(r, scale));
   2871 		} else {
   2872 			f32 dMouseX = ui_context->current_mouse.x - ui_context->last_mouse.x;
   2873 			view->rotation -= dMouseX / (f32)beamformer_context->window_size.w;
   2874 			if (view->rotation > 1.0f) view->rotation -= 1.0f;
   2875 			if (view->rotation < 0.0f) view->rotation += 1.0f;
   2876 		}
   2877 	}
   2878 
   2879 	if ui_released(signal) {
   2880 		ui_enable_cursor();
   2881 
   2882 		if (view->plane_drag_index != -1) {
   2883 			m4 x_rotation = m4_rotation_about_y(x_plane_rotation_for_view_plane(view, view->plane_drag_index));
   2884 			v3 Z = x_rotation.c[2].xyz;
   2885 			f32 delta = v3_dot(Z, v3_sub(view->hit_test_point, view->hit_start_point));
   2886 
   2887 			BeamformerSharedMemory          *sm = beamformer_context->shared_memory;
   2888 			BeamformerLiveImagingParameters *li = &sm->live_imaging_parameters;
   2889 			li->image_plane_offsets[view->plane_drag_index] += delta;
   2890 			atomic_or_u32(&sm->live_imaging_dirty_flags, BeamformerLiveImagingDirtyFlags_ImagePlaneOffsets);
   2891 		}
   2892 
   2893 		view->plane_drag_index = -1;
   2894 		view->hit_start_point = view->hit_test_point = (v3){0};
   2895 	}
   2896 }
   2897 
   2898 function void
   2899 ui_build_frame_view_context_menu(BeamformerUIPanel *panel, BeamformerFrameView *view)
   2900 {
   2901 	UINode *label_column, *button_column;
   2902 	UIParent(ui_context->context_menu_root)
   2903 	UIChildLayoutAxis(Axis2_X)
   2904 	UIPrefHeight(ui_children_sum(1.f))
   2905 	UIPrefWidth(ui_children_sum(1.f))
   2906 	UIParent(ui_spacer(0))
   2907 	UIChildLayoutAxis(Axis2_Y)
   2908 	{
   2909 		ui_padw(UI_NODE_PAD);
   2910 		UIAxisAlign(Axis2_X, Left)   label_column  = ui_node_from_string(0, str8("###labels"));
   2911 		ui_padw(UI_NODE_PAD * 2.f);
   2912 		UIAxisAlign(Axis2_X, Center)
   2913 			button_column = ui_node_from_string(0, str8("###buttons"));
   2914 		ui_padw(UI_NODE_PAD);
   2915 	}
   2916 
   2917 	UIPrefHeight(ui_text_dim(1.1f, 1.f))
   2918 	UIPrefWidth(ui_text_dim(1.f, 1.f))
   2919 	{
   2920 		read_only local_persist str8 dimension_strings[2][2] = {
   2921 			{str8_comp("Extent Scale Bar"),  str8_comp("Magnitude Scale Bar")},
   2922 			{str8_comp("Lateral Scale Bar"), str8_comp("Axial Scale Bar")    },
   2923 		};
   2924 
   2925 		UIParent(label_column)  ui_label(str8("Plane Tag"));
   2926 		UIParent(button_column)
   2927 		UIFlags(UINodeFlag_Scroll)
   2928 		{
   2929 			str8 tag = str8("Any");
   2930 			if (view->view_plane != BeamformerViewPlaneTag_Count)
   2931 				tag = beamformer_view_plane_tag_strings[view->view_plane];
   2932 			UISignal signal = ui_label_button(push_str8_from_parts(ui_build_arena(), str8(""),
   2933 			                                                       tag, str8("###PlaneTagButton")));
   2934 			i32 delta = signal.scroll.y + ui_pressed(signal);
   2935 			view->view_plane = circular_add(view->view_plane, delta, BeamformerViewPlaneTag_Count + 1);
   2936 			if (ui_pressed(signal) || ui_scrolled(signal))
   2937 				view->dirty = 1;
   2938 		}
   2939 
   2940 		i32 dimension = iv3_dimension(view->frame.points);
   2941 		dimension = Min(dimension, 2);
   2942 		if (dimension > 0) {
   2943 			for EachEnumValue(Axis2, axis) {
   2944 				f32 row_height;
   2945 				UIParent(label_column)
   2946 					row_height = ui_label(dimension_strings[dimension - 1][axis]).node->computed_size[Axis2_Y];
   2947 
   2948 				UIParent(button_column)
   2949 				// TODO(rnp): ui_em(1.f, 1.f) once font size matches directly
   2950 				UIPrefHeight(ui_px(row_height, 1.f))
   2951 				UIPrefWidth(ui_px(row_height, 1.f))
   2952 				{
   2953 					UISignal signal = ui_toggle_buttonf(view->scale_bar_active[axis], "###axis_%u", axis);
   2954 					if ui_pressed(signal)
   2955 						view->scale_bar_active[axis] = !view->scale_bar_active[axis];
   2956 				}
   2957 			}
   2958 		}
   2959 
   2960 		{
   2961 			f32 row_height;
   2962 			UIParent(label_column)
   2963 				row_height = ui_label(str8("Log Scale")).node->computed_size[Axis2_Y];
   2964 
   2965 			UIParent(button_column)
   2966 			// TODO(rnp): ui_em(1.f, 1.f) once font size matches directly
   2967 			UIPrefHeight(ui_px(row_height, 1.f))
   2968 			UIPrefWidth(ui_px(row_height, 1.f))
   2969 			{
   2970 				UISignal signal = ui_toggle_button(view->log_scale, str8("###log_scale"));
   2971 				if ui_pressed(signal) {
   2972 					view->log_scale = !view->log_scale;
   2973 					view->dirty     = 1;
   2974 				}
   2975 			}
   2976 		}
   2977 
   2978 		if (dimension > 0 && panel->kind != BeamformerPanelKind_FrameViewCopy) {
   2979 			f32 row_height;
   2980 			UIParent(label_column)
   2981 			{
   2982 				UISignal signal = ui_label_button(str8("Copy Frame"));
   2983 				row_height = signal.node->computed_size[Axis2_Y];
   2984 				if ui_pressed(signal) {
   2985 					ui_context_menu_close();
   2986 					beamformer_command(beamformer_command_infos[BeamformerCommandKind_OpenTab].string,
   2987 					                   .tree_node  = (u64)panel->parent,
   2988 					                   .frame_view = (u64)view,
   2989 					                   .string     = beamformer_panel_infos[BeamformerPanelKind_FrameViewCopy].string);
   2990 				}
   2991 			}
   2992 
   2993 			// TODO(rnp): ui_em(1.f, 1.f) once font size matches directly
   2994 			UIParent(button_column) ui_padh(row_height);
   2995 		}
   2996 
   2997 		// TODO(rnp): extra frame view copy settings
   2998 		if (panel->kind == BeamformerPanelKind_FrameViewCopy) {
   2999 		}
   3000 	}
   3001 }
   3002 
   3003 function void
   3004 ui_build_frame_view(UINode *container, BeamformerFrameView *view)
   3005 {
   3006 	assert(view->kind != BeamformerFrameViewKind_3DXPlane);
   3007 
   3008 	BeamformerUI    *ui    = ui_context;
   3009 	BeamformerFrame *frame = &view->frame;
   3010 	b32 is_1d = iv3_dimension(frame->points) == 1;
   3011 	f32 txt_w = measure_text(ui->small_font, str8(" -288.8 mm")).w;
   3012 	f32 scale_bar_size = 1.2f * txt_w + RULER_TICK_LENGTH;
   3013 
   3014 	v3 U = frame->voxel_transform.c[0].xyz;
   3015 	v3 V = frame->voxel_transform.c[1].xyz;
   3016 
   3017 	v2 output_dim;
   3018 	output_dim.x = v3_magnitude(U);
   3019 	output_dim.y = v3_magnitude(V);
   3020 
   3021 	U = v3_scale(U, 1.f / output_dim.x);
   3022 	V = v3_scale(V, 1.f / output_dim.y);
   3023 
   3024 	v3 min_coordinate = m4_mul_v3(frame->voxel_transform, (v3){{0.f, 0.f, 0.f}});
   3025 	v3 max_coordinate = m4_mul_v3(frame->voxel_transform, (v3){{1.f, 1.f, 1.f}});
   3026 
   3027 	v2 min_2d = {{v3_dot(U, min_coordinate), v3_dot(V, min_coordinate)}};
   3028 	v2 max_2d = {{v3_dot(U, max_coordinate), v3_dot(V, max_coordinate)}};
   3029 
   3030 	f32 aspect = is_1d ? 1.0f : output_dim.w / output_dim.h;
   3031 
   3032 	Rect display_rect = ui_node_rect(container);
   3033 	Rect vr = rect_shrink_centered(display_rect, (v2){{UI_NODE_PAD, UI_NODE_PAD}});
   3034 
   3035 	v2 scale_bar_area = {0};
   3036 	if (view->scale_bar_active[Axis2_Y]) {
   3037 		vr.pos.y         += 0.5f * (f32)ui->small_font.baseSize;
   3038 		scale_bar_area.x += scale_bar_size;
   3039 		scale_bar_area.y += (f32)ui->small_font.baseSize;
   3040 	}
   3041 	if (view->scale_bar_active[Axis2_X]) {
   3042 		vr.pos.x         += 0.5f * (f32)ui->small_font.baseSize;
   3043 		scale_bar_area.x += (f32)ui->small_font.baseSize;
   3044 		scale_bar_area.y += scale_bar_size;
   3045 	}
   3046 
   3047 	vr.size = v2_sub(vr.size, scale_bar_area);
   3048 	if (aspect > 1) vr.size.h = vr.size.w / aspect;
   3049 	else            vr.size.w = vr.size.h * aspect;
   3050 
   3051 	v2 occupied = v2_add(vr.size, scale_bar_area);
   3052 	if (occupied.w > display_rect.size.w) {
   3053 		vr.size.w -= (occupied.w - display_rect.size.w);
   3054 		vr.size.h  = vr.size.w / aspect;
   3055 	} else if (occupied.h > display_rect.size.h) {
   3056 		vr.size.h -= (occupied.h - display_rect.size.h);
   3057 		vr.size.w  = vr.size.h * aspect;
   3058 	}
   3059 
   3060 	b32 rebuild_transform = 0;
   3061 
   3062 	UIParent(container)
   3063 	{
   3064 		ui_padh(UI_NODE_PAD);
   3065 
   3066 		UINode *frame_top, *frame_view;
   3067 		UIChildLayoutAxis(Axis2_X)
   3068 		UIPrefHeight(ui_children_sum(1.f))
   3069 		UIPrefWidth(ui_children_sum(1.f))
   3070 		frame_top = ui_node_from_string(0, str8("###frame_view_top"));
   3071 
   3072 		UIParent(frame_top)
   3073 		UIPrefHeight(ui_px(vr.size.h, 1.f))
   3074 		{
   3075 			UIChildLayoutAxis(Axis2_Y)
   3076 			UIPrefWidth(ui_px(vr.size.w, 1.f))
   3077 			frame_view = ui_node_from_string(UINodeFlag_Clickable|
   3078 			                                 UINodeFlag_CustomDraw|
   3079 			                                 UINodeFlag_Clip|
   3080 			                                 UINodeFlag_Scroll|
   3081 			                                 0, str8("###frame_view"));
   3082 			frame_view->custom_draw_function = beamformer_custom_draw_frame_view;
   3083 			frame_view->custom_draw_context  = push_struct(ui_build_arena(), BeamformerCustomDrawFrameViewData);
   3084 			{
   3085 				BeamformerCustomDrawFrameViewData *data = frame_view->custom_draw_context;
   3086 				data->uv_start = (v2){0};
   3087 				data->uv_end   = (v2){{1.f, 1.f}};
   3088 				data->view     = view;
   3089 			}
   3090 
   3091 			ui_build_frame_view_overlay(frame_view, view, min_2d, max_2d);
   3092 
   3093 			UISignal signal = ui_signal_from_node(frame_view);
   3094 			// TODO(rnp): is this correct for x-plane?
   3095 			if (ui_tweak_f32_compute_variable(signal, &view->threshold, 1.f, 1.f, V2_INFINITY))
   3096 				view->dirty = 1;
   3097 
   3098 			if ui_pressed(signal) {
   3099 				view->ruler.state = circular_add(view->ruler.state, 1, RulerState_Count);
   3100 				// TODO(rnp): cleanup: this
   3101 				v3 p = world_point_from_plane_uv(frame->voxel_transform, rect_uv(ui->last_mouse, ui_node_rect(frame_view)));
   3102 				switch (view->ruler.state) {
   3103 				InvalidDefaultCase;
   3104 				case RulerState_None:{}break;
   3105 				case RulerState_Start:{view->ruler.start = p;}break;
   3106 				case RulerState_Hold:{ view->ruler.end   = p;}break;
   3107 				}
   3108 			}
   3109 
   3110 			if (view->scale_bar_active[Axis2_Y]) {
   3111 				signal = ui_build_scale_bar(Axis2_Y, min_2d, max_2d);
   3112 				if ui_scrolled(signal) {
   3113 					max_2d.y += signal.scroll.y * 1e-3f;
   3114 					rebuild_transform = 1;
   3115 				}
   3116 			}
   3117 		}
   3118 
   3119 		if (view->scale_bar_active[Axis2_X])
   3120 		UIChildLayoutAxis(Axis2_X)
   3121 		UIPrefHeight(ui_children_sum(1.0f))
   3122 		UIPrefWidth(ui_children_sum(1.0f))
   3123 		UIParent(ui_node_from_string(0, str8("###frame_view_bot")))
   3124 		UIPrefWidth(ui_px(vr.size.w, 1.f))
   3125 		{
   3126 			f32 top_position_offset = frame_view->computed_position[Axis2_X] - display_rect.pos.x;
   3127 			ui_padw(top_position_offset);
   3128 
   3129 			UISignal signal = ui_build_scale_bar(Axis2_X, min_2d, max_2d);
   3130 			if ui_scrolled(signal) {
   3131 				min_2d.x += signal.scroll.y * 0.5e-3f;
   3132 				max_2d.x -= signal.scroll.y * 0.5e-3f;
   3133 				rebuild_transform = 1;
   3134 			}
   3135 
   3136 			ui_padw(display_rect.size.x - top_position_offset);
   3137 		}
   3138 	}
   3139 
   3140 	if (rebuild_transform) {
   3141 		min_coordinate.E[0] = min_2d.E[0]; min_coordinate.E[1] = min_2d.E[1];
   3142 		max_coordinate.E[0] = max_2d.E[0]; max_coordinate.E[1] = max_2d.E[1];
   3143 		if (ui_rebuild_das_transform(frame->parameter_block, iv3_dimension(frame->points), min_coordinate, max_coordinate))
   3144 			ui->flush_parameters = 1;
   3145 	}
   3146 }
   3147 
   3148 function UI_CUSTOM_DRAW_FUNCTION(beamformer_ui_custom_draw_compute_bar_graph)
   3149 {
   3150 	ComputeShaderStats *stats = beamformer_context->compute_shader_stats;
   3151 
   3152 	UINode *labels = node->previous_sibling->previous_sibling;
   3153 
   3154 	u32  label_count = labels->child_count;
   3155 	f32 *total_times = push_array(ui_build_arena(), f32, label_count);
   3156 	f32  compute_time_sum = 0;
   3157 
   3158 	u32 stages = stats->table.shader_count;
   3159 	for (u32 index = 0; index < stages; index++)
   3160 		compute_time_sum += stats->average_times[index];
   3161 	for EachIndex(label_count, frame) {
   3162 		u32 frame_index = (stats->latest_frame_index - frame - 1) % countof(stats->table.times);
   3163 		for EachIndex(stages, stage)
   3164 			total_times[frame] += stats->table.times[frame_index][stage];
   3165 	}
   3166 
   3167 	f32 remaining_width = node_rect.size.w;
   3168 	f32 average_width   = 0.8f * remaining_width;
   3169 
   3170 	s8 mouse_text = s8("");
   3171 	v2 text_pos;
   3172 
   3173 	u32 row_index = 0;
   3174 	for (UINode *ln = labels->first_child; !ui_node_is_nil(ln); ln = ln->next_sibling, row_index++) {
   3175 		u32 frame_index = (stats->latest_frame_index - row_index - 1) % countof(stats->table.times);
   3176 		f32 total_width = average_width * total_times[row_index] / compute_time_sum;
   3177 		Rect rect;
   3178 		rect.pos  = (v2){{node_rect.pos.x, ln->computed_position[Axis2_Y]}};
   3179 		rect.size = (v2){.y = ln->computed_size[Axis2_Y]};
   3180 		rect = rect_squish_centered(rect, (v2){.y = 0.4f});
   3181 
   3182 		for (u32 i = 0; i < stages; i++) {
   3183 			rect.size.w = total_width * stats->table.times[frame_index][i] / total_times[row_index];
   3184 			Color color = colour_from_normalized(g_colour_palette[i % countof(g_colour_palette)]);
   3185 			DrawRectangleRec(rl_rect(rect), color);
   3186 			if (point_in_rect(ui_context->last_mouse, rect)) {
   3187 				// TODO(rnp): tooltips
   3188 				text_pos  = v2_add(rect.pos, (v2){{UI_NODE_PAD, 3.f}});
   3189 				Stream sb = arena_stream(*ui_build_arena());
   3190 				stream_append_s8s(&sb, beamformer_shader_names[stats->table.shader_ids[i]], s8(": "));
   3191 				stream_append_f64_e(&sb, stats->table.times[frame_index][i]);
   3192 				mouse_text = arena_stream_commit(ui_build_arena(), &sb);
   3193 			}
   3194 			rect.pos.x += rect.size.w;
   3195 		}
   3196 	}
   3197 
   3198 	v2 start = v2_add(node_rect.pos, (v2){.x = average_width, .y = 0.01f * node_rect.size.y});
   3199 	v2 end   = v2_add(start, (v2){.y = node_rect.size.y - 0.02f * node_rect.size.y});
   3200 	DrawLineEx(rl_v2(start), rl_v2(end), 4, colour_from_normalized(FG_COLOUR));
   3201 
   3202 	if (mouse_text.len) {
   3203 		TextSpec ts = {.font = &ui_context->small_font, .flags = TF_OUTLINED, .colour = FG_COLOUR,
   3204 		               .outline_colour = {.a = 1.f}, .outline_thick = 1.f};
   3205 		draw_text(str8_from_s8(mouse_text), text_pos, &ts);
   3206 	}
   3207 }
   3208 
   3209 function void
   3210 ui_build_compute_stats(BeamformerComputePlan *cp, f32 broken_shader_t)
   3211 {
   3212 	ComputeShaderStats *stats = beamformer_context->compute_shader_stats;
   3213 	f32 compute_time_sum = 0;
   3214 	u32 stages           = stats->table.shader_count;
   3215 
   3216 	for (u32 index = 0; index < stages; index++)
   3217 		compute_time_sum += stats->average_times[index];
   3218 
   3219 	UIFontSize(30.f)
   3220 	UIScroll(Axis2_Count)
   3221 	{
   3222 		ui_top_parent()->child_layout_axis = Axis2_X;
   3223 
   3224 		UINode *label_column, *value_column, *unit_column;
   3225 		UIAxisAlign(Axis2_X, Left)
   3226 		UIChildLayoutAxis(Axis2_Y)
   3227 		UIPrefWidth(ui_children_sum(1.0f))
   3228 		UIPrefHeight(ui_children_sum(1.0f))
   3229 		{
   3230 			label_column = ui_node_from_string(0, str8("###labels"));
   3231 			ui_padw(UI_NODE_PAD);
   3232 			value_column = ui_node_from_string(0, str8("###values"));
   3233 			ui_padw(UI_NODE_PAD);
   3234 			unit_column  = ui_node_from_string(0, str8("###units"));
   3235 		}
   3236 
   3237 		UIPrefWidth(ui_text_dim(1.0f, 1.0f))
   3238 		UIPrefHeight(ui_text_dim(1.05f, 1.0f))
   3239 		{
   3240 			for EachIndex(stages, it) {
   3241 				v4 label_colour = FG_COLOUR;
   3242 				if (vk_pipeline_valid(cp->vulkan_pipelines[it]) == 0 &&
   3243 				    stats->table.shader_ids[it] != BeamformerShaderKind_Hilbert)
   3244 				{
   3245 					label_colour = v4_lerp(FG_COLOUR, FOCUSED_COLOUR, ease_in_out_quartic(broken_shader_t));
   3246 				}
   3247 
   3248 				str8 shader = str8_from_s8(beamformer_shader_names[stats->table.shader_ids[it]]);
   3249 
   3250 				UITextColour(label_colour)
   3251 				UIParent(label_column) ui_labelf("%.*s:###csl%u", (i32)shader.length, shader.data, (u32)it);
   3252 				UIParent(value_column) ui_labelf("%0.2e###csv%u", stats->average_times[it], (u32)it);
   3253 				UIParent(unit_column)  ui_labelf("[s]###csu%u", (u32)it);
   3254 			}
   3255 
   3256 			UIParent(label_column) ui_label(str8("Compute Total:"));
   3257 			UIParent(value_column) ui_labelf("%0.2e (%0.2f)###csv_total", compute_time_sum,
   3258 			                                 compute_time_sum > 0.f ? 1.0f / compute_time_sum : 0.f);
   3259 			UIParent(unit_column)  ui_label(str8("[s] (FPS)###csv_total"));
   3260 
   3261 			UIParent(label_column) ui_label(str8("RF Upload Delta:"));
   3262 			UIParent(value_column) ui_labelf("%0.2e (%0.2f)###csv_upload", stats->rf_time_delta_average,
   3263 			                                 stats->rf_time_delta_average > 0.f ? 1.0f / stats->rf_time_delta_average
   3264 			                                                                    : 0.f);
   3265 			UIParent(unit_column)  ui_label(str8("[s] (FPS)###csv_upload"));
   3266 
   3267 			u32 rf_size = beamformer_context->compute_context.rf_buffer.active_rf_size;
   3268 			UIParent(label_column) ui_label(str8("Input RF Size:"));
   3269 			UIParent(value_column) ui_labelf("%u###csv_rf_size", rf_size);
   3270 			UIParent(unit_column)  ui_label(str8("[B/F]###csv_rf_size"));
   3271 
   3272 			UIParent(label_column) ui_label(str8("DAS RF Size:"));
   3273 			UIParent(value_column) ui_labelf("%u###csv_das_size", cp->rf_size);
   3274 			UIParent(unit_column)  ui_label(str8("[B/F]###csv_das_size"));
   3275 		}
   3276 	}
   3277 }
   3278 
   3279 function void
   3280 ui_build_parameters_listing(BeamformerUIPanel *panel)
   3281 {
   3282 	BeamformerUI *ui = ui_context;
   3283 
   3284 	if ui_context_menu(panel) {
   3285 		UINode *label_column, *button_column;
   3286 		UIParent(ui->context_menu_root)
   3287 		UIChildLayoutAxis(Axis2_X)
   3288 		UIPrefHeight(ui_children_sum(1.f))
   3289 		UIPrefWidth(ui_children_sum(1.f))
   3290 		UIParent(ui_spacer(0))
   3291 		UIChildLayoutAxis(Axis2_Y)
   3292 		{
   3293 			ui_padw(UI_NODE_PAD);
   3294 			UIAxisAlign(Axis2_X, Left)   label_column  = ui_node_from_string(0, str8("###labels"));
   3295 			ui_padw(UI_NODE_PAD * 2.f);
   3296 			UIAxisAlign(Axis2_X, Center)
   3297 				button_column = ui_node_from_string(0, str8("###buttons"));
   3298 			ui_padw(UI_NODE_PAD);
   3299 		}
   3300 
   3301 		UIPrefHeight(ui_text_dim(1.1f, 1.f))
   3302 		UIPrefWidth(ui_text_dim(1.f, 1.f))
   3303 		{
   3304 			UIParent(label_column) ui_label(str8("Block"));
   3305 			UIParent(button_column)
   3306 			{
   3307 				UISignal signal;
   3308 				u32 cycle = beamformer_context->shared_memory->reserved_parameter_blocks;
   3309 				u32 block = panel->u.parameter_listing.parameter_block;
   3310 				UIFlags(cycle <= 1 ? UINodeFlag_Disabled : 0)
   3311 					signal = ui_label_buttonf("%u", block);
   3312 				if (ui_pressed(signal) || ui_scrolled(signal)) {
   3313 					i32 delta = signal.scroll.y + ui_pressed(signal);
   3314 					panel->u.parameter_listing.parameter_block = circular_add(block, delta, cycle);
   3315 				}
   3316 			}
   3317 		}
   3318 	}
   3319 
   3320 	UIFontSize(30.f)
   3321 	UIScroll(Axis2_Count)
   3322 	{
   3323 		ui_top_parent()->child_layout_axis = Axis2_X;
   3324 
   3325 		UINode *label_column, *value_column, *unit_column;
   3326 		UIChildLayoutAxis(Axis2_Y)
   3327 		UIPrefWidth(ui_children_sum(1.0f))
   3328 		UIPrefHeight(ui_children_sum(1.0f))
   3329 		{
   3330 			UIAxisAlign(Axis2_X, Left)   label_column = ui_node_from_string(0, str8("###labels"));
   3331 			ui_padw(UI_NODE_PAD);
   3332 			UIAxisAlign(Axis2_X, Center) value_column = ui_node_from_string(0, str8("###values"));
   3333 			ui_padw(UI_NODE_PAD);
   3334 			UIAxisAlign(Axis2_X, Right)  unit_column  = ui_node_from_string(0, str8("###units"));
   3335 		}
   3336 
   3337 		f32 line_pad_pct = 1.05f;
   3338 		UIPrefWidth(ui_text_dim(1.f, 1.f))
   3339 		UIPrefHeight(ui_text_dim(line_pad_pct, 1.f))
   3340 		{
   3341 			BeamformerUIParameters *bp = &ui_context->parameters;
   3342 			UIParent(label_column) ui_label(str8("Sampling Frequency"));
   3343 			UIParent(value_column) ui_labelf("%0.2f##sampling", bp->sampling_frequency * 1e-6);
   3344 			UIParent(unit_column)  ui_label(str8("[MHz]##sampling"));
   3345 
   3346 			UIParent(label_column) ui_label(str8("Demodulation Frequency"));
   3347 			UIParent(value_column) ui_labelf("%0.2f###demod", bp->demodulation_frequency * 1e-6);
   3348 			UIParent(unit_column)  ui_label(str8("[MHz]##demod"));
   3349 
   3350 			UIParent(label_column) ui_label(str8("Speed of Sound"));
   3351 			UIParent(unit_column)  ui_label(str8("[m/s]"));
   3352 			UIParent(value_column)
   3353 			UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric)
   3354 			{
   3355 				UISignal signal = ui_text_boxf("%0.2f###sound", bp->speed_of_sound);
   3356 				if (ui_tweak_f32_compute_variable(signal, &bp->speed_of_sound, 1.f, 10.f, (v2){{0, inf32()}}))
   3357 					ui->flush_parameters = 1;
   3358 			}
   3359 
   3360 			u32 parameter_block = panel->u.parameter_listing.parameter_block;
   3361 			b32 rebuild_transform = 0;
   3362 
   3363 			BeamformerParameterBlock *pb = beamformer_parameter_block(beamformer_context->shared_memory, parameter_block);
   3364 			BeamformerComputePlan    *cp = beamformer_context->compute_context.compute_plans[parameter_block];
   3365 			m4 das_transform = pb->parameters.das_voxel_transform;
   3366 			if (cp) das_transform = m4_mul(cp->ui_voxel_transform, das_transform);
   3367 			v3 coordinates[2] = {
   3368 				m4_mul_v3(das_transform, (v3){{0.0f, 0.0f, 0.0f}}),
   3369 				m4_mul_v3(das_transform, (v3){{1.0f, 1.0f, 1.0f}}),
   3370 			};
   3371 
   3372 			i32 dimension = iv3_dimension(bp->output_points.xyz);
   3373 			if (dimension > 0) {
   3374 				read_only local_persist str8 dimension_strings[3][2] = {
   3375 					{str8_comp("Start Point"),    str8_comp("End Point")   },
   3376 					{str8_comp("Lateral Extent"), str8_comp("Axial Extent")},
   3377 					{str8_comp("Min Corner"),     str8_comp("Max Corner")  },
   3378 				};
   3379 
   3380 				for (u32 index = 0; index < 2; index++) {
   3381 					UIParent(label_column)
   3382 					{
   3383 						UISignal signal = ui_button(dimension_strings[dimension - 1][index]);
   3384 						signal.node->flags &= ~(UINodeFlag_DrawBackground|UINodeFlag_DrawBorder);
   3385 						if ui_pressed(signal)
   3386 							panel->u.parameter_listing.expand_coordinate[index] ^= 1u;
   3387 					}
   3388 
   3389 					f32 values[3] = {coordinates[index].x, coordinates[index].y, coordinates[index].z};
   3390 					u32 value_count = dimension == 2 ? 2 : 3;
   3391 					v3  normalized_axis = v3_normalize(das_transform.c[index].xyz);
   3392 					if (dimension == 2) {
   3393 						values[0] = v3_dot(normalized_axis, coordinates[0]);
   3394 						values[1] = v3_dot(normalized_axis, coordinates[1]);
   3395 					}
   3396 
   3397 					if (panel->u.parameter_listing.expand_coordinate[index]) {
   3398 						UIPrefHeight(ui_px((f32)ui_font_for_node(value_column).baseSize * line_pad_pct, 1.f))
   3399 						{
   3400 							UIParent(value_column) ui_spacer(0);
   3401 							UIParent(unit_column)  ui_spacer(0);
   3402 						}
   3403 
   3404 						read_only local_persist str8 axis_strings[2][3] = {
   3405 							{str8_comp("  X:"),   str8_comp("  Y:"),   str8_comp("  Z:")},
   3406 							{str8_comp("  Min:"), str8_comp("  Max:"),                  },
   3407 						};
   3408 						str8 *strs  = dimension == 2 ? axis_strings[1] : axis_strings[0];
   3409 						for EachIndex(value_count, it) {
   3410 							UIParent(label_column) ui_labelf("  %.*s##label%u_%u",
   3411 							                                 (i32)strs[it].length, strs[it].data,
   3412 							                                 index, (u32)it);
   3413 							UIParent(unit_column)  ui_labelf("[mm]##%u_%u", index, (u32)it);
   3414 							UIParent(value_column)
   3415 							UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric)
   3416 							{
   3417 								UISignal signal = ui_text_boxf("%0.2f###%u_%u", values[it] * 1e3f, index, (u32)it);
   3418 								rebuild_transform |= ui_tweak_f32_compute_variable(signal, values + it,
   3419 								                                                   1e-3f, 0.5e-3f, V2_INFINITY);
   3420 							}
   3421 						}
   3422 					} else {
   3423 						UIParent(unit_column)  ui_labelf("[mm]##dim%u", index);
   3424 
   3425 						UINode *group;
   3426 						UIParent(value_column)
   3427 						UIChildLayoutAxis(Axis2_X)
   3428 						UIPrefWidth(ui_children_sum(1.f))
   3429 						UIPrefHeight(ui_children_sum(1.f))
   3430 							group = ui_spacer(0);
   3431 
   3432 						UIParent(group)
   3433 						{
   3434 							ui_labelf("{##%u", index);
   3435 							for EachIndex(value_count, it) {
   3436 								if (it != 0) ui_labelf(", ##%u_%u", index, (u32)it);
   3437 								UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric)
   3438 								{
   3439 									UISignal signal = ui_text_boxf("%0.2f###%u_%u", values[it] * 1e3f, index, (u32)it);
   3440 									rebuild_transform |= ui_tweak_f32_compute_variable(signal, values + it,
   3441 									                                                   1e-3f, 0.5e-3f, V2_INFINITY);
   3442 								}
   3443 							}
   3444 							ui_labelf("}##%u", index);
   3445 						}
   3446 					}
   3447 
   3448 					if (dimension == 2) {
   3449 						coordinates[0].E[index] = values[0];
   3450 						coordinates[1].E[index] = values[1];
   3451 					}
   3452 				}
   3453 			}
   3454 
   3455 			if (dimension == 2) {
   3456 				UIParent(label_column) ui_label(str8("Off Axis Position"));
   3457 				UIParent(unit_column)  ui_label(str8("[mm]##off_axis"));
   3458 				UIParent(value_column)
   3459 				UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric)
   3460 				{
   3461 					UISignal signal = ui_text_boxf("%0.2f###off_axis", plane_offset_from_transform(das_transform) * 1e3f);
   3462 					rebuild_transform |= ui_tweak_f32_compute_variable(signal, &ui->off_axis_position,
   3463 					                                                   1e-3f, 0.1e-3f, V2_INFINITY);
   3464 				}
   3465 
   3466 				UIParent(label_column) ui_label(str8("Beamform Plane"));
   3467 				UIParent(unit_column)  UIPrefHeight(ui_em(1.f, 1.f)) ui_spacer(0);
   3468 				UIParent(value_column)
   3469 				UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric)
   3470 				{
   3471 					UISignal signal = ui_text_boxf("%0.2f###beamform_plane", ui->beamform_plane);
   3472 					rebuild_transform |= ui_tweak_f32_compute_variable(signal, &ui->beamform_plane,
   3473 					                                                   1.f, 0.025f, (v2){{-1.f, 1.f}});
   3474 				}
   3475 			}
   3476 
   3477 			UIParent(label_column) ui_label(str8("F#"));
   3478 			UIParent(unit_column)  UIPrefHeight(ui_em(1.f, 1.f)) ui_spacer(0);
   3479 			UIParent(value_column)
   3480 			UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric)
   3481 			{
   3482 				UISignal signal = ui_text_boxf("%0.2f###f_number", bp->f_number);
   3483 				if (ui_tweak_f32_compute_variable(signal, &bp->f_number, 1.f, 0.05f, (v2){{0, inf32()}}))
   3484 					ui->flush_parameters = 1;
   3485 			}
   3486 
   3487 			UIParent(label_column) ui_label(str8("Interpolation"));
   3488 			UIParent(unit_column)  ui_build_node_from_key(0, ui_node_key_zero());
   3489 			UIParent(value_column)
   3490 			UIFlags(UINodeFlag_Scroll)
   3491 			{
   3492 				str8 label = str8_from_s8(beamformer_interpolation_mode_strings[bp->interpolation_mode]);
   3493 				UISignal signal = ui_label_button(label);
   3494 				if (ui_pressed(signal) || ui_scrolled(signal)) {
   3495 					i32 delta = signal.scroll.y + ui_pressed(signal);
   3496 					bp->interpolation_mode = circular_add(bp->interpolation_mode, delta,
   3497 					                                      BeamformerInterpolationMode_Count);
   3498 					ui->flush_parameters = 1;
   3499 				}
   3500 			}
   3501 
   3502 			UIParent(label_column) ui_label(str8("Coherency Weighting"));
   3503 			UIParent(unit_column)  UIPrefHeight(ui_em(1.0f, 1.0f)) ui_spacer(0);
   3504 			UIParent(value_column)
   3505 			UIFlags(UINodeFlag_Scroll)
   3506 			{
   3507 				UISignal signal = ui_label_button(bp->coherency_weighting ?
   3508 				                                  str8("True###coherency_weighting") :
   3509 				                                  str8("False###coherency_weighting"));
   3510 				if (signal.flags & (UISignalFlag_Pressed|UISignalFlag_Scrolled)) {
   3511 					bp->coherency_weighting = !bp->coherency_weighting;
   3512 					ui->flush_parameters = 1;
   3513 				}
   3514 			}
   3515 
   3516 			if (rebuild_transform) {
   3517 				if (ui_rebuild_das_transform(parameter_block, dimension, coordinates[0], coordinates[1]))
   3518 					ui->flush_parameters = 1;
   3519 			}
   3520 		}
   3521 	}
   3522 }
   3523 
   3524 function f32
   3525 ui_slider_update_from_signal(f32 percent, UISignal signal)
   3526 {
   3527 	f32 result = percent;
   3528 	result += 0.05f * signal.scroll.y;
   3529 	if ui_dragging(signal)
   3530 		result = rect_uv(ui_context->last_mouse, ui_node_rect(signal.node)).E[signal.node->parent->child_layout_axis];
   3531 	result = Clamp01(result);
   3532 	return result;
   3533 }
   3534 
   3535 function void
   3536 ui_build_live_imaging_controls(BeamformerUIPanel *panel)
   3537 {
   3538 	BeamformerLiveImagingParameters *lip = &beamformer_context->shared_memory->live_imaging_parameters;
   3539 
   3540 	UIFontSize(30.f)
   3541 	UIScroll(Axis2_Count)
   3542 	{
   3543 		ui_top_parent()->child_layout_axis = Axis2_Y;
   3544 
   3545 		if (popcount_u64(lip->acquisition_kind_enabled_flags) > 1)
   3546 		UIPrefWidth(ui_children_sum(1.f))
   3547 		UIPrefHeight(ui_children_sum(1.f))
   3548 		UIChildLayoutAxis(Axis2_X)
   3549 		UIParent(ui_spacer(0))
   3550 		UIPrefHeight(ui_text_dim(1.1f, 1.f))
   3551 		UIPrefWidth(ui_text_dim(1.f, 1.f))
   3552 		{
   3553 			u32 kind = lip->acquisition_kind;
   3554 			ui_label(str8("Acquisition: "));
   3555 			str8 kind_string = kind < BeamformerAcquisitionKind_Count ? beamformer_acquisition_kind_strings[kind]
   3556 			                                                          : str8("Invalid");
   3557 
   3558 			UISignal signal = ui_label_button(kind_string);
   3559 			if ui_pressed(signal)
   3560 				ui_context_menu_open(signal.node->key, panel);
   3561 
   3562 			if ui_context_menu(panel) {
   3563 				u64 enabled_kinds = atomic_load_u64(&lip->acquisition_kind_enabled_flags);
   3564 
   3565 				UIParent(ui_context->context_menu_root)
   3566 				UIFontSize(24.f)
   3567 				UIChildLayoutAxis(Axis2_X)
   3568 				UIPrefHeight(ui_children_sum(1.f))
   3569 				UIPrefWidth(ui_children_sum(1.f))
   3570 				for EachBit(enabled_kinds, kind)
   3571 				UIParent(ui_spacer(0))
   3572 				{
   3573 					ui_padw(UI_NODE_PAD);
   3574 					UIPrefHeight(ui_text_dim(1.1f, 1.f))
   3575 					UIPrefWidth(ui_text_dim(1.f, 1.f))
   3576 						signal = ui_label_button(beamformer_acquisition_kind_strings[kind]);
   3577 					ui_padw(UI_NODE_PAD);
   3578 
   3579 					if ui_pressed(signal) {
   3580 						ui_context_menu_close();
   3581 						lip->acquisition_kind = kind;
   3582 						atomic_or_u32(&beamformer_context->shared_memory->live_imaging_dirty_flags,
   3583 						              BeamformerLiveImagingDirtyFlags_AcquisitionKind);
   3584 					}
   3585 				}
   3586 			}
   3587 		}
   3588 
   3589 		UIPrefHeight(ui_text_dim(1.1f, 1.f))
   3590 		UIPrefWidth(ui_text_dim(1.f, 1.f))
   3591 		{
   3592 			UINode *spacer;
   3593 			UISignal signal;
   3594 
   3595 			f32 row_height = ui_label(str8("Power:")).node->computed_size[Axis2_Y];
   3596 
   3597 			UIPrefWidth(ui_pct(1.f, 1.f))
   3598 			UIPrefHeight(ui_px(row_height, 1.f))
   3599 			UIChildLayoutAxis(Axis2_X)
   3600 			spacer = ui_spacer(0);
   3601 			UIParent(spacer)
   3602 			{
   3603 				ui_padw(2 * UI_NODE_PAD);
   3604 				v4 hsv_power_slider = {{0.35f * ease_in_out_cubic(1.0f - lip->transmit_power), 0.65f, 0.65f, 1}};
   3605 				UIBGColour(hsv_to_rgb(hsv_power_slider))
   3606 				UIPrefHeight(ui_px(row_height, 1.f))
   3607 				UIPrefWidth(ui_pct(1.f, 0.5f))
   3608 				signal = ui_slider(lip->transmit_power, str8("###transmit_power"));
   3609 				if (signal.flags) {
   3610 					lip->transmit_power = ui_slider_update_from_signal(lip->transmit_power, signal);
   3611 					atomic_or_u32(&beamformer_context->shared_memory->live_imaging_dirty_flags,
   3612 						            BeamformerLiveImagingDirtyFlags_TransmitPower);
   3613 				}
   3614 				ui_padw(2 * UI_NODE_PAD);
   3615 			}
   3616 
   3617 			row_height = ui_label(str8("TGC:")).node->computed_size[Axis2_Y];
   3618 			for EachElement(lip->tgc_control_points, it) {
   3619 				UIPrefWidth(ui_pct(1.f, 1.f))
   3620 				UIPrefHeight(ui_px(row_height, 1.f))
   3621 				UIChildLayoutAxis(Axis2_X)
   3622 				spacer = ui_spacer(0);
   3623 				UIParent(spacer)
   3624 				{
   3625 					ui_padw(2 * UI_NODE_PAD);
   3626 					UIBGColour(g_colour_palette[1])
   3627 					UIPrefHeight(ui_px(row_height, 1.f))
   3628 					UIPrefWidth(ui_pct(1.f, 0.5f))
   3629 					signal = ui_sliderf(lip->tgc_control_points[it], "###tgc_%u", (u32)it);
   3630 					if (signal.flags) {
   3631 						lip->tgc_control_points[it] = ui_slider_update_from_signal(lip->tgc_control_points[it], signal);
   3632 						atomic_or_u32(&beamformer_context->shared_memory->live_imaging_dirty_flags,
   3633 							            BeamformerLiveImagingDirtyFlags_TGCControlPoints);
   3634 					}
   3635 					ui_padw(2 * UI_NODE_PAD);
   3636 				}
   3637 			}
   3638 
   3639 			if (lip->save_enabled) {
   3640 				ui_label(str8("File Name Tag:"));
   3641 				str8 save_name  = (str8){.data = (u8 *)lip->save_name_tag,
   3642 				                         .length = Clamp(lip->save_name_tag_length, 0, countof(lip->save_name_tag))};
   3643 
   3644 
   3645 				v4  save_text_colour = FG_COLOUR;
   3646 				u64 text_input_flags = 0;
   3647 				if (lip->save_name_tag_length <= 0) {
   3648 					save_text_colour.a = 0.6f;
   3649 					save_name = str8("Insert Text...");
   3650 					text_input_flags = UINodeFlag_TextInputClearOnStart;
   3651 				}
   3652 
   3653 				UIPrefWidth(ui_children_sum(1.f))
   3654 				UIPrefHeight(ui_children_sum(1.f))
   3655 				UIChildLayoutAxis(Axis2_X)
   3656 				spacer = ui_spacer(0);
   3657 				UIParent(spacer)
   3658 				{
   3659 					ui_padw(2 * UI_NODE_PAD);
   3660 					UITextColour(save_text_colour)
   3661 					UIFlags(text_input_flags)
   3662 					signal = ui_text_box(push_str8_from_parts(ui_build_arena(), str8(""), save_name,
   3663 					                                          str8("###save_name_field")));
   3664 					if (ui_node_key_equal(signal.node->key, ui_context->text_input_state.node_key))
   3665 						signal.node->text_colour = FG_COLOUR;
   3666 
   3667 					if (signal.flags & UISignalFlag_TextCommit) {
   3668 						str8 string = signal.string;
   3669 						lip->save_name_tag_length = Min(string.length, countof(lip->save_name_tag));
   3670 						memory_copy(lip->save_name_tag, string.data, lip->save_name_tag_length);
   3671 						atomic_or_u32(&beamformer_context->shared_memory->live_imaging_dirty_flags,
   3672 						              BeamformerLiveImagingDirtyFlags_SaveNameTag);
   3673 					}
   3674 				}
   3675 
   3676 				ui_padh(UI_NODE_PAD);
   3677 
   3678 				UIChildLayoutAxis(Axis2_Y)
   3679 				UIAxisAlign(Axis2_X, Center)
   3680 				UIPrefWidth(ui_children_sum(1.f))
   3681 				UIPrefHeight(ui_children_sum(1.f))
   3682 				spacer = ui_spacer(0);
   3683 
   3684 				UIParent(spacer)
   3685 				UITextAlign(Center)
   3686 				UIBGColour((v4){0})
   3687 				UIPrefWidth(ui_text_dim(1.3f, 1.f))
   3688 				UIPrefHeight(ui_text_dim(1.3f, 1.f))
   3689 				{
   3690 					b32  active = lip->save_active;
   3691 					str8 label  = active ? str8("Saving...###save_button") : str8("Save Data###save_button");
   3692 					f32 save_t = beamformer_ui_blinker_update(&panel->u.live_imaging_save_button_blinker, BLINK_SPEED);
   3693 					v4 border_colour = (v4){.a = 0.6f};
   3694 					if (active) border_colour = v4_lerp(BORDER_COLOUR, FOCUSED_COLOUR, ease_in_out_cubic(save_t));
   3695 					UIBorderColour(border_colour)
   3696 					signal = ui_button(label);
   3697 					if ui_pressed(signal) {
   3698 						lip->save_active = !active;
   3699 						atomic_or_u32(&beamformer_context->shared_memory->live_imaging_dirty_flags,
   3700 						              BeamformerLiveImagingDirtyFlags_SaveData);
   3701 					}
   3702 
   3703 					ui_padh(UI_NODE_PAD);
   3704 
   3705 					UIBorderColour((v4){.a = 0.6f})
   3706 					signal = ui_button(str8("Stop Imaging"));
   3707 					if ui_pressed(signal)
   3708 						atomic_or_u32(&beamformer_context->shared_memory->live_imaging_dirty_flags,
   3709 						              BeamformerLiveImagingDirtyFlags_StopImaging);
   3710 				}
   3711 			}
   3712 		}
   3713 	}
   3714 }
   3715 
   3716 function UISignal
   3717 ui_panel_label(BeamformerUIPanel *panel)
   3718 {
   3719 	Stream sb = arena_stream(*ui_build_arena());
   3720 	switch (panel->kind) {
   3721 	InvalidDefaultCase;
   3722 	case BeamformerPanelKind_ComputeBarGraph:{stream_append_s8(&sb, s8("Compute Bar Graph"));}break;
   3723 	case BeamformerPanelKind_ComputeStats:{stream_append_s8(&sb, s8("Compute Stats"));}break;
   3724 	case BeamformerPanelKind_FrameViewLive:{stream_append_s8(&sb, s8("Frame View"));}break;
   3725 	case BeamformerPanelKind_FrameViewXPlane:{stream_append_s8(&sb, s8("X-Plane View"));}break;
   3726 	case BeamformerPanelKind_LiveImagingControls:{stream_append_s8(&sb, s8("Live Controls"));}break;
   3727 	case BeamformerPanelKind_FrameViewCopy:{
   3728 		stream_append_s8(&sb, s8("Frame Copy ["));
   3729 		stream_append_hex_u64(&sb, panel->u.frame_view->frame.id);
   3730 		stream_append_s8(&sb, s8("]#"));
   3731 	}break;
   3732 	case BeamformerPanelKind_ParameterListing:{
   3733 		stream_append_s8(&sb, s8("Parameter Listing ["));
   3734 		stream_append_u64(&sb, panel->u.parameter_listing.parameter_block);
   3735 		stream_append_s8(&sb, s8("]#"));
   3736 	}break;
   3737 	}
   3738 	stream_append_s8(&sb, s8("##"));
   3739 	stream_append_hex_u64(&sb, (u64)panel);
   3740 	s8 title_s8 = arena_stream_commit(ui_build_arena(), &sb);
   3741 
   3742 	UISignal result;
   3743 	UIPrefWidth(ui_text_dim(1.f, 1.f))
   3744 	UIPrefHeight(ui_text_dim(1.4f, 1.f))
   3745 	result = ui_label(str8_from_s8(title_s8));
   3746 
   3747 	return result;
   3748 }
   3749 
   3750 function void
   3751 ui_insert_drop_site_spacer_before(UINode *before, f32 pad_node_width)
   3752 {
   3753 	UIParent(0)
   3754 	{
   3755 		UINode *spacer, *spacer_gap;
   3756 		UIPrefHeight(ui_pct(1.f, 0.5f))
   3757 		UIPrefWidth(ui_px(24.f, 1.f))
   3758 		UIBorderColour((v4){.a = 0.9f})
   3759 		UIBGColour((v4){.a = 0.6f})
   3760 		spacer = ui_spacer(UINodeFlag_DrawBackground|UINodeFlag_DrawBorder);
   3761 
   3762 		UIPrefWidth(ui_px(pad_node_width, 1.f))
   3763 		spacer_gap = ui_spacer(0);
   3764 
   3765 		spacer->parent     = before->parent;
   3766 		spacer_gap->parent = before->parent;
   3767 		spacer->parent->child_count += 2;
   3768 
   3769 		spacer->previous_sibling = before->previous_sibling;
   3770 		spacer->next_sibling     = spacer_gap;
   3771 		before->previous_sibling->next_sibling = spacer;
   3772 
   3773 		spacer_gap->previous_sibling = spacer;
   3774 		spacer_gap->next_sibling     = before;
   3775 
   3776 		before->previous_sibling = spacer_gap;
   3777 	}
   3778 }
   3779 
   3780 function UINode *
   3781 ui_box_pad(UINode *container, UISize pad, str8 tag)
   3782 {
   3783 	UINode *result;
   3784 	UIParent(container)
   3785 	UIAxisSize(Axis2_X, ui_pct(1.f, 0.5f))
   3786 	{
   3787 		UIAxisSize(Axis2_Y, pad) ui_spacer(0);
   3788 
   3789 		UIAxisSize(Axis2_Y, ui_pct(1.f, 0.5f))
   3790 		UIChildLayoutAxis(Axis2_X)
   3791 		UIParent(ui_spacer(0))
   3792 		{
   3793 			UIAxisSize(Axis2_X, pad) ui_spacer(0);
   3794 			UIChildLayoutAxis(container->child_layout_axis)
   3795 			result = ui_node_from_string(0, tag);
   3796 			UIAxisSize(Axis2_X, pad) ui_spacer(0);
   3797 		}
   3798 
   3799 		UIAxisSize(Axis2_Y, pad) ui_spacer(0);
   3800 	}
   3801 	return result;
   3802 }
   3803 
   3804 function print_format(3, 4) UINode *
   3805 ui_box_padf(UINode *container, UISize pad, const char *format, ...)
   3806 {
   3807 	va_list args;
   3808 	va_start(args, format);
   3809 	UINode *result = ui_box_pad(container, pad, push_str8_fv(ui_build_arena(), format, args));
   3810 	va_end(args);
   3811 	return result;
   3812 }
   3813 
   3814 function BeamformerUIPanel *
   3815 ui_panel_group_equip(UINode *node, BeamformerUIPanel *group)
   3816 {
   3817 	BeamformerUIPanel *result = group;
   3818 
   3819 	if (group->kind != BeamformerPanelKind_Split)
   3820 	UIPrefWidth(ui_children_sum(1.f))
   3821 	UIPrefHeight(ui_children_sum(1.f))
   3822 	{
   3823 		assert(group->kind == BeamformerPanelKind_TabGroup);
   3824 		BeamformerUIPanel *focus = result = group->u.tab_focus;
   3825 
   3826 		node->flags |= UINodeFlag_DropSite;
   3827 
   3828 		UINode *tab_bar_node, *tab_clip_node;
   3829 		UIParent(node)
   3830 		UIChildLayoutAxis(Axis2_X)
   3831 		UIPrefWidth(ui_pct(1.f, 1.f))
   3832 		tab_bar_node = ui_node_from_string(UINodeFlag_Clip, str8("###tab_scroll"));
   3833 
   3834 		UIParent(tab_bar_node)
   3835 		UIAxisAlign(Axis2_Y, Center)
   3836 		UIChildLayoutAxis(Axis2_X)
   3837 		tab_clip_node = ui_node_from_string(UINodeFlag_ViewScrollX, str8("###tab_clip"));
   3838 
   3839 		b32 drop_site = ui_node_key_equal(node->key, ui_context->drop_target_key) &&
   3840 		                ui_context->drag_panel &&
   3841 		                !beamformer_registers()->split_left_tree &&
   3842 		                !beamformer_registers()->split_right_tree;
   3843 		b32 drop_site_handled = 0;
   3844 		u32 drop_site_index   = 0;
   3845 		f32 tab_pad = 6.f;
   3846 		UIParent(tab_clip_node)
   3847 		UIFontSize(24.f)
   3848 		{
   3849 			for (BeamformerUIPanel *tab = group->first_child; tab; tab = tab->next_sibling) {
   3850 				ui_padw(tab_pad);
   3851 
   3852 				// NOTE(rnp): push tab
   3853 				UINode *tab_node;
   3854 				UIAxisAlign(Axis2_Y, Center)
   3855 				UIChildLayoutAxis(Axis2_X)
   3856 				// TODO(rnp): per edge border colour
   3857 				UIBorderColour((v4){.a = 0.9f})
   3858 				UIBGColour(tab == focus ? BG_COLOUR : (v4){.a = 0.6f})
   3859 				UIFlags(UINodeFlag_Clickable|
   3860 				        UINodeFlag_DrawBackground|
   3861 				        UINodeFlag_DrawBorder|
   3862 				        UINodeFlag_DrawHotEffects|
   3863 				        UINodeFlag_DrawActiveEffects)
   3864 				tab_node = ui_node_from_stringf(tab == focus ? UINodeFlag_FocusActive : 0, "###tab%p", tab);
   3865 
   3866 				if (drop_site && !drop_site_handled &&
   3867 				    ui_context->last_mouse.x < (tab_node->computed_position[Axis2_X] + 0.5f * tab_node->computed_size[Axis2_X]))
   3868 				{
   3869 					drop_site_handled = 1;
   3870 				  if (ui_context->drag_panel != tab && ui_context->drag_panel != tab->previous_sibling)
   3871 						ui_insert_drop_site_spacer_before(tab_node, tab_pad);
   3872 				}
   3873 
   3874 				UISignal signal = {0};
   3875 				UIParent(tab_node)
   3876 				{
   3877 					ui_padw(UI_BORDER_THICK + tab_pad);
   3878 
   3879 					v4 fg_colour = FG_COLOUR;
   3880 					if (tab != focus) fg_colour.a = 0.8f;
   3881 					UITextColour(fg_colour)
   3882 					ui_panel_label(tab);
   3883 
   3884 					b32 has_settings = (beamformer_panel_infos[tab->kind].flags & BeamformerPanelFlags_HasSettings) != 0;
   3885 					if (tab == focus && has_settings)
   3886 					UIPrefWidth(ui_text_dim(2.f, 1.f))
   3887 					UIPrefHeight(ui_pct(1.f, 1.f))
   3888 					UITextAlign(Right)
   3889 					UIFlags(UINodeFlag_IconText)
   3890 					{
   3891 						ui_padw(0.5f * UI_NODE_PAD);
   3892 
   3893 						signal = ui_label_button(str8("+"));
   3894 						if ui_pressed(signal)
   3895 							ui_context_menu_open(signal.node->key, tab);
   3896 					}
   3897 
   3898 					ui_padw(0.5f * UI_NODE_PAD);
   3899 
   3900 					UIPrefWidth(ui_text_dim(2.f, 1.f))
   3901 					UIPrefHeight(ui_pct(1.f, 1.f))
   3902 					UITextAlign(Center)
   3903 					UIFlags(UINodeFlag_IconText)
   3904 					signal = ui_label_button(str8("x"));
   3905 					if (ui_pressed(signal) || signal.flags & UISignalFlag_MiddlePressed)
   3906 						beamformer_command(beamformer_command_infos[BeamformerCommandKind_CloseTab].string, .tree_node = (u64)tab);
   3907 
   3908 					ui_padw(0.5f * UI_NODE_PAD);
   3909 				}
   3910 
   3911 				if (!drop_site_handled) drop_site_index++;
   3912 
   3913 				signal = ui_signal_from_node(tab_node);
   3914 				if ui_pressed(signal)
   3915 					beamformer_command(beamformer_command_infos[BeamformerCommandKind_FocusTab].string, .tree_node = (u64)tab);
   3916 				if (signal.flags & UISignalFlag_MiddlePressed)
   3917 					beamformer_command(beamformer_command_infos[BeamformerCommandKind_CloseTab].string, .tree_node = (u64)tab);
   3918 				if (ui_dragging(signal) && !point_in_rect(ui_context->last_mouse, ui_node_rect(signal.node)))
   3919 					ui_drag_begin(tab);
   3920 				if ui_released(signal)
   3921 					ui_context->drag_end = 1;
   3922 			}
   3923 
   3924 			ui_padw(tab_pad);
   3925 
   3926 			// NOTE(rnp): context menu opener
   3927 			UISignal signal;
   3928 			UIPrefWidth(ui_text_dim(3.f, 1.f))
   3929 			UIPrefHeight(ui_text_dim(3.f, 1.f))
   3930 			UITextAlign(Center)
   3931 			UIFlags(UINodeFlag_IconText)
   3932 			signal = ui_label_button(str8("+"));
   3933 			if ui_pressed(signal)
   3934 				ui_context_menu_open(signal.node->key, group);
   3935 
   3936 			if ui_context_menu(group) {
   3937 				UIParent(ui_context->context_menu_root)
   3938 				UIChildLayoutAxis(Axis2_X)
   3939 				UIPrefHeight(ui_children_sum(1.f))
   3940 				UIPrefWidth(ui_children_sum(1.f))
   3941 				for EachElement(beamformer_panel_infos, it)
   3942 				{
   3943 					BeamformerPanelInfo *info = beamformer_panel_infos + it;
   3944 					b32 list        = (info->flags & BeamformerPanelFlags_List) != 0;
   3945 					b32 needs_frame = (info->flags & BeamformerPanelFlags_NeedsFrame) != 0;
   3946 					if (list && (!needs_frame || beamformer_frame_valid(beamformer_registers()->frame))) {
   3947 						UIParent(ui_spacer(0))
   3948 						{
   3949 							ui_padw(UI_NODE_PAD);
   3950 							UIPrefHeight(ui_text_dim(1.1f, 1.f))
   3951 							UIPrefWidth(ui_text_dim(1.f, 1.f))
   3952 								signal = ui_label_button(info->display);
   3953 							ui_padw(UI_NODE_PAD);
   3954 
   3955 							if ui_pressed(signal) {
   3956 								ui_context_menu_close();
   3957 								beamformer_command(beamformer_command_infos[BeamformerCommandKind_OpenTab].string,
   3958 								                   .tree_node = (u64)group,
   3959 								                   .string    = info->string);
   3960 							}
   3961 						}
   3962 					}
   3963 				}
   3964 			}
   3965 
   3966 			if (drop_site && !drop_site_handled && ui_context->drag_panel != group->last_child) {
   3967 				drop_site_handled = 1;
   3968 				ui_insert_drop_site_spacer_before(signal.node, tab_pad);
   3969 			}
   3970 
   3971 			ui_padw(tab_pad);
   3972 		}
   3973 
   3974 		ui_signal_from_node(tab_clip_node);
   3975 
   3976 		if (drop_site || drop_site_handled) {
   3977 			beamformer_registers()->drop_target_tree = (u64)group;
   3978 			beamformer_registers()->drop_child_index = drop_site_handled ? drop_site_index : group->child_count;
   3979 		}
   3980 	}
   3981 
   3982 	// NOTE(rnp): close tabgroup button
   3983 	if (!result && group != ui_context->tree)
   3984 	UIParent(ui_box_pad(node, ui_pct(0.5f, 0.5f), str8("")))
   3985 	{
   3986 		ui_top_parent()->semantic_size[Axis2_X] = ui_children_sum(1.f);
   3987 
   3988 		UISignal signal;
   3989 		UIPrefWidth(ui_text_dim(1.5f, 1.f))
   3990 		UIPrefHeight(ui_text_dim(2.f, 1.f))
   3991 		UIBGColour((v4){.a = 0.3f})
   3992 		UIBorderColour((v4){.a = 0.6f})
   3993 		UITextAlign(Center)
   3994 		signal = ui_button(str8("Close Panel"));
   3995 		if ui_pressed(signal)
   3996 			beamformer_command(beamformer_command_infos[BeamformerCommandKind_CloseTab].string, .tree_node = (u64)group);
   3997 	}
   3998 
   3999 	ui_signal_from_node(node);
   4000 
   4001 	return result;
   4002 }
   4003 
   4004 function void
   4005 ui_build_regions(UINode *root_node, BeamformerUIPanel *tree_root)
   4006 {
   4007 	BeamformerUI *ui = ui_context;
   4008 
   4009 	struct tree_frame {
   4010 		BeamformerUIPanel *tree;
   4011 		UINode            *node;
   4012 	} init[64];
   4013 
   4014 	struct {
   4015 		struct tree_frame *data;
   4016 		da_count           count;
   4017 		da_count           capacity;
   4018 	} stack = {init, 0, countof(init)};
   4019 
   4020 	*da_push(ui_build_arena(), &stack) = (struct tree_frame){
   4021 		.node = ui_box_padf(root_node, ui_px(UI_NODE_PAD, 1.f), "%p_padded", root_node),
   4022 		.tree = tree_root,
   4023 	};
   4024 	while (stack.count) {
   4025 		struct tree_frame *top = stack.data + --stack.count;
   4026 
   4027 		BeamformerUIPanel *panel    = top->tree;
   4028 		UINode            *top_node = top->node;
   4029 
   4030 		UIParent(top_node)
   4031 		switch (panel->kind) {
   4032 
   4033 		case BeamformerPanelKind_TabGroup:{
   4034 			UINode *node;
   4035 			UIChildLayoutAxis(Axis2_Y)
   4036 			node = ui_node_from_stringf(UINodeFlag_Clip, "###%p_group", panel);
   4037 			BeamformerUIPanel *next = ui_panel_group_equip(node, panel);
   4038 			if (next) *da_push(ui_build_arena(), &stack) = (struct tree_frame){
   4039 				.tree = next,
   4040 				.node = node,
   4041 			};
   4042 		}break;
   4043 
   4044 		case BeamformerPanelKind_Split:{
   4045 			assert(panel->child_count == 2);
   4046 
   4047 			Axis2 axis = panel->u.split.axis;
   4048 			top_node->child_layout_axis = axis;
   4049 
   4050 			UIAxisSize(axis2_flip(axis), ui_pct(1.f, 0.5f))
   4051 			{
   4052 				f32 split_pct = panel->u.split.fraction;
   4053 
   4054 				UINode *left;
   4055 				UIAxisSize(axis, ui_pct(split_pct, 0.5f))
   4056 				UIChildLayoutAxis(Axis2_Y)
   4057 				left = ui_node_from_stringf(UINodeFlag_Clip, "###%p_left", panel);
   4058 
   4059 				BeamformerUIPanel *next = ui_panel_group_equip(left, panel->first_child);
   4060 				if (next) *da_push(ui_build_arena(), &stack) = (struct tree_frame){
   4061 					.tree = next,
   4062 					.node = left,
   4063 				};
   4064 
   4065 				UIAxisSize(axis, ui_children_sum(1.f))
   4066 				UIChildLayoutAxis(axis)
   4067 				UIParent(ui_node_from_stringf(UINodeFlag_Clickable, "###%p_split", panel))
   4068 				{
   4069 					UIAxisSize(axis, ui_px(UI_NODE_PAD, 1.f)) ui_spacer(0);
   4070 					UIAxisSize(axis, ui_px(UI_SPLIT_HANDLE_THICK, 1.f))
   4071 					UIBGColour((v4){.a = 0.6f})
   4072 					{
   4073 						UINode *rn = ui_spacer(UINodeFlag_DrawBackground|UINodeFlag_DrawHotEffects|UINodeFlag_DrawActiveEffects);
   4074 						rn->hot_t = ui_top_parent()->hot_t;
   4075 					}
   4076 					UIAxisSize(axis, ui_px(UI_NODE_PAD, 1.f)) ui_spacer(0);
   4077 
   4078 					UISignal signal = ui_signal_from_node(ui_top_parent());
   4079 					if ui_dragging(signal) {
   4080 						Rect nr = ui_node_rect(top_node);
   4081 						v2   uv = rect_uv(clamp_v2_rect(ui->last_mouse, nr), nr);
   4082 						panel->u.split.fraction = Clamp(uv.E[panel->u.split.axis], 0.03f, 0.97f);
   4083 					}
   4084 				}
   4085 
   4086 				UINode *right;
   4087 				UIAxisSize(axis, ui_pct(1.f - split_pct, 0.5f))
   4088 				UIChildLayoutAxis(Axis2_Y)
   4089 				right = ui_node_from_stringf(UINodeFlag_Clip, "###%p_right", panel);
   4090 
   4091 				next = ui_panel_group_equip(right, panel->last_child);
   4092 				if (next) *da_push(ui_build_arena(), &stack) = (struct tree_frame){
   4093 					.tree = next,
   4094 					.node = right,
   4095 				};
   4096 			}
   4097 		}break;
   4098 
   4099 		case BeamformerPanelKind_ComputeBarGraph:{
   4100 			UIFontSize(30.f)
   4101 			UIScroll(Axis2_Y)
   4102 			{
   4103 				ui_top_parent()->child_layout_axis = Axis2_X;
   4104 
   4105 				UINode *label_column, *bar_column;
   4106 				UIAxisAlign(Axis2_X, Left)
   4107 				UIChildLayoutAxis(Axis2_Y)
   4108 				UIPrefWidth(ui_children_sum(1.f))
   4109 				UIPrefHeight(ui_children_sum(1.f))
   4110 				{
   4111 					UIAxisAlign(Axis2_X, Right)
   4112 					label_column = ui_node_from_string(0, str8("###labels"));
   4113 					ui_padw(UI_NODE_PAD);
   4114 
   4115 					f32 bar_width = ui_top_parent()->parent->parent->parent->computed_size[Axis2_X]
   4116 					                - label_column->computed_size[Axis2_X] - UI_NODE_PAD;
   4117 					bar_column = ui_node_from_string(UINodeFlag_CustomDraw, str8("###bars"));
   4118 					bar_column->semantic_size[Axis2_X] = ui_px(bar_width, 0.1f);
   4119 					bar_column->semantic_size[Axis2_Y] = ui_px(label_column->computed_size[Axis2_Y], 1.f);
   4120 					bar_column->custom_draw_function = beamformer_ui_custom_draw_compute_bar_graph;
   4121 				}
   4122 				UIParent(label_column)
   4123 				UIPrefHeight(ui_text_dim(1.3f, 1.f))
   4124 				UIPrefWidth(ui_text_dim(1.f, 1.f))
   4125 				for (i32 i = 0; i < 4; i++)
   4126 					ui_labelf("%d:", -i);
   4127 			}
   4128 
   4129 		}break;
   4130 
   4131 		case BeamformerPanelKind_ComputeStats:{
   4132 			u32 selected_plan = ui->selected_parameter_block % BeamformerMaxParameterBlocks;
   4133 			BeamformerComputePlan *cp = beamformer_context->compute_context.compute_plans[selected_plan];
   4134 			if (!cp) cp = &beamformer_nil_compute_plan;
   4135 			f32 t = beamformer_ui_blinker_update(&panel->u.compute_stats_broken_shader_blinker, BLINK_SPEED);
   4136 			ui_build_compute_stats(cp, t);
   4137 		}break;
   4138 
   4139 		case BeamformerPanelKind_FrameViewXPlane:
   4140 		{
   4141 			BeamformerFrameView *view = panel->u.frame_view;
   4142 			b32 any_valid = 0;
   4143 			for EachElement(view->plane_active, plane)
   4144 				any_valid |= (view->plane_active[plane] && ui_context->latest_plane[plane].timeline_valid_value);
   4145 			if (any_valid) {
   4146 				UINode *container;
   4147 				UIChildLayoutAxis(Axis2_Y)
   4148 				UIPrefWidth(ui_pct(1.f, 0.5f))
   4149 				UIPrefHeight(ui_pct(1.f, 0.5f))
   4150 				UIAxisAlign(Axis2_X, Center)
   4151 					container = ui_node_from_string(0, str8("###frame_view_container"));
   4152 				ui_build_3d_xplane_frame_view(container, view);
   4153 			}
   4154 			if ui_context_menu(panel) ui_build_3d_xplane_context_menu(view);
   4155 		}break;
   4156 
   4157 		case BeamformerPanelKind_FrameViewCopy:
   4158 		case BeamformerPanelKind_FrameViewLive:
   4159 		{
   4160 			BeamformerFrameView *view = panel->u.frame_view;
   4161 			if (iv3_dimension(view->frame.points) != 0) {
   4162 				// TODO(rnp): cleanup, why do we need this extra container
   4163 				UINode *container;
   4164 				UIChildLayoutAxis(Axis2_Y)
   4165 				UIPrefWidth(ui_pct(1.f, 0.5f))
   4166 				UIPrefHeight(ui_pct(1.f, 0.5f))
   4167 				UIAxisAlign(Axis2_X, Center)
   4168 					container = ui_node_from_string(0, str8("###frame_view_container"));
   4169 				ui_build_frame_view(container, view);
   4170 			}
   4171 			if ui_context_menu(panel) ui_build_frame_view_context_menu(panel, view);
   4172 		}break;
   4173 
   4174 		case BeamformerPanelKind_ParameterListing:{ ui_build_parameters_listing(panel); }break;
   4175 
   4176 		case BeamformerPanelKind_LiveImagingControls:{ ui_build_live_imaging_controls(panel); }break;
   4177 
   4178 		InvalidDefaultCase;
   4179 		}
   4180 
   4181 		ui_signal_from_node(top_node);
   4182 	}
   4183 }
   4184 
   4185 function UINode *
   4186 ui_build_drag_hover_node(void)
   4187 {
   4188 	BeamformerUI *ui = ui_context;
   4189 
   4190 	BeamformerUIPanel *tree   = (BeamformerUIPanel *)beamformer_registers()->drop_target_tree;
   4191 	UINode            *target = (UINode *)ui->drop_target_node;
   4192 	Axis2 axis = beamformer_registers()->split_axis;
   4193 	Axis2 flip = axis2_flip(axis);
   4194 	Rect  nr   = ui_node_rect(target);
   4195 	f32   pct     = 4.0f;
   4196 	f32   off_pct = 0.95f;
   4197 	if (target == ui->root_node)
   4198 		pct = 0.1f;
   4199 	if (tree->kind == BeamformerPanelKind_TabGroup) {
   4200 		off_pct = 0.8f;
   4201 		pct     = 0.3f;
   4202 	}
   4203 	if (beamformer_registers()->split_left_tree == beamformer_registers()->split_right_tree)
   4204 		off_pct = pct = 0.8f;
   4205 
   4206 	UINode *result = push_struct(ui_build_arena(), UINode);
   4207 	result->flags     = UINodeFlag_DrawBackground;
   4208 	result->bg_colour = NODE_SPLIT_COLOUR;
   4209 	result->computed_size[flip]     = off_pct * nr.size.E[flip];
   4210 	result->computed_size[axis]     = pct     * nr.size.E[axis];
   4211 	result->computed_position[axis] = nr.pos.E[axis];
   4212 	result->computed_position[flip] = nr.pos.E[flip];
   4213 
   4214 	if (target == ui->root_node) {
   4215 		result->computed_position[flip] += 0.5f * (nr.size.E[flip] - result->computed_size[flip]);
   4216 		if (beamformer_registers()->split_left_tree == (u64)ui->tree)
   4217 			result->computed_position[axis] = nr.pos.E[axis] + nr.size.E[axis] - result->computed_size[axis];
   4218 	} else {
   4219 		result->computed_position[axis] += 0.5f * (nr.size.E[axis] - result->computed_size[axis]);
   4220 		result->computed_position[flip] += 0.5f * (nr.size.E[flip] - result->computed_size[flip]);
   4221 
   4222 		if (tree->kind == BeamformerPanelKind_TabGroup) {
   4223 			if (beamformer_registers()->split_left_tree == (u64)tree)
   4224 				result->computed_position[axis] += 0.45f * (nr.size.E[axis] - result->computed_size[axis]);
   4225 			if (beamformer_registers()->split_right_tree == (u64)tree)
   4226 				result->computed_position[axis] -= 0.45f * (nr.size.E[axis] - result->computed_size[axis]);
   4227 		}
   4228 	}
   4229 
   4230 	return result;
   4231 }
   4232 
   4233 function b32
   4234 ui_build_drag_split_box(Axis2 axis, b32 two_way, i32 highlight_index, str8 tag)
   4235 {
   4236 	UINode *split_box;
   4237 	UIAxisAlign(axis2_flip(axis), Center)
   4238 	UIAxisSize(axis2_flip(axis), ui_px(60.f, 1.f))
   4239 	UIAxisSize(axis, ui_children_sum(1.f))
   4240 	UIChildLayoutAxis(axis)
   4241 	UIBorderColour((v4){.a = 0.8f})
   4242 	UIBGColour(BG_COLOUR)
   4243 	split_box = ui_node_from_string(UINodeFlag_DrawBorder|UINodeFlag_DrawBackground,
   4244 	                                push_str8_from_parts(ui_build_arena(), str8(""),
   4245 	                                                     str8("drag_split_box_"), tag));
   4246 	b32 result = point_in_rect(ui_context->last_mouse, ui_node_rect(split_box));
   4247 
   4248 	UIParent(split_box)
   4249 	UIAxisSize(axis2_flip(axis), ui_pct(0.7f, 0.5f))
   4250 	UIAxisSize(axis, ui_px(1.5f * UI_NODE_PAD, 1.f))
   4251 	{
   4252 		UIAxisSize(axis, ui_px(UI_NODE_PAD, 1.f)) ui_spacer(0);
   4253 
   4254 		UIBorderColour((v4){.a = highlight_index <= 0 ? 0.0f : 0.4f})
   4255 		UIBGColour(highlight_index <= 0 ? NODE_SPLIT_COLOUR : (v4){0})
   4256 		ui_spacer(UINodeFlag_DrawBorder|UINodeFlag_DrawBackground);
   4257 
   4258 		UIAxisSize(axis, ui_px(UI_NODE_PAD, 1.f)) ui_spacer(0);
   4259 
   4260 		if (two_way) {
   4261 			UIBorderColour((v4){.a = highlight_index != 0 ? 0.0f : 0.4f})
   4262 			UIBGColour(highlight_index != 0 ? NODE_SPLIT_COLOUR : (v4){0})
   4263 			ui_spacer(UINodeFlag_DrawBorder|UINodeFlag_DrawBackground);
   4264 
   4265 			UIAxisSize(axis, ui_px(UI_NODE_PAD, 1.f)) ui_spacer(0);
   4266 		}
   4267 	}
   4268 	return result;
   4269 }
   4270 
   4271 function b32
   4272 ui_build_drag_overlay_splitter(Axis2 axis, b32 two_way, i32 highlight_index, str8 tag)
   4273 {
   4274 	b32 result = 0;
   4275 
   4276 	UINode *container;
   4277 	UIChildLayoutAxis(axis2_flip(axis))
   4278 	UIAxisAlign(axis2_flip(axis), Center)
   4279 	UIAxisSize(axis, ui_children_sum(1.f))
   4280 	UIAxisSize(axis2_flip(axis), ui_pct(1.f, 0.5f))
   4281 	{
   4282 		container = ui_spacer(0);
   4283 	}
   4284 
   4285 	UIParent(container)
   4286 	result = ui_build_drag_split_box(axis, two_way, highlight_index, tag);
   4287 	return result;
   4288 }
   4289 
   4290 function void
   4291 ui_build_drag_overlay(Rect window_rect)
   4292 {
   4293 	BeamformerUI *ui = ui_context;
   4294 
   4295 	UIChildLayoutAxis(Axis2_Y)
   4296 	UIPrefWidth(ui_px(window_rect.size.x, 1.f))
   4297 	UIPrefHeight(ui_px(window_rect.size.y, 1.f))
   4298 	ui->drag_overlay_edges_root = ui_node_from_string(0, str8("drag_overlay_edges_root"));
   4299 
   4300 	UIParent(ui->drag_overlay_edges_root)
   4301 	{
   4302 		if (ui_build_drag_overlay_splitter(Axis2_Y, 0, 0, str8("top")))
   4303 		{
   4304 			beamformer_registers()->split_axis       = Axis2_Y;
   4305 			beamformer_registers()->split_left_tree  = 0;
   4306 			beamformer_registers()->split_right_tree = (u64)ui->tree;
   4307 			beamformer_registers()->drop_target_tree = (u64)ui->tree;
   4308 			ui->drop_target_node = ui->root_node;
   4309 		}
   4310 
   4311 		UIPrefHeight(ui_pct(1.f, 0.5f))
   4312 		UIParent(ui_spacer(0))
   4313 		{
   4314 			if (ui_build_drag_overlay_splitter(Axis2_X, 0, 0, str8("left")))
   4315 			{
   4316 				beamformer_registers()->split_axis       = Axis2_X;
   4317 				beamformer_registers()->split_left_tree  = 0;
   4318 				beamformer_registers()->split_right_tree = (u64)ui->tree;
   4319 				beamformer_registers()->drop_target_tree = (u64)ui->tree;
   4320 				ui->drop_target_node = ui->root_node;
   4321 			}
   4322 
   4323 			UIPrefWidth(ui_pct(1.f, 0.5f)) ui_spacer(0);
   4324 
   4325 			if (ui_build_drag_overlay_splitter(Axis2_X, 0, 0, str8("right")))
   4326 			{
   4327 				beamformer_registers()->split_axis       = Axis2_X;
   4328 				beamformer_registers()->split_left_tree  = (u64)ui->tree;
   4329 				beamformer_registers()->split_right_tree = 0;
   4330 				beamformer_registers()->drop_target_tree = (u64)ui->tree;
   4331 				ui->drop_target_node = ui->root_node;
   4332 			}
   4333 		}
   4334 
   4335 		if (ui_build_drag_overlay_splitter(Axis2_Y, 0, 0, str8("bottom")))
   4336 		{
   4337 			beamformer_registers()->split_axis       = Axis2_Y;
   4338 			beamformer_registers()->split_left_tree  = (u64)ui->tree;
   4339 			beamformer_registers()->split_right_tree = 0;
   4340 			beamformer_registers()->drop_target_tree = (u64)ui->tree;
   4341 			ui->drop_target_node = ui->root_node;
   4342 		}
   4343 	}
   4344 
   4345 	struct tree_frame {
   4346 		BeamformerUIPanel *tree;
   4347 		UINode            *node;
   4348 	} init[64];
   4349 
   4350 	struct {
   4351 		struct tree_frame *data;
   4352 		da_count           count;
   4353 		da_count           capacity;
   4354 	} stack = {init, 0, countof(init)};
   4355 
   4356 	UIChildLayoutAxis(Axis2_Y)
   4357 	UIPrefWidth(ui_px(window_rect.size.x, 1.f))
   4358 	UIPrefHeight(ui_px(window_rect.size.y, 1.f))
   4359 	ui->drag_overlay_root = ui_node_from_string(0, str8("drag_overlay_root"));
   4360 
   4361 	*da_push(ui_build_arena(), &stack) = (struct tree_frame){
   4362 		.node = ui->drag_overlay_root,
   4363 		.tree = ui->tree,
   4364 	};
   4365 	while (stack.count) {
   4366 		struct tree_frame *top = stack.data + --stack.count;
   4367 
   4368 		BeamformerUIPanel *panel    = top->tree;
   4369 		UINode            *top_node = top->node;
   4370 
   4371 		UIParent(top_node)
   4372 		switch (panel->kind) {
   4373 		default:{}break;
   4374 		case BeamformerPanelKind_Split:{
   4375 			Axis2 axis = panel->u.split.axis;
   4376 			top_node->child_layout_axis = axis;
   4377 
   4378 			UIAxisSize(axis2_flip(axis), ui_pct(1.f, 0.5f))
   4379 			{
   4380 				f32 split_pct = panel->u.split.fraction;
   4381 
   4382 				UINode *spacer;
   4383 				UIAxisSize(axis, ui_pct(split_pct, 0.5f)) spacer = ui_spacer(0);
   4384 
   4385 				if (panel->first_child->kind == BeamformerPanelKind_Split) {
   4386 					*da_push(ui_build_arena(), &stack) = (struct tree_frame){
   4387 						.tree = panel->first_child,
   4388 						.node = spacer,
   4389 					};
   4390 				}
   4391 
   4392 				UIChildLayoutAxis(axis2_flip(axis))
   4393 				UIAxisAlign(axis2_flip(axis), Center)
   4394 				UIAxisSize(axis, ui_children_sum(1.f))
   4395 				UIParent(ui_spacer(0))
   4396 				{
   4397 					// TODO(rnp): cleanup
   4398 					Stream sb = arena_stream(*ui_build_arena());
   4399 					stream_appendf(&sb, "###%p_split", panel);
   4400 					s8 tag = arena_stream_commit(ui_build_arena(), &sb);
   4401 
   4402 					if (ui_build_drag_split_box(axis, 1, -1, str8_from_s8(tag))) {
   4403 						beamformer_registers()->split_axis       = axis;
   4404 						beamformer_registers()->split_left_tree  = (u64)panel;
   4405 						beamformer_registers()->split_right_tree = (u64)ui->drag_panel;
   4406 						beamformer_registers()->drop_target_tree = (u64)panel;
   4407 						ui->drop_target_node = ui_top_parent();
   4408 					}
   4409 				}
   4410 
   4411 				UIAxisSize(axis, ui_pct(1.f - split_pct, 0.5f)) spacer = ui_spacer(0);
   4412 
   4413 				if (panel->last_child->kind == BeamformerPanelKind_Split) {
   4414 					*da_push(ui_build_arena(), &stack) = (struct tree_frame){
   4415 						.tree = panel->last_child,
   4416 						.node = spacer,
   4417 					};
   4418 				}
   4419 			}
   4420 		}break;
   4421 		}
   4422 	}
   4423 
   4424 	ui->drag_overlay_tab_root = 0;
   4425 	if (beamformer_registers()->drop_target_tree &&
   4426 	    !beamformer_registers()->split_left_tree &&
   4427 	    !beamformer_registers()->split_right_tree)
   4428 	{
   4429 		BeamformerUIPanel *group  = (BeamformerUIPanel *)beamformer_registers()->drop_target_tree;
   4430 		UINode            *target = ui_node_from_key(ui->drop_target_key);
   4431 
   4432 		assert(!group->parent || (group->parent && group->parent->kind == BeamformerPanelKind_Split));
   4433 		Axis2 parent_axis = group->parent ? group->parent->u.split.axis : Axis2_Count;
   4434 
   4435 		Rect tr = ui_node_rect(target);
   4436 		if (point_in_rect(ui->last_mouse, tr)) {
   4437 			UIPrefWidth(ui_px(tr.size.x, 1.f))
   4438 			UIPrefHeight(ui_px(tr.size.h, 1.f))
   4439 			UIAxisAlign(Axis2_X, Center)
   4440 			UIAxisAlign(Axis2_Y, Center)
   4441 			UIChildLayoutAxis(Axis2_Y)
   4442 			{
   4443 				ui->drag_overlay_tab_root = ui_node_from_string(0, str8("drag_overlay_tab_root"));
   4444 			}
   4445 
   4446 			ui->drag_overlay_tab_root->computed_position[Axis2_X] = tr.pos.x;
   4447 			ui->drag_overlay_tab_root->computed_position[Axis2_Y] = tr.pos.y;
   4448 
   4449 			UINode *inner;
   4450 			UIAxisAlign(Axis2_X, Center)
   4451 			UIChildLayoutAxis(Axis2_Y)
   4452 			UIPrefHeight(ui_children_sum(1.f))
   4453 			UIPrefWidth(ui_children_sum(1.f))
   4454 			UIParent(ui->drag_overlay_tab_root)
   4455 			inner = ui_spacer(0);
   4456 
   4457 			UIParent(inner)
   4458 			{
   4459 				if (parent_axis != Axis2_Y && ui_build_drag_split_box(Axis2_Y, 1, 0, str8("top")))
   4460 				{
   4461 					beamformer_registers()->split_axis       = Axis2_Y;
   4462 					beamformer_registers()->split_left_tree  = (u64)ui->drag_panel;
   4463 					beamformer_registers()->split_right_tree = (u64)group;
   4464 					ui->drop_target_node = target;
   4465 				}
   4466 
   4467 				ui_padh(UI_NODE_PAD);
   4468 
   4469 				UIPrefHeight(ui_children_sum(1.f))
   4470 				UIPrefWidth(ui_children_sum(1.f))
   4471 				UIParent(ui_spacer(0))
   4472 				{
   4473 					if (parent_axis != Axis2_X && ui_build_drag_split_box(Axis2_X, 1, 0, str8("left")))
   4474 					{
   4475 						beamformer_registers()->split_axis       = Axis2_X;
   4476 						beamformer_registers()->split_left_tree  = (u64)ui->drag_panel;
   4477 						beamformer_registers()->split_right_tree = (u64)group;
   4478 						ui->drop_target_node = target;
   4479 					}
   4480 
   4481 					ui_padw(UI_NODE_PAD);
   4482 
   4483 					if (parent_axis != Axis2_Count &&
   4484 					    ui_build_drag_split_box(axis2_flip(parent_axis), 0, 0, str8("center")))
   4485 					{
   4486 						beamformer_registers()->split_left_tree  = (u64)group;
   4487 						beamformer_registers()->split_right_tree = (u64)group;
   4488 						beamformer_registers()->drop_target_tree = (u64)group;
   4489 						beamformer_registers()->drop_child_index = group->child_count;
   4490 						ui->drop_target_node = target;
   4491 					}
   4492 
   4493 					ui_padw(UI_NODE_PAD);
   4494 
   4495 					if (parent_axis != Axis2_X && ui_build_drag_split_box(Axis2_X, 1, 1, str8("right")))
   4496 					{
   4497 						beamformer_registers()->split_axis       = Axis2_X;
   4498 						beamformer_registers()->split_left_tree  = (u64)group;
   4499 						beamformer_registers()->split_right_tree = (u64)ui->drag_panel;
   4500 						ui->drop_target_node = target;
   4501 					}
   4502 				}
   4503 
   4504 				ui_padh(UI_NODE_PAD);
   4505 
   4506 				if (parent_axis != Axis2_Y && ui_build_drag_split_box(Axis2_Y, 1, 1, str8("bottom")))
   4507 				{
   4508 					beamformer_registers()->split_axis       = Axis2_Y;
   4509 					beamformer_registers()->split_left_tree  = (u64)group;
   4510 					beamformer_registers()->split_right_tree = (u64)ui->drag_panel;
   4511 					ui->drop_target_node = target;
   4512 				}
   4513 			}
   4514 		}
   4515 	}
   4516 }
   4517 
   4518 function void
   4519 ui_layout_constrain(UINode *root)
   4520 {
   4521 	assert(!ui_node_is_nil(root->first_child));
   4522 
   4523 	// NOTE(rnp): for violations in non-layout axis all we can do is clamp
   4524 	{
   4525 		Axis2 axis  = axis2_flip(root->child_layout_axis);
   4526 		if ((root->flags & (UINodeFlag_AllowOverflowX << axis)) == 0) {
   4527 			for (UINode *child = root->first_child; !ui_node_is_nil(child); child = child->next_sibling)
   4528 				child->computed_size[axis] = Min(child->computed_size[axis], root->computed_size[axis]);
   4529 		}
   4530 	}
   4531 
   4532 	Axis2 axis = root->child_layout_axis;
   4533 	if ((root->flags & (UINodeFlag_AllowOverflowX << axis)) == 0) {
   4534 		f32 allowed_size        = root->computed_size[axis];
   4535 		f32 total_size          = 0;
   4536 		f32 total_weighted_size = 0;
   4537 
   4538 		for (UINode *child = root->first_child; !ui_node_is_nil(child); child = child->next_sibling) {
   4539 			total_size          += child->computed_size[axis];
   4540 			total_weighted_size += child->computed_size[axis] * (1.0f - child->semantic_size[axis].strictness);
   4541 		}
   4542 
   4543 		f32 remaining_size = root->computed_size[axis];
   4544 		f32 violation = total_size - allowed_size;
   4545 		if (violation > 0 && total_weighted_size > 0) {
   4546 			f32 fixup_fraction = Clamp01(violation / total_weighted_size);
   4547 			for (UINode *child = root->first_child; !ui_node_is_nil(child); child = child->next_sibling) {
   4548 				f32 fixup = Max(0, child->computed_size[axis] * (1.0f - child->semantic_size[axis].strictness));
   4549 				child->computed_size[axis] -= fixup * fixup_fraction;
   4550 
   4551 				if (child->semantic_size[axis].kind != UISizeKind_PercentOfParent)
   4552 					remaining_size -= child->computed_size[axis];
   4553 			}
   4554 		}
   4555 
   4556 		// NOTE(rnp): fixup sizes dependant on parent
   4557 		for (UINode *child = root->first_child; !ui_node_is_nil(child); child = child->next_sibling)
   4558 			if (child->semantic_size[axis].kind == UISizeKind_PercentOfParent)
   4559 				child->computed_size[axis] = remaining_size * child->semantic_size[axis].value;
   4560 	}
   4561 }
   4562 
   4563 function void
   4564 ui_layout_nodes(UINode *root)
   4565 {
   4566 	struct node_frame {
   4567 		UINode *node;
   4568 		// NOTE(rnp): for post order traversal
   4569 		b32     visited;
   4570 	} init[64] = {0};
   4571 
   4572 	struct {
   4573 		struct node_frame *data;
   4574 		da_count           count;
   4575 		da_count           capacity;
   4576 	} stack = {init, 0, countof(init)};
   4577 
   4578 	///////////////////////
   4579 	// NOTE(rnp): First Pass: non dependant sizes
   4580 	da_push(ui_build_arena(), &stack)->node = root;
   4581 	while (stack.count) {
   4582 		struct node_frame *top = stack.data + --stack.count;
   4583 		UINode *node = top->node;
   4584 
   4585 		if (node->flags & UINodeFlag_DrawText) {
   4586 			Font font   = ui_font_for_node(node);
   4587 			str8 string = ui_draw_part_from_key_string(node->string);
   4588 			if (node->flags & UINodeFlag_IconText)
   4589 				node->text_size = measure_text_tight(font, string);
   4590 			else
   4591 				node->text_size = measure_text(font, string);
   4592 		}
   4593 
   4594 		for EachElement(node->semantic_size, it) {
   4595 			switch (node->semantic_size[it].kind) {
   4596 			case UISizeKind_Pixels:{node->computed_size[it] = node->semantic_size[it].value;}break;
   4597 
   4598 			case UISizeKind_TextContent:{
   4599 				node->computed_size[it] = node->semantic_size[it].value * node->text_size.E[it];
   4600 			}break;
   4601 
   4602 			default:{}break;
   4603 			}
   4604 		}
   4605 
   4606 		// NOTE(rnp): push children
   4607 		for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling)
   4608 			da_push(ui_build_arena(), &stack)->node = child;
   4609 	}
   4610 
   4611 	///////////////////////
   4612 	// NOTE(rnp): Second Pass (Pre Order): parent dependant sizes
   4613 	da_push(ui_build_arena(), &stack)->node = root;
   4614 	while (stack.count) {
   4615 		struct node_frame *top = stack.data + --stack.count;
   4616 		UINode *node = top->node;
   4617 
   4618 		for EachElement(node->semantic_size, it) {
   4619 			if (node->semantic_size[it].kind == UISizeKind_PercentOfParent) {
   4620 				f32 parent_size = node->parent->computed_size[it];
   4621 				node->computed_size[it] = node->semantic_size[it].value * parent_size;
   4622 			}
   4623 		}
   4624 
   4625 		// NOTE(rnp): push children
   4626 		for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling)
   4627 			da_push(ui_build_arena(), &stack)->node = child;
   4628 	}
   4629 
   4630 	///////////////////////
   4631 	// NOTE(rnp): Third Pass (Post Order): child dependant sizes
   4632 	da_push(ui_build_arena(), &stack)->node = root;
   4633 	while (stack.count) {
   4634 		struct node_frame *top = stack.data + stack.count - 1;
   4635 
   4636 		UINode *node = top->node;
   4637 		if (!top->visited && node->child_count) {
   4638 			top->visited = 1;
   4639 
   4640 			// NOTE(rnp): push children
   4641 			for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling)
   4642 				da_push(ui_build_arena(), &stack)->node = child;
   4643 		} else {
   4644 			// NOTE(rnp): pop
   4645 			stack.count--;
   4646 
   4647 			for EachElement(node->semantic_size, it) {
   4648 				if (node->semantic_size[it].kind == UISizeKind_ChildrenSum) {
   4649 					f32 size_sum = 0;
   4650 					for (UINode *child = node->first_child;
   4651 					     !ui_node_is_nil(child);
   4652 					     child = child->next_sibling)
   4653 					{
   4654 						if (it == node->child_layout_axis) {
   4655 							size_sum += child->computed_size[it];
   4656 						} else {
   4657 							size_sum = Max(size_sum, child->computed_size[it]);
   4658 						}
   4659 					}
   4660 					node->computed_size[it] = size_sum;
   4661 				}
   4662 			}
   4663 		}
   4664 	}
   4665 
   4666 	///////////////////////
   4667 	// NOTE(rnp): Fourth Pass (Pre Order): solve violations
   4668 	da_push(ui_build_arena(), &stack)->node = root;
   4669 	while (stack.count) {
   4670 		struct node_frame *top = stack.data + --stack.count;
   4671 
   4672 		UINode *node = top->node;
   4673 		if (node->child_count)
   4674 			ui_layout_constrain(node);
   4675 
   4676 		// NOTE(rnp): push children
   4677 		for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling)
   4678 			da_push(ui_build_arena(), &stack)->node = child;
   4679 	}
   4680 
   4681 	///////////////////////
   4682 	// NOTE(rnp): Final Pass (Pre Order): fill positions
   4683 	da_push(ui_build_arena(), &stack)->node = root;
   4684 	while (stack.count) {
   4685 		struct node_frame *top = stack.data + --stack.count;
   4686 
   4687 		UINode *node = top->node;
   4688 		Axis2 layout_axis  = node->child_layout_axis;
   4689 		Axis2 flipped_axis = axis2_flip(layout_axis);
   4690 		f32   offset       = 0;
   4691 		for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling) {
   4692 			child->computed_position[flipped_axis] = node->computed_position[flipped_axis];
   4693 			child->computed_position[layout_axis]  = offset + node->computed_position[layout_axis];
   4694 			offset += child->computed_size[layout_axis];
   4695 		}
   4696 
   4697 		for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling) {
   4698 			for EachElement(node->alignment, axis) {
   4699 				f32 size_delta = node->computed_size[axis] - child->computed_size[axis];
   4700 				child->computed_position[axis] += ui_alignment_correction(node->alignment[axis], size_delta);
   4701 			}
   4702 		}
   4703 
   4704 		// NOTE(rnp): push children
   4705 		for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling)
   4706 			if (child->child_count > 0)
   4707 				da_push(ui_build_arena(), &stack)->node = child;
   4708 	}
   4709 }
   4710 
   4711 function void
   4712 ui_draw_nodes(UINode *root, Rect window_rect)
   4713 {
   4714 	BeamformerUI *ui = ui_context;
   4715 
   4716 	struct node_frame {
   4717 		b32     visited;
   4718 		UINode *node;
   4719 	} init[64];
   4720 
   4721 	struct {
   4722 		struct node_frame *data;
   4723 		da_count           count;
   4724 		da_count           capacity;
   4725 	} stack = {init, 0, countof(init)};
   4726 
   4727 	u32 colour_index = 0;
   4728 	(void)colour_index;
   4729 
   4730 	da_push(ui_build_arena(), &stack)->node = root;
   4731 	while (stack.count) {
   4732 		struct node_frame *top = stack.data + stack.count - 1;
   4733 
   4734 		UINode *node = top->node;
   4735 		if (!top->visited) {
   4736 			top->visited = 1;
   4737 
   4738 			Rect r = ui_node_rect(node);
   4739 			if (node->flags & UINodeFlag_Clip)
   4740 				BeginScissorMode(r.pos.x, r.pos.y, r.size.w, r.size.h);
   4741 
   4742 			if (node->flags & UINodeFlag_ViewScroll) {
   4743 				v2 view_off = node->view_scroll_offset;
   4744 				rlPushMatrix();
   4745 				rlTranslatef(-view_off.x, -view_off.y, 0);
   4746 			}
   4747 
   4748 			//v4 colour = g_colour_palette[(colour_index++) % countof(g_colour_palette)];
   4749 			//DrawRectangleLinesEx(rl_rect(r), 4.0f, colour_from_normalized(colour));
   4750 
   4751 			v4 bg_colour = node->bg_colour;
   4752 			if (node->flags & UINodeFlag_DrawHotEffects)
   4753 				bg_colour = v4_lerp(bg_colour, HOVERED_COLOUR, node->hot_t);
   4754 
   4755 			if (node->flags & UINodeFlag_DrawBackground)
   4756 				DrawRectangleRec(rl_rect(r), colour_from_normalized(bg_colour));
   4757 
   4758 			if (node->flags & UINodeFlag_DrawBorder) {
   4759 				v4  colour = node->border_colour;
   4760 				u64 masked = node->flags & (UINodeFlag_DrawBackground|UINodeFlag_DrawHotEffects);
   4761 				if (masked == UINodeFlag_DrawHotEffects)
   4762 					colour = v4_lerp(colour, HOVERED_COLOUR, node->hot_t);
   4763 
   4764 				DrawRectangleLinesEx(rl_rect(r), node->border_thickness, colour_from_normalized(colour));
   4765 			}
   4766 
   4767 			if (node->flags & UINodeFlag_CustomDraw) {
   4768 				node->custom_draw_function(node, r);
   4769 			} else {
   4770 				if (node->flags & UINodeFlag_DrawText) {
   4771 					Font font = ui_font_for_node(node);
   4772 
   4773 					TextSpec text_spec = {
   4774 						.font           = &font,
   4775 						.flags          = TF_LIMITED,
   4776 						.colour         = node->text_colour,
   4777 						.outline_colour = node->text_outline_colour,
   4778 						.outline_thick  = node->text_outline_thickness,
   4779 						.limits.size    = r.size,
   4780 					};
   4781 					if (node->text_outline_thickness > 0)
   4782 						text_spec.flags |= TF_OUTLINED;
   4783 
   4784 					v2 pos = ui_node_text_position(node);
   4785 
   4786 					UITextInputState *tis = &ui_context->text_input_state;
   4787 					b32  input  = ui_node_key_equal(node->key, tis->node_key);
   4788 					// TODO(rnp): cleanup: visible part
   4789 					str8 string = ui_draw_part_from_key_string(node->string);
   4790 					if (!input && node->flags & UINodeFlag_DrawHotEffects && (node->flags & UINodeFlag_DrawBackground) == 0)
   4791 						text_spec.colour = v4_lerp(text_spec.colour, HOVERED_COLOUR, node->hot_t);
   4792 
   4793 					if (node->flags & UINodeFlag_IconText)
   4794 						draw_text_tight(*text_spec.font, string, pos, colour_from_normalized(text_spec.colour));
   4795 					else
   4796 						draw_text(string, pos, &text_spec);
   4797 
   4798 					if (input) {
   4799 						iv2 range = ui_text_input_cursor_range();
   4800 						str8 parts[2];
   4801 						parts[0] = (str8){.data = string.data,           .length = range.x};
   4802 						parts[1] = (str8){.data = string.data + range.x, .length = range.y - range.x};
   4803 
   4804 						Rect cursor = {.pos = pos};
   4805 						cursor.pos.x += measure_text(font, parts[0]).x;
   4806 
   4807 						v4 cursor_colour = FOCUSED_COLOUR;
   4808 						if (parts[1].length > 0) {
   4809 							cursor_colour = SELECTION_COLOUR;
   4810 							cursor.size   = measure_text(font, parts[1]);
   4811 
   4812 							if (range.x == 0) {
   4813 								cursor.pos.x  -= 2.f;
   4814 								cursor.size.x += 2.f;
   4815 							}
   4816 
   4817 							if (range.y == tis->count)
   4818 								cursor.size.x += 2.f;
   4819 						} else {
   4820 							cursor_colour.a = ease_in_out_cubic(ui->text_input_state.blinker.t);
   4821 							cursor.size.x   = string.length - range.y > 0 ? 4.0f : 0.55f * (f32)font.baseSize;
   4822 							cursor.size.y   = font.baseSize;
   4823 						}
   4824 
   4825 						if (cursor.size.x > 0)
   4826 							DrawRectanglePro(rl_rect(cursor), (Vector2){0}, 0, colour_from_normalized(cursor_colour));
   4827 					}
   4828 				}
   4829 			}
   4830 
   4831 			// NOTE(rnp): push children
   4832 			for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling) {
   4833 				Rect cr         = ui_node_rect(child);
   4834 				if ((cr.size.x > 0 && cr.size.y > 0) || ui_node_key_equal(child->key, ui->text_input_state.node_key))
   4835 					da_push(ui_build_arena(), &stack)->node = child;
   4836 			}
   4837 		} else {
   4838 			// NOTE(rnp): pop
   4839 			stack.count--;
   4840 
   4841 			if (node->flags & UINodeFlag_ViewScroll) {
   4842 				rlPopMatrix();
   4843 			}
   4844 
   4845 			if (node->flags & UINodeFlag_Clip)
   4846 				EndScissorMode();
   4847 		}
   4848 	}
   4849 
   4850 	// TODO(rnp): can we make the mouse latency not shit?
   4851 	//if (ui->current_mouse.x > 0) DrawCircle(ui->current_mouse.x, ui->current_mouse.y, 6, GREEN);
   4852 }
   4853 
   4854 function void
   4855 beamformer_ui_panel_unlink(BeamformerUIPanel *node)
   4856 {
   4857 	BeamformerUIPanel *parent = node->parent;
   4858 	if (parent->kind == BeamformerPanelKind_TabGroup && parent->u.tab_focus == node)
   4859 		parent->u.tab_focus = node->previous_sibling ? node->previous_sibling : node->next_sibling;
   4860 	DLLRemove(0, parent->first_child, parent->last_child, node, next_sibling, previous_sibling);
   4861 	parent->child_count--;
   4862 }
   4863 
   4864 function void
   4865 ui_kill_panel(BeamformerUIPanel *node)
   4866 {
   4867 	BeamformerUI      *ui     = ui_context;
   4868 	BeamformerUIPanel *parent = node->parent;
   4869 
   4870 	if (node->kind == BeamformerPanelKind_FrameViewLive ||
   4871 	    node->kind == BeamformerPanelKind_FrameViewCopy ||
   4872 	    node->kind == BeamformerPanelKind_FrameViewXPlane)
   4873 	{
   4874 		BeamformerFrameView *bv = node->u.frame_view;
   4875 		beamformer_ui_frame_view_release_subresources(bv, bv->kind);
   4876 		DLLRemove(0, ui->view_first, ui->view_last, bv, next, prev);
   4877 		SLLStackPush(ui->view_freelist, bv, next);
   4878 	}
   4879 
   4880 	beamformer_ui_panel_unlink(node);
   4881 
   4882 	if (node->kind == BeamformerPanelKind_TabGroup) {
   4883 		assert(parent->kind == BeamformerPanelKind_Split);
   4884 
   4885 		BeamformerUIPanel *old_child = parent->first_child;
   4886 		parent->kind        = old_child->kind;
   4887 		parent->first_child = old_child->first_child;
   4888 		parent->last_child  = old_child->last_child;
   4889 		parent->child_count = old_child->child_count;
   4890 		memory_copy(&parent->u, &old_child->u, sizeof(parent->u));
   4891 
   4892 		for (BeamformerUIPanel *child = parent->first_child; child; child = child->next_sibling)
   4893 			child->parent = parent;
   4894 
   4895 		SLLStackPush(ui->tree_node_freelist, old_child, next_sibling);
   4896 	}
   4897 
   4898 	SLLStackPush(ui->tree_node_freelist, node, next_sibling);
   4899 }
   4900 
   4901 function BeamformerUIPanel *
   4902 beamformer_ui_push_panel_node(BeamformerUIPanel *parent)
   4903 {
   4904 	BeamformerUI *ui = ui_context;
   4905 	BeamformerUIPanel *result = ui->tree_node_freelist;
   4906 	if (result) SLLStackPop(ui->tree_node_freelist, next_sibling);
   4907 	else result = push_struct_no_zero(&ui->arena, BeamformerUIPanel);
   4908 	zero_struct(result);
   4909 
   4910 	result->parent = parent;
   4911 	if (parent) {
   4912 		DLLInsertLast(0, parent->first_child, parent->last_child, result, next_sibling, previous_sibling);
   4913 		parent->child_count++;
   4914 	}
   4915 
   4916 	return result;
   4917 }
   4918 
   4919 function BeamformerUIPanel *
   4920 beamformer_ui_push_panel(BeamformerUIPanel *parent, BeamformerPanelKind kind)
   4921 {
   4922 	BeamformerUIPanel *result = beamformer_ui_push_panel_node(parent);
   4923 	result->kind = kind;
   4924 	if (parent && parent->kind == BeamformerPanelKind_TabGroup)
   4925 		parent->u.tab_focus = result;
   4926 
   4927 	if (kind == BeamformerPanelKind_FrameViewLive ||
   4928 	    kind == BeamformerPanelKind_FrameViewCopy ||
   4929 	    kind == BeamformerPanelKind_FrameViewXPlane)
   4930 	{
   4931 		BeamformerFrameViewKind view_kind = BeamformerFrameViewKind_Latest;
   4932 		if (kind == BeamformerPanelKind_FrameViewCopy)
   4933 			view_kind = BeamformerFrameViewKind_Copy;
   4934 		if (kind == BeamformerPanelKind_FrameViewXPlane)
   4935 			view_kind = BeamformerFrameViewKind_3DXPlane;
   4936 		result->u.frame_view = beamformer_ui_frame_view_new(view_kind);
   4937 	}
   4938 
   4939 	return result;
   4940 }
   4941 
   4942 /* NOTE(rnp): this only exists to make asan less annoying. do not waste
   4943  * people's time by freeing, closing, etc... */
   4944 DEBUG_EXPORT BEAMFORMER_DEBUG_UI_DEINIT_FN(beamformer_debug_ui_deinit)
   4945 {
   4946 #if ASAN_ACTIVE
   4947 	BeamformerUI *ui = ctx->ui;
   4948 	UnloadFont(ui->font);
   4949 	UnloadFont(ui->small_font);
   4950 	CloseWindow();
   4951 #endif
   4952 }
   4953 
   4954 function void
   4955 ui_init(BeamformerCtx *ctx, Arena store)
   4956 {
   4957 	BeamformerUI *ui = ui_context = ctx->ui;
   4958 	if (!ui) {
   4959 		ui = ui_context = ctx->ui = push_struct(&store, typeof(*ui));
   4960 		ui->arena = store;
   4961 
   4962 		for EachElement(ui->build_arenas, it) {
   4963 			ui->build_arenas[it] = sub_arena(&ui->arena, KB(128), KB(4));
   4964 			ui->build_arena_savepoints[it] = begin_temp_arena(ui->build_arenas + it);
   4965 		}
   4966 		ui->node_freelist = &ui_node_nil;
   4967 
   4968 		/* TODO(rnp): better font, this one is jank at small sizes */
   4969 		ui->font       = LoadFontFromMemory(".ttf", beamformer_base_font, sizeof(beamformer_base_font), 28, 0, 0);
   4970 		ui->small_font = LoadFontFromMemory(".ttf", beamformer_base_font, sizeof(beamformer_base_font), 20, 0, 0);
   4971 
   4972 		// NOTE(rnp): push default UI layout
   4973 		// TODO(rnp): load last layout from file and only load default if not present
   4974 		{
   4975 			BeamformerUIPanel *node = ui->tree = beamformer_ui_push_panel(0, BeamformerPanelKind_Split);
   4976 			node->u.split.fraction = 0.35f;
   4977 			node->u.split.axis     = Axis2_X;
   4978 
   4979 			DeferLoop(node = beamformer_ui_push_panel(node, BeamformerPanelKind_Split), node = node->parent)
   4980 			{
   4981 				node->u.split.fraction = 0.65f;
   4982 				node->u.split.axis     = Axis2_Y;
   4983 
   4984 				BeamformerUIPanel *left  = beamformer_ui_push_panel(node, BeamformerPanelKind_TabGroup);
   4985 				BeamformerUIPanel *right = beamformer_ui_push_panel(node, BeamformerPanelKind_TabGroup);
   4986 				beamformer_ui_push_panel(left,  BeamformerPanelKind_ParameterListing);
   4987 				beamformer_ui_push_panel(right, BeamformerPanelKind_ComputeBarGraph);
   4988 				beamformer_ui_push_panel(right, BeamformerPanelKind_ComputeStats);
   4989 			}
   4990 
   4991 			DeferLoop(node = beamformer_ui_push_panel(node, BeamformerPanelKind_TabGroup), node = node->parent)
   4992 			{
   4993 				beamformer_ui_push_panel(node, BeamformerPanelKind_FrameViewLive);
   4994 			}
   4995 		}
   4996 
   4997 		u32 samples = vk_gpu_info()->max_msaa_samples;
   4998 		vk_image_allocate(&ui->render_3d_image,       FRAME_VIEW_RENDER_TARGET_SIZE, 1, samples, VulkanImageUsage_Colour,       0, 0, s8("Render Target Colour"));
   4999 		vk_image_allocate(&ui->render_3d_depth_image, FRAME_VIEW_RENDER_TARGET_SIZE, 1, samples, VulkanImageUsage_DepthStencil, 0, 0, s8("Render Target Depth"));
   5000 
   5001 		glGenSemaphoresEXT(countof(ui->render_semaphores_gl), ui->render_semaphores_gl);
   5002 		for EachElement(ui->render_semaphores, it)
   5003 			ui->render_semaphores[it] = vk_create_semaphore(ui->render_semaphores_export + it);
   5004 
   5005 		if (OS_WINDOWS) {
   5006 			glImportSemaphoreWin32HandleEXT(ui->render_semaphores_gl[0], GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, (void *)ui->render_semaphores_export[0].value[0]);
   5007 			glImportSemaphoreWin32HandleEXT(ui->render_semaphores_gl[1], GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, (void *)ui->render_semaphores_export[1].value[0]);
   5008 		} else {
   5009 			glImportSemaphoreFdEXT(ui->render_semaphores_gl[0], GL_HANDLE_TYPE_OPAQUE_FD_EXT, ui->render_semaphores_export[0].value[0]);
   5010 			glImportSemaphoreFdEXT(ui->render_semaphores_gl[1], GL_HANDLE_TYPE_OPAQUE_FD_EXT, ui->render_semaphores_export[1].value[0]);
   5011 			ui->render_semaphores_export[0].value[0] = OSInvalidHandleValue;
   5012 			ui->render_semaphores_export[1].value[0] = OSInvalidHandleValue;
   5013 		}
   5014 
   5015 		if (!BakeShaders)
   5016 		{
   5017 			for EachElement(beamformer_reloadable_render_shader_info_indices, it) {
   5018 				i32 index = beamformer_reloadable_render_shader_info_indices[it];
   5019 				for (u32 i = 0; i < 2; i++) {
   5020 					BeamformerFileReloadContext *frc = push_struct(&ui->arena, typeof(*frc));
   5021 					frc->kind                   = BeamformerFileReloadKind_RenderShader;
   5022 					frc->shader_reload.shader   = beamformer_reloadable_shader_kinds[index];
   5023 					frc->shader_reload.pipeline = ui->pipelines + it;
   5024 
   5025 					Arena scratch = ui->arena;
   5026 					s8 file = push_s8_from_parts(&scratch, os_path_separator(), s8("shaders"),
   5027 					                             beamformer_reloadable_shader_files[index][i]);
   5028 
   5029 					os_add_file_watch((char *)file.data, file.len, frc);
   5030 				}
   5031 			}
   5032 		}
   5033 
   5034 		f32 unit_cube_vertices[] = {
   5035 			 0.5f,  0.5f, -0.5f, 0.0f,
   5036 			 0.5f,  0.5f, -0.5f, 0.0f,
   5037 			 0.5f,  0.5f, -0.5f, 0.0f,
   5038 			 0.5f, -0.5f, -0.5f, 0.0f,
   5039 			 0.5f, -0.5f, -0.5f, 0.0f,
   5040 			 0.5f, -0.5f, -0.5f, 0.0f,
   5041 			 0.5f,  0.5f,  0.5f, 0.0f,
   5042 			 0.5f,  0.5f,  0.5f, 0.0f,
   5043 			 0.5f,  0.5f,  0.5f, 0.0f,
   5044 			 0.5f, -0.5f,  0.5f, 0.0f,
   5045 			 0.5f, -0.5f,  0.5f, 0.0f,
   5046 			 0.5f, -0.5f,  0.5f, 0.0f,
   5047 			-0.5f,  0.5f, -0.5f, 0.0f,
   5048 			-0.5f,  0.5f, -0.5f, 0.0f,
   5049 			-0.5f,  0.5f, -0.5f, 0.0f,
   5050 			-0.5f, -0.5f, -0.5f, 0.0f,
   5051 			-0.5f, -0.5f, -0.5f, 0.0f,
   5052 			-0.5f, -0.5f, -0.5f, 0.0f,
   5053 			-0.5f,  0.5f,  0.5f, 0.0f,
   5054 			-0.5f,  0.5f,  0.5f, 0.0f,
   5055 			-0.5f,  0.5f,  0.5f, 0.0f,
   5056 			-0.5f, -0.5f,  0.5f, 0.0f,
   5057 			-0.5f, -0.5f,  0.5f, 0.0f,
   5058 			-0.5f, -0.5f,  0.5f, 0.0f,
   5059 		};
   5060 		f32 unit_cube_normals[] = {
   5061 			 0.0f,  0.0f, -1.0f, 0.0f,
   5062 			 0.0f,  1.0f,  0.0f, 0.0f,
   5063 			 1.0f,  0.0f,  0.0f, 0.0f,
   5064 			 0.0f,  0.0f, -1.0f, 0.0f,
   5065 			 0.0f, -1.0f,  0.0f, 0.0f,
   5066 			 1.0f,  0.0f,  0.0f, 0.0f,
   5067 			 0.0f,  0.0f,  1.0f, 0.0f,
   5068 			 0.0f,  1.0f,  0.0f, 0.0f,
   5069 			 1.0f,  0.0f,  0.0f, 0.0f,
   5070 			 0.0f,  0.0f,  1.0f, 0.0f,
   5071 			 0.0f, -1.0f,  0.0f, 0.0f,
   5072 			 1.0f,  0.0f,  0.0f, 0.0f,
   5073 			 0.0f,  0.0f, -1.0f, 0.0f,
   5074 			 0.0f,  1.0f,  0.0f, 0.0f,
   5075 			-1.0f,  0.0f,  0.0f, 0.0f,
   5076 			 0.0f,  0.0f, -1.0f, 0.0f,
   5077 			 0.0f, -1.0f,  0.0f, 0.0f,
   5078 			-1.0f,  0.0f,  0.0f, 0.0f,
   5079 			 0.0f,  0.0f,  1.0f, 0.0f,
   5080 			 0.0f,  1.0f,  0.0f, 0.0f,
   5081 			-1.0f,  0.0f,  0.0f, 0.0f,
   5082 			 0.0f,  0.0f,  1.0f, 0.0f,
   5083 			 0.0f, -1.0f,  0.0f, 0.0f,
   5084 			-1.0f,  0.0f,  0.0f, 0.0f,
   5085 		};
   5086 		u16 unit_cube_indices[] = {
   5087 			1,  13, 19,
   5088 			1,  19, 7,
   5089 			9,  6,  18,
   5090 			9,  18, 21,
   5091 			23, 20, 14,
   5092 			23, 14, 17,
   5093 			16, 4,  10,
   5094 			16, 10, 22,
   5095 			5,  2,  8,
   5096 			5,  8,  11,
   5097 			15, 12, 0,
   5098 			15, 0,  3
   5099 		};
   5100 
   5101 		static_assert(countof(unit_cube_normals) == countof(unit_cube_vertices), "");
   5102 
   5103 		RenderModel *rm = &ui->unit_cube_model;
   5104 		rm->vertex_count   = countof(unit_cube_vertices) / 4;
   5105 		rm->normals_offset = round_up_to(sizeof(unit_cube_vertices), 16);
   5106 
   5107 		u64 model_size = 2 * round_up_to(sizeof(unit_cube_vertices), 16);
   5108 		vk_render_model_allocate(&rm->model, unit_cube_indices, countof(unit_cube_indices), model_size, s8("unit_cube_model"));
   5109 		vk_render_model_range_upload(&rm->model, unit_cube_vertices, 0,                  sizeof(unit_cube_vertices), 0);
   5110 		vk_render_model_range_upload(&rm->model, unit_cube_normals,  rm->normals_offset, sizeof(unit_cube_normals),  0);
   5111 	}
   5112 
   5113 	for EachElement(beamformer_reloadable_render_shader_info_indices, it) {
   5114 		i32 index = beamformer_reloadable_render_shader_info_indices[it];
   5115 		BeamformerShaderKind shader = beamformer_reloadable_shader_kinds[index];
   5116 		beamformer_reload_render_pipeline(ui->pipelines + it, shader, ui->arena);
   5117 	}
   5118 }
   5119 
   5120 function void
   5121 beamformer_ui_frame(void)
   5122 {
   5123 	BeamformerUI *ui = ui_context = beamformer_context->ui;
   5124 
   5125 	{
   5126 		BeamformerFrame *frame = beamformer_frame_from_index(beamformer_registers()->frame);
   5127 		memory_copy(ui->latest_plane + frame->view_plane_tag, frame, sizeof(*frame));
   5128 	}
   5129 
   5130 	BeamformerInput *input = beamformer_input;
   5131 	for EachIndex(input->event_count, it) {
   5132 		if (input->event_queue[it].kind == BeamformerInputEventKind_WindowResize) {
   5133 			// TODO(rnp): match window against window list
   5134 			beamformer_context->window_size.w = input->event_queue[it].window_resize.width;
   5135 			beamformer_context->window_size.h = input->event_queue[it].window_resize.height;
   5136 		}
   5137 	}
   5138 
   5139 	asan_poison_region(ui->arena.beg, ui->arena.end - ui->arena.beg);
   5140 
   5141 	u32 selected_block = ui->selected_parameter_block % BeamformerMaxParameterBlocks;
   5142 	u32 selected_mask  = 1 << selected_block;
   5143 	if (beamformer_context->ui_dirty_parameter_blocks & selected_mask) {
   5144 		BeamformerParameterBlock *pb = beamformer_parameter_block_lock(beamformer_context->shared_memory, selected_block, 0);
   5145 		if (pb) {
   5146 			ui->flush_parameters = 0;
   5147 
   5148 			m4 das_transform;
   5149 			memory_copy(&ui->parameters, &pb->parameters_ui, sizeof(ui->parameters));
   5150 			memory_copy(das_transform.E, pb->parameters.das_voxel_transform.E, sizeof(das_transform));
   5151 
   5152 			atomic_and_u32(&beamformer_context->ui_dirty_parameter_blocks, ~selected_mask);
   5153 			beamformer_parameter_block_unlock(beamformer_context->shared_memory, selected_block);
   5154 
   5155 			BeamformerComputePlan *cp = beamformer_context->compute_context.compute_plans[selected_block];
   5156 			m4 identity = m4_identity();
   5157 			b32 recompute = !m4_equal(identity, cp->ui_voxel_transform);
   5158 			memory_copy(cp->ui_voxel_transform.E, identity.E, sizeof(identity));
   5159 
   5160 			if (recompute) {
   5161 				mark_parameter_block_region_dirty(beamformer_context->shared_memory, selected_block,
   5162 				                                  BeamformerParameterBlockRegion_Parameters);
   5163 				beamformer_queue_compute(beamformer_context,
   5164 				                         beamformer_frame_from_index(beamformer_registers()->frame),
   5165 				                         selected_block);
   5166 			}
   5167 
   5168 			ui->off_axis_position = plane_offset_from_transform(das_transform);
   5169 			ui->beamform_plane    = 0;
   5170 		}
   5171 	}
   5172 
   5173 	/* NOTE: process interactions first because the user interacted with
   5174 	 * the ui that was presented last frame */
   5175 	Rect window_rect = {.size = {{(f32)beamformer_context->window_size.w, (f32)beamformer_context->window_size.h}}};
   5176 
   5177 	ui->last_mouse      = ui->current_mouse;
   5178 	ui->current_mouse.x = input->mouse_x;
   5179 	ui->current_mouse.y = input->mouse_y;
   5180 	for EachElement(ui->input_consumed, it)
   5181 		ui->input_consumed[it] = 0;
   5182 
   5183 	if (ui->flush_parameters && beamformer_frame_valid(beamformer_registers()->frame)) {
   5184 		BeamformerParameterBlock *pb = beamformer_parameter_block_lock(beamformer_context->shared_memory, selected_block, 0);
   5185 		if (pb) {
   5186 			ui->flush_parameters = 0;
   5187 			memory_copy(&pb->parameters_ui, &ui->parameters, sizeof(ui->parameters));
   5188 			mark_parameter_block_region_dirty(beamformer_context->shared_memory, selected_block,
   5189 			                                  BeamformerParameterBlockRegion_Parameters);
   5190 			beamformer_parameter_block_unlock(beamformer_context->shared_memory, selected_block);
   5191 			beamformer_queue_compute(beamformer_context,
   5192 			                         beamformer_frame_from_index(beamformer_registers()->frame),
   5193 			                         selected_block);
   5194 		}
   5195 	}
   5196 
   5197 	/* NOTE(rnp): can't render to a different framebuffer in the middle of BeginDrawing()... */
   5198 	update_frame_views(ui, window_rect);
   5199 
   5200 	////////////////////////////
   5201 	// NOTE(rnp): Text Input
   5202 	{
   5203 		UITextInputState *tis = &ui->text_input_state;
   5204 		// NOTE(rnp): transition to new node
   5205 		tis->last_node_key = ui_node_key_zero();
   5206 		tis->last_count    = 0;
   5207 		if (tis->changed) {
   5208 			tis->changed = 0;
   5209 			ui_text_input_end();
   5210 			if (!ui_node_key_nil(tis->next_node_key)) {
   5211 				tis->node_key      = tis->next_node_key;
   5212 				tis->next_node_key = ui_node_key_zero();
   5213 				if (point_in_rect(ui->current_mouse, ui_text_input_rect()))
   5214 					tis->cursor = tis->mark = ui_text_input_index_from_point(ui->last_mouse.x);
   5215 				tis->blinker.t = 1.0f;
   5216 			}
   5217 		}
   5218 
   5219 		if (!ui_node_key_nil(tis->node_key)) {
   5220 			beamformer_ui_blinker_update(&tis->blinker, BLINK_SPEED);
   5221 
   5222 			UISignal signal = ui_signal_from_node(ui_node_from_key(tis->node_key));
   5223 
   5224 			if (signal.flags & UISignalFlag_LeftPressed) {
   5225 				if (point_in_rect(ui->current_mouse, ui_text_input_rect()))
   5226 					tis->cursor = tis->mark = ui_text_input_index_from_point(ui->last_mouse.x);
   5227 				tis->blinker.t = 1.0f;
   5228 			}
   5229 
   5230 			if (signal.flags & UISignalFlag_LeftDragging)
   5231 				tis->mark = ui_text_input_index_from_point(ui->last_mouse.x);
   5232 
   5233 			if (signal.flags & UISignalFlag_DoubleClicked) {
   5234 				// TODO(rnp): select word
   5235 			}
   5236 
   5237 			if (signal.flags & UISignalFlag_TripleClicked) {
   5238 				tis->cursor = 0;
   5239 				tis->mark   = tis->count;
   5240 			}
   5241 
   5242 			if (ui_text_input_update(input))
   5243 				ui_text_input_end();
   5244 		}
   5245 	}
   5246 
   5247 	if (ui->context_menu_state_changed) {
   5248 		ui->context_menu_state_changed = 0;
   5249 		ui->context_menu_anchor_key    = ui->context_menu_next_anchor_key;
   5250 		ui->context_menu_panel         = ui->context_menu_next_panel;
   5251 	}
   5252 
   5253 	{
   5254 		////////////////////////////
   5255 		// NOTE(rnp): Build Pass
   5256 		end_temp_arena(ui->build_arena_savepoints[ui->current_frame_index % countof(ui->build_arenas)]);
   5257 		// NOTE(rnp): reset last frame's build stacks
   5258 		{
   5259 			#define X(type, name, ...) \
   5260 				ui->name##_node_stack.top   = &ui_##name##_node_nil; \
   5261 				ui->name##_node_stack.free  = 0; \
   5262 				ui->name##_node_stack.count = 0;
   5263 			UI_STACK_LIST
   5264 			#undef X
   5265 
   5266 			UIPrefWidth(ui_px(window_rect.size.x, 1.f))
   5267 			UIPrefHeight(ui_px(window_rect.size.y, 1.f))
   5268 			UIChildLayoutAxis(Axis2_Y)
   5269 			ui->root_node = ui_node_from_string(0, str8("UI Root Node"));
   5270 			ui_push_semantic_width(ui_pct(1.f, 0.5f));
   5271 			ui_push_semantic_height(ui_pct(1.f, 0.5f));
   5272 		}
   5273 
   5274 		ui->drag_root               = 0;
   5275 		ui->drop_target_node        = 0;
   5276 		ui->drag_overlay_root       = 0;
   5277 		ui->drag_overlay_edges_root = 0;
   5278 		ui->drag_overlay_tab_root   = 0;
   5279 
   5280 		beamformer_registers()->split_left_tree  = 0;
   5281 		beamformer_registers()->split_right_tree = 0;
   5282 		beamformer_registers()->drop_child_index = 0;
   5283 
   5284 		// NOTE(rnp): check for active nodes
   5285 		{
   5286 			b32 active = 0;
   5287 			for EachEnumValue(UIMouseButtonKind, k)
   5288 				active |= !ui_node_key_equal(ui->active_node_key[k], ui_node_key_zero());
   5289 			// NOTE(rnp): clear hot node if there are no active nodes
   5290 			if (!active) ui->hot_node_key = ui_node_key_zero();
   5291 		}
   5292 
   5293 		// NOTE(rnp): context menu
   5294 		if (!ui_node_key_nil(ui->context_menu_anchor_key)) {
   5295 			// TODO(rnp): context_menu_open_t
   5296 			UIPrefWidth(ui_children_sum(1.f))
   5297 			UIPrefHeight(ui_children_sum(1.f))
   5298 			UIChildLayoutAxis(Axis2_Y)
   5299 			UIBGColour((v4){.a = 0.8f})
   5300 			{
   5301 				// TODO(rnp): this should be tied to the window state
   5302 				ui->context_menu_root = ui_node_from_string(UINodeFlag_DrawBackground, str8("context_menu_root"));
   5303 			}
   5304 
   5305 			UIParent(ui->context_menu_root) UIAxisSize(Axis2_X, ui_px(0.f, 0.5f)) ui_padh(0.8f * UI_NODE_PAD);
   5306 		}
   5307 
   5308 		// NOTE(rnp): drag panel
   5309 		if (ui->drag_panel) {
   5310 			ui_build_drag_overlay(window_rect);
   5311 
   5312 			UIPrefWidth(ui_px(640.f, 1.f))
   5313 			UIPrefHeight(ui_px(480.f, 1.f))
   5314 			UIChildLayoutAxis(Axis2_Y)
   5315 			UIBGColour((v4){.a = 0.8f})
   5316 			{
   5317 				ui->drag_root = ui_node_from_string(UINodeFlag_DrawBackground, str8("drag_panel_root"));
   5318 			}
   5319 
   5320 			UIParent(ui->drag_root)
   5321 			{
   5322 				UIChildLayoutAxis(Axis2_X)
   5323 				UIPrefHeight(ui_children_sum(1.f))
   5324 				UIPrefWidth(ui_children_sum(1.f))
   5325 				UIParent(ui_spacer(0))
   5326 				{
   5327 					ui_padw(UI_NODE_PAD);
   5328 					ui_panel_label(ui->drag_panel);
   5329 				}
   5330 			}
   5331 
   5332 			ui_build_regions(ui->drag_root, ui->drag_panel);
   5333 		}
   5334 
   5335 		ui_build_regions(ui->root_node, ui->tree);
   5336 
   5337 		////////////////////////////
   5338 		// NOTE(rnp): Prune Dead UI Nodes
   5339 		for EachElement(ui->node_hash_table, it) {
   5340 			UINodeHashBucket *hb = ui->node_hash_table + it;
   5341 			UINode *next = hb->first;
   5342 			for (UINode *b = next; !ui_node_is_nil(b); b = next) {
   5343 				next = b == b->hash_next ? 0 : b->hash_next;
   5344 				if (b->last_frame_active_index != ui->current_frame_index) {
   5345 					for EachEnumValue(UIMouseButtonKind, k)
   5346 						if (ui_node_key_equal(ui->active_node_key[k], b->key))
   5347 							ui->active_node_key[k] = ui_node_key_zero();
   5348 
   5349 					DLLRemove(&ui_node_nil, hb->first, hb->last, b, hash_next, hash_prev);
   5350 					SLLStackPush(ui->node_freelist, b, next_sibling);
   5351 				}
   5352 			}
   5353 		}
   5354 
   5355 		for (BeamformerInputEvent *event = ui_event_next(input, 0);
   5356 		     event;
   5357 		     event = ui_event_next(input, event))
   5358 		{
   5359 			if (event->kind == BeamformerInputEventKind_ButtonPress) {
   5360 				if (event->button_id == BeamformerButtonID_Escape)
   5361 					beamformer_context->state = BeamformerState_ShouldClose;
   5362 
   5363 				if (!Between(event->button_id, BeamformerButtonID_ModifierFirst, BeamformerButtonID_ModifierLast)) {
   5364 					ui_context_menu_close();
   5365 					ui->text_input_state.changed       = 1;
   5366 					ui->text_input_state.next_node_key = ui_node_key_zero();
   5367 				}
   5368 			}
   5369 		}
   5370 
   5371 		////////////////////////////
   5372 		// NOTE(rnp): Layout Pass
   5373 		if (ui->drag_root) {
   5374 			ui->drag_root->computed_position[Axis2_X] = ui->last_mouse.x;
   5375 			ui->drag_root->computed_position[Axis2_Y] = ui->last_mouse.y;
   5376 			ui_layout_nodes(ui->drag_root);
   5377 			ui_layout_nodes(ui->drag_overlay_edges_root);
   5378 			if (ui->drag_overlay_tab_root)
   5379 				ui_layout_nodes(ui->drag_overlay_tab_root);
   5380 			ui_layout_nodes(ui->drag_overlay_root);
   5381 		}
   5382 
   5383 		ui_layout_nodes(ui->root_node);
   5384 
   5385 		if (!ui_node_key_nil(ui->context_menu_anchor_key)) {
   5386 			UIParent(ui->context_menu_root) UIAxisSize(Axis2_X, ui_px(0.f, 0.5f)) ui_padh(0.8f * UI_NODE_PAD);
   5387 
   5388 			UINode *anchor   = ui_node_from_key(ui->context_menu_anchor_key);
   5389 			v2      anchor_p = ui_node_final_position(anchor);
   5390 			ui->context_menu_root->computed_position[Axis2_X] = anchor_p.x;
   5391 			ui->context_menu_root->computed_position[Axis2_Y] = anchor_p.y + anchor->computed_size[Axis2_Y];
   5392 
   5393 			ui_layout_nodes(ui->context_menu_root);
   5394 		}
   5395 
   5396 		BeginDrawing();
   5397 			glClearNamedFramebufferfv(0, GL_COLOR, 0, BG_COLOUR.E);
   5398 			glClearNamedFramebufferfv(0, GL_DEPTH, 0, (f32 []){1});
   5399 			ui_draw_nodes(ui->root_node, window_rect);
   5400 
   5401 			if (!ui_node_key_nil(ui->context_menu_anchor_key))
   5402 				ui_draw_nodes(ui->context_menu_root, window_rect);
   5403 
   5404 			if (ui->drag_root) {
   5405 				if (beamformer_registers()->split_left_tree || beamformer_registers()->split_right_tree)
   5406 					ui_draw_nodes(ui_build_drag_hover_node(), window_rect);
   5407 				ui_draw_nodes(ui->drag_overlay_root, window_rect);
   5408 				ui_draw_nodes(ui->drag_overlay_edges_root, window_rect);
   5409 				if (ui->drag_overlay_tab_root)
   5410 					ui_draw_nodes(ui->drag_overlay_tab_root, window_rect);
   5411 				ui_draw_nodes(ui->drag_root, window_rect);
   5412 			}
   5413 
   5414 			// TODO(rnp): hack: until raylib is removed this happens in ui since raylib will cause
   5415 			// glfw to call the input callbacks during EndDrawing()
   5416 			input->event_count = 0;
   5417 		EndDrawing();
   5418 
   5419 		if (ui->drag_end)
   5420 			ui_drag_end();
   5421 
   5422 		ui->current_frame_index++;
   5423 	}
   5424 }