ogl_beamforming

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

ui.c (182642B)


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