ogl_beamforming

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

ui.c (188321B)


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