ogl_beamforming

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

ui.c (187026B)


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