ogl_beamforming

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

Commit: 05efc8efd5bac3faec0b00e33377007c1b8d296b
Parent: 9b2aa15a0e5a21afefe185cb6513a621a2493312
Author: Randy Palamar
Date:   Tue, 14 Jul 2026 19:47:12 -0700

ui: fully immediate mode rewrite

Diffstat:
Mbeamformer.c | 5+++++
Mbeamformer.h | 140++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mbeamformer.meta | 153++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mbeamformer_core.c | 279++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mbeamformer_internal.h | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mbeamformer_parameters.h | 15---------------
Mbuild.c | 1+
Mgenerated/beamformer.meta.c | 156++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mintrinsics.c | 8++++++++
Mmain_linux.c | 20++++++++++++++++++++
Mmain_w32.c | 20++++++++++++++++++++
Mmath.c | 94++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mui.c | 7577+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mutil.c | 82-------------------------------------------------------------------------------
Mutil.h | 10+++++++---
Mutil_os_ui.c | 201+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
16 files changed, 5453 insertions(+), 3459 deletions(-)

diff --git a/beamformer.c b/beamformer.c @@ -175,6 +175,11 @@ beamformer_init(BeamformerInput *input) BeamformerCtx *ctx = push_struct(&memory, BeamformerCtx); + for EachElement(ctx->frame_arenas, it) { + ctx->frame_arenas[it] = sub_arena(&memory, KB(64), KB(4)); + ctx->frame_arena_savepoints[it] = begin_temp_arena(ctx->frame_arenas + it); + } + str8 window_title = str8("VK Beamformer"); ctx->main_window = os_window_create(window_title.data, window_title.length, 1280, 840); ctx->window_size = (iv2){{1280, 840}}; diff --git a/beamformer.h b/beamformer.h @@ -109,6 +109,9 @@ BEAMFORMER_IMPORT OSWindow os_window_create(uint8_t *title, int64_t title_ //BEAMFORMER_IMPORT void os_window_title(OSWindow window, uint8_t *title, int64_t title_length); //BEAMFORMER_IMPORT void os_window_destroy(OSWindow window); +BEAMFORMER_IMPORT uint8_t * os_get_clipboard_text(int64_t *length); +BEAMFORMER_IMPORT void os_set_clipboard_text(uint8_t *data, int64_t length); + // NOTE(rnp): eventually logging will just be done internally BEAMFORMER_IMPORT void os_console_log(uint8_t *data, int64_t length); BEAMFORMER_IMPORT void os_fatal(uint8_t *data, int64_t length); @@ -138,11 +141,135 @@ typedef enum { BeamformerInputEventKind_FileEvent, } BeamformerInputEventKind; -// TODO: not yet used typedef enum { + BeamformerButtonID_Space = ' ', + BeamformerButtonID_Apostrophe = '\'', + BeamformerButtonID_Comma = ',', + BeamformerButtonID_Minus = '-', + BeamformerButtonID_Period = '.', + BeamformerButtonID_Slash = '/', + BeamformerButtonID_0 = '0', + BeamformerButtonID_1 = '1', + BeamformerButtonID_2 = '2', + BeamformerButtonID_3 = '3', + BeamformerButtonID_4 = '4', + BeamformerButtonID_5 = '5', + BeamformerButtonID_6 = '6', + BeamformerButtonID_7 = '7', + BeamformerButtonID_8 = '8', + BeamformerButtonID_9 = '9', + BeamformerButtonID_Semicolon = ';', + BeamformerButtonID_Equal = '=', + BeamformerButtonID_A = 'A', + BeamformerButtonID_B = 'B', + BeamformerButtonID_C = 'C', + BeamformerButtonID_D = 'D', + BeamformerButtonID_E = 'E', + BeamformerButtonID_F = 'F', + BeamformerButtonID_G = 'G', + BeamformerButtonID_H = 'H', + BeamformerButtonID_I = 'I', + BeamformerButtonID_J = 'J', + BeamformerButtonID_K = 'K', + BeamformerButtonID_L = 'L', + BeamformerButtonID_M = 'M', + BeamformerButtonID_N = 'N', + BeamformerButtonID_O = 'O', + BeamformerButtonID_P = 'P', + BeamformerButtonID_Q = 'Q', + BeamformerButtonID_R = 'R', + BeamformerButtonID_S = 'S', + BeamformerButtonID_T = 'T', + BeamformerButtonID_U = 'U', + BeamformerButtonID_V = 'V', + BeamformerButtonID_W = 'W', + BeamformerButtonID_X = 'X', + BeamformerButtonID_Y = 'Y', + BeamformerButtonID_Z = 'Z', + BeamformerButtonID_LeftBracket = '[', + BeamformerButtonID_Backslash = '\\', + BeamformerButtonID_RightBracket = ']', + BeamformerButtonID_Grave = '`', + + BeamformerButtonID_Escape, + BeamformerButtonID_Enter, + BeamformerButtonID_Tab, + BeamformerButtonID_Backspace, + BeamformerButtonID_Insert, + BeamformerButtonID_Delete, + BeamformerButtonID_Right, + BeamformerButtonID_Left, + BeamformerButtonID_Down, + BeamformerButtonID_Up, + BeamformerButtonID_PageUp, + BeamformerButtonID_PageDown, + BeamformerButtonID_Home, + BeamformerButtonID_End, + BeamformerButtonID_CapsLock, + BeamformerButtonID_ScrollLock, + BeamformerButtonID_NumLock, + BeamformerButtonID_PrintScreen, + BeamformerButtonID_Pause, + BeamformerButtonID_F1, + BeamformerButtonID_F2, + BeamformerButtonID_F3, + BeamformerButtonID_F4, + BeamformerButtonID_F5, + BeamformerButtonID_F6, + BeamformerButtonID_F7, + BeamformerButtonID_F8, + BeamformerButtonID_F9, + BeamformerButtonID_F10, + BeamformerButtonID_F11, + BeamformerButtonID_F12, + BeamformerButtonID_F13, + BeamformerButtonID_F14, + BeamformerButtonID_F15, + BeamformerButtonID_F16, + BeamformerButtonID_F17, + BeamformerButtonID_F18, + BeamformerButtonID_F19, + BeamformerButtonID_F20, + BeamformerButtonID_F21, + BeamformerButtonID_F22, + BeamformerButtonID_F23, + BeamformerButtonID_F24, + BeamformerButtonID_F25, + BeamformerButtonID_KP0, + BeamformerButtonID_KP1, + BeamformerButtonID_KP2, + BeamformerButtonID_KP3, + BeamformerButtonID_KP4, + BeamformerButtonID_KP5, + BeamformerButtonID_KP6, + BeamformerButtonID_KP7, + BeamformerButtonID_KP8, + BeamformerButtonID_KP9, + BeamformerButtonID_KPDecimal, + BeamformerButtonID_KPDivide, + BeamformerButtonID_KPMultiply, + BeamformerButtonID_KPSubtract, + BeamformerButtonID_KPAdd, + BeamformerButtonID_KPEnter, + BeamformerButtonID_KPEqual, + + BeamformerButtonID_LeftShift, + BeamformerButtonID_LeftControl, + BeamformerButtonID_LeftAlt, + BeamformerButtonID_LeftSuper, + BeamformerButtonID_RightShift, + BeamformerButtonID_RightControl, + BeamformerButtonID_RightAlt, + BeamformerButtonID_RightSuper, + BeamformerButtonID_ModifierFirst = BeamformerButtonID_LeftShift, + BeamformerButtonID_ModifierLast = BeamformerButtonID_RightSuper, + + BeamformerButtonID_Menu, + BeamformerButtonID_MouseLeft, BeamformerButtonID_MouseRight, BeamformerButtonID_MouseMiddle, + BeamformerButtonID_Count, } BeamformerButtonID; @@ -171,8 +298,14 @@ typedef enum { typedef struct { BeamformerInputEventKind kind; + BeamformerInputModifiers modifiers; union { - BeamformerButtonID button_id; + struct { + BeamformerButtonID button_id; + // NOTE(rnp): if the button is not also an input key codepoint should be 0 + uint32_t codepoint; + }; + struct {float x, y;} scroll; struct { uint32_t width, height; @@ -206,8 +339,7 @@ typedef struct { uint32_t event_count; - BeamformerInputModifiers input_modifiers; - BeamformerInputEvent event_queue[256]; + BeamformerInputEvent event_queue[256]; /* NOTE(rnp): the beamformer is not allowed to dynamically load libraries * itself. Besides Vulkan, which is required, libraries are optional and diff --git a/beamformer.meta b/beamformer.meta @@ -68,6 +68,32 @@ Cubic } +@Enumeration ViewPlaneTag +{ + XZ + YZ + XY + Arbitrary +} + +@Table([name pretty_name fixed_transmits]) AcquisitionKindTable +{ + [FORCES FORCES 1] + [UFORCES UFORCES 0] + [HERCULES HERCULES 1] + [RCA_VLS VLS 0] + [RCA_TPW TPW 0] + [UHERCULES UHERCULES 0] + [RACES RACES 1] + [EPIC_FORCES EPIC-FORCES 1] + [EPIC_UFORCES EPIC-UFORCES 0] + [EPIC_UHERCULES EPIC-UHERCULES 0] + [Flash Flash 0] + [HERO_PA HERO-PA 0] + [ULM ULM 0] +} +@Expand(AcquisitionKindTable) @Enumeration(`$(name)`) AcquisitionKind + @Struct SineParameters { [cycles F32] @@ -142,24 +168,6 @@ @Library @Struct FilterParameters @MATLAB(Filter) @Struct FilterParameters -@Table([name pretty_name fixed_transmits]) AcquisitionKindTable -{ - [FORCES FORCES 1] - [UFORCES UFORCES 0] - [HERCULES HERCULES 1] - [RCA_VLS VLS 0] - [RCA_TPW TPW 0] - [UHERCULES UHERCULES 0] - [RACES RACES 1] - [EPIC_FORCES EPIC-FORCES 1] - [EPIC_UFORCES EPIC-UFORCES 0] - [EPIC_UHERCULES EPIC-UHERCULES 0] - [Flash Flash 0] - [HERO_PA HERO-PA 0] - [ULM ULM 0] -} -@Expand(AcquisitionKindTable) @Enumeration(`$(name)`) AcquisitionKind - @Struct ParametersHead { [das_voxel_transform M4 ] @@ -264,12 +272,16 @@ @Expand(ContrastModeTable) ` str8_comp("$(name)"),` `};` `` + `read_only global str8 beamformer_view_plane_tag_strings[] = {` + @Expand(ViewPlaneTag) ` str8_comp("$(name)"),` + `};` + `` `read_only global u8 beamformer_acquisition_kind_has_fixed_transmits[] = {` @Expand(AcquisitionKindTable) ` $(fixed_transmits),` `};` `` - `read_only global s8 beamformer_acquisition_kind_strings[] = {` - @Expand(AcquisitionKindTable) ` s8_comp("$(pretty_name)"),` + `read_only global str8 beamformer_acquisition_kind_strings[] = {` + @Expand(AcquisitionKindTable) ` str8_comp("$(pretty_name)"),` `};` `` `read_only global s8 beamformer_filter_kind_strings[] = {` @@ -283,10 +295,6 @@ `read_only global s8 beamformer_shader_resource_kind_strings[] = {` @Expand(ShaderResourceKind) ` s8_comp("$(name)"),` `};` - `` - `read_only global s8 game_shader_buffer_slot_strings[] = {` - @Expand(ShaderBufferSlot) ` s8_comp("$(name)"),` - `};` } @ShaderGroup Compute @@ -490,3 +498,100 @@ } } } + +//////////////////// +// NOTE: Core Data + +@Flags PanelFlags +{ + List + NeedsFrame + HasSettings +} + +@Struct PanelInfo +{ + [display STR] + [string STR] + [description STR] + [flags PanelFlags] +} + +@Table([name display string list needs_frame has_settings description]) PanelTable +{ + [Nil `` nil 0 0 0 ``] + [Split `` split 0 0 0 ``] + [TabGroup `` group 0 0 0 ``] + + [ComputeBarGraph `Compute Bar Graph` compute_bar_graph 1 0 0 `Bar graph showing portions of compute occupied by each stage.`] + [ComputeStats `Compute Stats` compute_stats 1 0 0 `Average stats about beamforming computations.`] + [FrameViewLive `Frame View` frame_view_live 1 0 1 `Latest frame with selected tag.`] + [FrameViewCopy `Frame View (Copy)` frame_view_copy 0 1 1 `Copy of an old frame. Useful for comparisons.`] + [FrameViewXPlane `3D X-Plane` frame_view_xplane 1 0 1 `3D Cross Plane View.`] + [LiveImagingControls `Live Controls` live_controls 1 0 0 `Imaging system controls.`] + [ParameterListing `Parameter Listing` parameters_listing 1 0 1 `Editable parameter set used for beamforming.`] +} +@Expand(PanelTable) @Enumeration(`$(name)`) PanelKind +@Emit +{ + `read_only global BeamformerPanelInfo beamformer_panel_infos[] = {` + @Expand(PanelTable) ` {str8_comp("$(display)"), str8_comp("$(string)"), str8_comp("$(description)"), ($(list)*BeamformerPanelFlags_List)|($(needs_frame)*BeamformerPanelFlags_NeedsFrame)|($(has_settings)*BeamformerPanelFlags_HasSettings)},` + `};` +} + +@Table ([type name upper]) RegistersTable +{ + [STR string String ] + [U64 tree_node TreeNode ] + [U64 window Window ] + [U64 frame Frame ] + + [U64 frame_view FrameView ] + + // NOTE(rnp): Panel Movement and Splitting + [U64 split_axis SplitAxis ] + [U64 split_left_tree SplitLeftTree ] + [U64 split_right_tree SplitRightTree] + + [U64 drop_target_tree DropTargetTree] + [U64 drop_child_index DropChildIndex] +} +@Expand(RegistersTable) @Struct([`$(name)` `$(type)`]) Registers +@Expand(RegistersTable) @Enumeration(`$(upper)`) RegisterSlot + +@Emit +{ + `#define beamformer_registers_init_literal \` + @Expand(RegistersTable) ` .$(name)$(|)= beamformer_registers()->$(name), \` +} + +@Struct CommandInfo +{ + [string STR] + [display STR] + [description STR] +} + +@Table([kind string display_string description]) CommandTable +{ + // NOTE(rnp): tab commands + [CloseTab `close_tab` `Close Tab` `Closes currently active tab.`] + [FocusTab `focus_tab` `Focus Tab` `Focus the currently selected tab in its group.`] + [MoveTab `move_tab` `Move Tab` `Moves an exisiting tab to a different group/split.`] + [OpenTab `open_tab` `Open Tab` `Opens a new tab with a specified view.`] + [SplitTree `split_tree` `Split Tree` `Splits the UI into two new splits.`] +} + +@Enumeration CommandKind +{ + Nil + @Expand(CommandTable) `$(kind)` +} + +@Emit +{ + `read_only global BeamformerCommandInfo beamformer_command_infos[] = {` + ` {0},` + @Expand(CommandTable) ` {str8_comp("$(string)"), str8_comp("$(display_string)"), str8_comp("$(description)")},` + `};` +} diff --git a/beamformer_core.c b/beamformer_core.c @@ -1,5 +1,6 @@ /* See LICENSE for license details. */ /* TODO(rnp): + * [ ]: bug? beamforming non XZ tag from non-0 parameter slot doesn't work? * [ ]: backtrace dumping on SIGSEGV * [ ]: bug? HERCULES might be broken, we may need to to chunk on transmits instead of channels * [ ]: refactor: do_compute should build its own "command graph" which tracks @@ -24,8 +25,6 @@ #include "beamformer_internal.h" -global f32 dt_for_frame; - typedef struct BeamformerComputeGraphNode BeamformerComputeGraphNode; struct BeamformerComputeGraphNode { // NOTE(rnp): will be BeamformerShaderKind_Count for root node @@ -65,6 +64,102 @@ read_only global u32 beamformer_compute_array_parameter_offsets[] = { #undef X }; +read_only global BeamformerFrame beamformer_nil_frame; +read_only global BeamformerComputePlan beamformer_nil_compute_plan; + +global BeamformerCtx *beamformer_context; +global BeamformerInput *beamformer_input; +global f32 dt_for_frame; + +#define beamformer_frame_arena() (beamformer_context->frame_arenas + beamformer_context->frame_index % countof(beamformer_context->frame_arenas)) +#define beamformer_registers() (&beamformer_context->registers->v) +#define beamformer_push_registers(...) beamformer_push_registers_(&(BeamformerRegisters){beamformer_registers_init_literal __VA_ARGS__}) +#define BeamformerRegistersScope(...) DeferLoop(beamformer_push_registers(__VA_ARGS__), beamformer_pop_registers()) +#define beamformer_command(name, ...) beamformer_push_command(name, &(BeamformerRegisters){beamformer_registers_init_literal __VA_ARGS__}) + +function BeamformerRegisters * +beamformer_pop_registers(void) +{ + BeamformerRegisters *result = &beamformer_context->registers->v; + SLLStackPop(beamformer_context->registers, next); + if (beamformer_context->registers == 0) + beamformer_context->registers = &beamformer_context->base_registers; + return result; +} + +function BeamformerRegisters * +beamformer_push_registers_(BeamformerRegisters *registers) +{ + BeamformerRegistersNode *node = push_struct(beamformer_frame_arena(), BeamformerRegistersNode); + BeamformerRegisters *result = &node->v; + memory_copy(result, registers, sizeof(node->v)); + SLLStackPush(beamformer_context->registers, node, next); + return result; +} + +function void +beamformer_command_list_push_new(Arena *arena, BeamformerCommandList *commands, str8 name, BeamformerRegisters *registers) +{ + BeamformerCommandNode *node = push_struct(arena, BeamformerCommandNode); + node->command.registers = push_struct_no_zero(arena, BeamformerRegisters); + node->command.name = push_str8(arena, name); + memory_copy(node->command.registers, registers, sizeof(*registers)); + DLLInsertLast(0, commands->first, commands->last, node, next, prev); + commands->count += 1; +} + +function void +beamformer_push_command(str8 name, BeamformerRegisters *registers) +{ + beamformer_command_list_push_new(beamformer_frame_arena(), beamformer_context->command_queues + 0, + name, registers); +} + +function BeamformerCommandKind +beamformer_command_kind_from_string(str8 s) +{ + BeamformerCommandKind result = BeamformerCommandKind_Nil; + for EachElement(beamformer_command_infos, it) { + if (str8_equal(beamformer_command_infos[it].string, s)) { + result = (BeamformerCommandKind)it; + break; + } + } + return result; +} + +function BeamformerPanelKind +beamformer_panel_kind_from_string(str8 s) +{ + BeamformerPanelKind result = BeamformerPanelKind_Nil; + for EachElement(beamformer_panel_infos, it) { + if (str8_equal(beamformer_panel_infos[it].string, s)) { + result = (BeamformerPanelKind)it; + break; + } + } + return result; +} + +function BeamformerFrame * +beamformer_frame_from_index(u64 index) +{ + BeamformerFrame *result = &beamformer_nil_frame; + if (index < countof(beamformer_context->compute_context.backlog.frames)) { + BeamformerFrame *frame = beamformer_context->compute_context.backlog.frames + index; + if (frame->timeline_valid_value != 0) + result = frame; + } + return result; +} + +function b32 +beamformer_frame_valid(u64 index) +{ + b32 result = beamformer_frame_from_index(index) != &beamformer_nil_frame; + return result; +} + function void beamformer_compute_plan_release(BeamformerComputeContext *cc, u32 block) { @@ -641,9 +736,11 @@ plan_compute_pipeline(BeamformerComputePlan *cp, BeamformerParameterBlock *pb, A cp->voxel_transform = m4_mul(cp->ui_voxel_transform, pb->parameters.das_voxel_transform); cp->xdc_element_pitch = pb->parameters.xdc_element_pitch; + memory_copy(cp->das_voxel_transform.E, cp->voxel_transform.E, sizeof(cp->voxel_transform)); + u32 id = pb->parameters.acquisition_kind; if (id == BeamformerAcquisitionKind_UFORCES || id == BeamformerAcquisitionKind_FORCES) - cp->voxel_transform = m4_mul(cp->xdc_transform, cp->voxel_transform); + cp->das_voxel_transform = m4_mul(cp->xdc_transform, cp->das_voxel_transform); db->sparse = id == BeamformerAcquisitionKind_UFORCES || id == BeamformerAcquisitionKind_UHERCULES; db->single_focus = pb->parameters.single_focus; @@ -1124,8 +1221,8 @@ do_compute_shader(BeamformerCtx *ctx, VulkanHandle cmd, BeamformerComputePlan *c .channel_offset = channel_offset, .array_parameters = cp->array_parameters.gpu_pointer + offsetof(BeamformerComputeArrayParameters, FocalVectors), }; - mem_copy(pc.voxel_transform.E, cp->voxel_transform.E, sizeof(pc.voxel_transform)); - mem_copy(pc.xdc_transform.E, cp->xdc_transform.E, sizeof(pc.xdc_transform)); + memory_copy(pc.voxel_transform.E, cp->das_voxel_transform.E, sizeof(pc.voxel_transform)); + memory_copy(pc.xdc_transform.E, cp->xdc_transform.E, sizeof(pc.xdc_transform)); b32 coherent = cp->shader_descriptors[shader_slot].bake.DAS.coherency_weighting; @@ -1395,6 +1492,7 @@ complete_queue(BeamformerCtx *ctx, BeamformWorkQueue *q, Arena *arena) frame->acquisition_kind = cp->acquisition_kind; frame->contrast_mode = cp->contrast_mode; frame->compound_count = cp->acquisition_count; + frame->parameter_block = work->compute_context.parameter_block; frame->view_plane_tag = work->compute_context.view_plane; mem_copy(frame->voxel_transform.E, cp->voxel_transform.E, sizeof(cp->voxel_transform)); @@ -1706,28 +1804,191 @@ beamformer_process_input_events(BeamformerCtx *ctx, BeamformerInput *input, } } +function void +beamformer_panel_group_insert_at(BeamformerUIPanel *group, BeamformerUIPanel *tab, u64 new_child_index) +{ + if (tab->parent) beamformer_ui_panel_unlink(tab); + new_child_index = Min(new_child_index, group->child_count); + + tab->parent = group; + group->child_count++; + if (group->kind == BeamformerPanelKind_TabGroup) group->u.tab_focus = tab; + + BeamformerUIPanel *previous_sibling = new_child_index == 0 ? 0 : group->first_child; + for (u64 child_index = 1; child_index < new_child_index; child_index++) + previous_sibling = previous_sibling->next_sibling; + + if (previous_sibling) { + tab->previous_sibling = previous_sibling; + tab->next_sibling = previous_sibling->next_sibling; + if (tab->next_sibling) tab->next_sibling->previous_sibling = tab; + previous_sibling->next_sibling = tab; + if (previous_sibling == group->last_child) group->last_child = tab; + } else { + DLLInsertFirst(0, group->first_child, group->last_child, tab, next_sibling, previous_sibling); + } +} + BEAMFORMER_EXPORT void beamformer_frame_step(BeamformerInput *input) { - BeamformerCtx *ctx = BeamformerContextMemory(input->memory); + BeamformerCtx *ctx = beamformer_context = BeamformerContextMemory(input->memory); + beamformer_input = input; u64 current_time = os_timer_count(); dt_for_frame = (f64)(current_time - ctx->frame_timestamp) / os_system_info()->timer_frequency; ctx->frame_timestamp = current_time; + ctx->frame_index++; coalesce_timing_table(ctx->compute_timing_table, ctx->compute_shader_stats); + // NOTE(rnp): reset frame state + { + ctx->registers = &ctx->base_registers; + swap(ctx->command_queues[0], ctx->command_queues[1]); + zero_struct(ctx->command_queues + 0); + //zero_struct(ctx->registers); + end_temp_arena(ctx->frame_arena_savepoints[ctx->frame_index % countof(ctx->frame_arenas)]); + } + beamformer_process_input_events(ctx, input, input->event_queue, input->event_count); BeamformerSharedMemory *sm = ctx->shared_memory; + u32 live_imaging_active = atomic_load_u32(&sm->live_imaging_parameters.active); + if (live_imaging_active != ctx->live_imaging_active) { + if (ctx->live_imaging_active) { + BeamformerUIPanel *parent = ctx->auto_live_control_panel->parent; + beamformer_command(beamformer_command_infos[BeamformerCommandKind_CloseTab].string, .tree_node = (u64)ctx->auto_live_control_panel); + if (parent->child_count == 1) + beamformer_command(beamformer_command_infos[BeamformerCommandKind_CloseTab].string, .tree_node = (u64)parent); + ctx->auto_live_control_panel = 0; + } else { + ctx->live_imaging_active_frame = ctx->frame_index; + ctx->auto_live_control_panel = beamformer_ui_push_panel(0, BeamformerPanelKind_LiveImagingControls); + beamformer_command(beamformer_command_infos[BeamformerCommandKind_SplitTree].string, + .tree_node = (u64)ctx->auto_live_control_panel, + .split_axis = Axis2_X, + .split_left_tree = (u64)ui_context->tree, + .split_right_tree = 0, + .drop_target_tree = (u64)ui_context->tree); + } + ctx->live_imaging_active = live_imaging_active; + } + if (atomic_load_u32(sm->locks + BeamformerSharedMemoryLockKind_UploadRF)) os_wake_all_waiters(&ctx->upload_worker.sync_variable); if (atomic_load_u32(sm->locks + BeamformerSharedMemoryLockKind_DispatchCompute)) os_wake_all_waiters(&ctx->compute_worker.sync_variable); - BeamformerFrame *frame = ctx->latest_frame; - BeamformerViewPlaneTag tag = frame? frame->view_plane_tag : 0; - draw_ui(ctx, input, frame, tag); + beamformer_registers()->frame = (u64)(ctx->latest_frame - ctx->compute_context.backlog.frames); + + beamformer_ui_frame(); + + // NOTE(rnp): execute commands + for (BeamformerCommandNode *node = ctx->command_queues[0].first; + node; + node = node == node->next ? 0 : node->next) + { + BeamformerRegistersScope() + { + memory_copy(beamformer_registers(), node->command.registers, sizeof(*node->command.registers)); + BeamformerCommandKind kind = beamformer_command_kind_from_string(node->command.name); + switch (kind) { + InvalidDefaultCase; + case BeamformerCommandKind_CloseTab:{ + BeamformerUIPanel *tab = (BeamformerUIPanel *)beamformer_registers()->tree_node; + ui_kill_panel(tab); + }break; + + case BeamformerCommandKind_FocusTab:{ + BeamformerUIPanel *tab = (BeamformerUIPanel *)beamformer_registers()->tree_node; + assert(tab->parent->kind == BeamformerPanelKind_TabGroup); + tab->parent->u.tab_focus = tab; + }break; + + case BeamformerCommandKind_MoveTab:{ + BeamformerUIPanel *move = (BeamformerUIPanel *)beamformer_registers()->tree_node; + BeamformerUIPanel *group = (BeamformerUIPanel *)beamformer_registers()->drop_target_tree; + u64 new_child_index = beamformer_registers()->drop_child_index; + beamformer_panel_group_insert_at(group, move, new_child_index); + }break; + + case BeamformerCommandKind_OpenTab:{ + BeamformerUIPanel *panel = (BeamformerUIPanel *)beamformer_registers()->tree_node; + assert(panel->kind == BeamformerPanelKind_TabGroup); + + BeamformerPanelKind new_panel_kind = beamformer_panel_kind_from_string(beamformer_registers()->string); + beamformer_ui_push_panel(panel, new_panel_kind); + }break; + + case BeamformerCommandKind_SplitTree:{ + BeamformerUIPanel *drag = (BeamformerUIPanel *)beamformer_registers()->tree_node; + BeamformerUIPanel *left = (BeamformerUIPanel *)beamformer_registers()->split_left_tree; + BeamformerUIPanel *right = (BeamformerUIPanel *)beamformer_registers()->split_right_tree; + Axis2 axis = beamformer_registers()->split_axis; + + BeamformerUIPanel *new_split = beamformer_ui_push_panel(0, BeamformerPanelKind_Split); + BeamformerUIPanel *new_tab_group = beamformer_ui_push_panel(0, BeamformerPanelKind_TabGroup); + beamformer_panel_group_insert_at(new_tab_group, drag, 0); + + BeamformerUIPanel *target = 0; + u32 target_child_index = 0; + f32 new_split_pct = 0.5f; + + if (left == 0 || right == 0) { + // NOTE(rnp): split on edge of window + target = left ? left : right; + target_child_index = left ? 0 : 1; + + if (target->kind == BeamformerPanelKind_TabGroup) { + new_split->kind = BeamformerPanelKind_TabGroup; + new_split->u.tab_focus = target->u.tab_focus; + } + + for (BeamformerUIPanel *child = target->last_child, *next; child; child = next) { + next = child->previous_sibling; + beamformer_panel_group_insert_at(new_split, child, 0); + } + + beamformer_panel_group_insert_at(target, new_tab_group, 0); + } else if (((drag == left) && right->kind == BeamformerPanelKind_Split) || + ((drag == right) && left->kind == BeamformerPanelKind_Split)) + { + // NOTE(rnp): split on internal split + target = left == drag ? right : left; + target_child_index = 1; + new_split_pct = 1.f / 3.f; + beamformer_panel_group_insert_at(new_split, new_tab_group, 0); + beamformer_panel_group_insert_at(new_split, target->last_child, 1); + } else { + // NOTE(rnp): TabGroup Split + target = left == drag ? right : left; + target_child_index = left == drag ? 1 : 0; + assert(target->kind == BeamformerPanelKind_TabGroup); + + new_split->kind = BeamformerPanelKind_TabGroup; + new_split->u.tab_focus = target->u.tab_focus; + for (BeamformerUIPanel *child = target->last_child, *next; child; child = next) { + next = child->previous_sibling; + beamformer_panel_group_insert_at(new_split, child, 0); + } + + beamformer_panel_group_insert_at(target, new_tab_group, 0); + } + + beamformer_panel_group_insert_at(target, new_split, target_child_index); + if (target->kind == BeamformerPanelKind_Split) { + new_split->u.split.axis = target->u.split.axis; + new_split->u.split.fraction = target->u.split.fraction; + } + target->kind = BeamformerPanelKind_Split; + target->u.split.axis = axis; + target->u.split.fraction = new_split_pct; + }break; + + } + } + } ctx->render_shader_updated = 0; } diff --git a/beamformer_internal.h b/beamformer_internal.h @@ -308,8 +308,11 @@ struct BeamformerComputePlan { i32 hadamard_order; b32 iq_pipeline; - m4 voxel_transform; m4 ui_voxel_transform; + // NOTE(rnp): final voxel transform determining frame dimensions + m4 voxel_transform; + // NOTE(rnp): final voxel transform used for beamforming + m4 das_voxel_transform; iv3 output_points; i32 average_frames; @@ -317,8 +320,6 @@ struct BeamformerComputePlan { // TODO(rnp): specialization constants v2 xdc_element_pitch; m4 xdc_transform; - // TODO(rnp): probably just compute this everytime - m4 das_voxel_transform; GPUBuffer array_parameters; @@ -406,6 +407,7 @@ typedef struct { u32 id; u32 compound_count; + u32 parameter_block; BeamformerDataKind data_kind; BeamformerAcquisitionKind acquisition_kind; BeamformerContrastMode contrast_mode; @@ -456,6 +458,30 @@ typedef struct { b32 awake; } GLWorkerThreadContext; +typedef struct { + str8 name; + BeamformerRegisters *registers; +} BeamformerCommand; + +typedef struct BeamformerRegistersNode BeamformerRegistersNode; +struct BeamformerRegistersNode { + BeamformerRegistersNode *next; + BeamformerRegisters v; +}; + +typedef struct BeamformerCommandNode BeamformerCommandNode; +struct BeamformerCommandNode { + BeamformerCommandNode *next; + BeamformerCommandNode *prev; + BeamformerCommand command; +}; + +typedef struct { + BeamformerCommandNode *first; + BeamformerCommandNode *last; + u64 count; +} BeamformerCommandList; + typedef enum { BeamformerState_Uninitialized = 0, BeamformerState_Running, @@ -463,6 +489,111 @@ typedef enum { BeamformerState_Terminated, } BeamformerState; +typedef enum { + RulerState_None, + RulerState_Start, + RulerState_Hold, + RulerState_Count, +} RulerState; + +typedef struct { + v3 start; + v3 end; + RulerState state; +} Ruler; + +typedef struct { + f32 t; + f32 scale; +} BeamformerUIBlinker; + +#define BEAMFORMER_FRAME_VIEW_KIND_LIST \ + X(Latest, "Latest") \ + X(3DXPlane, "3D X-Plane") \ + X(Copy, "Copy") \ + +typedef enum { + #define X(kind, ...) BeamformerFrameViewKind_##kind, + BEAMFORMER_FRAME_VIEW_KIND_LIST + #undef X + BeamformerFrameViewKind_Count, +} BeamformerFrameViewKind; + +typedef struct BeamformerFrameView BeamformerFrameView; +struct BeamformerFrameView { + BeamformerFrameViewKind kind; + b32 dirty; + + // NOTE(rnp): for FrameViewKindCopy + GPUBuffer copy_buffer; + + GPUImage colour_image; + // NOTE(rnp): temporary, on w32 we must hold onto this when importing vulkan data to OpenGL + OSHandle export_handle; + u32 memory_object; + u32 texture; + + b32 log_scale; + f32 threshold; + f32 dynamic_range; + f32 gamma; + + union { + /* BeamformerFrameViewKind_Latest/BeamformerFrameViewKind_Copy */ + struct { + b32 scale_bar_active[2]; + Ruler ruler; + BeamformerViewPlaneTag view_plane; + BeamformerFrame frame; + }; + + /* BeamformerFrameViewKind_3DXPlane */ + struct { + f32 hot_t[BeamformerViewPlaneTag_Count]; + //b32 plane_active[BeamformerViewPlaneTag_Count]; + b32 plane_active[BeamformerViewPlaneTag_Count - 2]; + i32 plane_drag_index; + f32 rotation; + b32 demo; + v3 hit_start_point; + v3 hit_test_point; + }; + }; + + BeamformerFrameView *prev, *next; +}; + +typedef struct BeamformerUIPanel BeamformerUIPanel; +struct BeamformerUIPanel { + BeamformerUIPanel *parent; + BeamformerUIPanel *last_child; + BeamformerUIPanel *first_child; + BeamformerUIPanel *previous_sibling; + BeamformerUIPanel *next_sibling; + + u64 child_count; + + BeamformerPanelKind kind; + union { + BeamformerFrameView *frame_view; + + BeamformerUIPanel *tab_focus; + + BeamformerUIBlinker compute_stats_broken_shader_blinker; + BeamformerUIBlinker live_imaging_save_button_blinker; + + struct { + u32 parameter_block; + b16 expand_coordinate[2]; + } parameter_listing; + + struct { + Axis2 axis; + f32 fraction; + } split; + } u; +}; + typedef struct { BeamformerState state; @@ -473,7 +604,19 @@ typedef struct { void *ui; u32 ui_dirty_parameter_blocks; - u64 frame_timestamp; + u64 frame_timestamp; + u64 frame_index; + Arena frame_arenas[2]; + TempArena frame_arena_savepoints[2]; + + BeamformerUIPanel *auto_live_control_panel; + u64 live_imaging_active_frame; + b32 live_imaging_active; + + BeamformerRegistersNode base_registers; + BeamformerRegistersNode *registers; + + BeamformerCommandList command_queues[2]; Stream error_stream; diff --git a/beamformer_parameters.h b/beamformer_parameters.h @@ -1,7 +1,6 @@ /* See LICENSE for license details. */ /* TODO(rnp): - * [ ]: shader kinds have ballooned; shader stats table needs to be compressed * [ ]: Upload previously exported data for display. maybe this is a UI thing but doing it * programatically would be nice. * [ ]: Add interface for multi frame upload. RF upload already uses an offset into SM so @@ -18,20 +17,6 @@ typedef struct { float rf_time_deltas[32]; } BeamformerComputeStatsTable; -/* X(type, id, pretty name) */ -#define BEAMFORMER_VIEW_PLANE_TAG_LIST \ - X(XZ, 0, "XZ") \ - X(YZ, 1, "YZ") \ - X(XY, 2, "XY") \ - X(Arbitrary, 3, "Arbitrary") - -typedef enum { - #define X(type, id, pretty) BeamformerViewPlaneTag_##type = id, - BEAMFORMER_VIEW_PLANE_TAG_LIST - #undef X - BeamformerViewPlaneTag_Count, -} BeamformerViewPlaneTag; - #define BEAMFORMER_LIVE_IMAGING_DIRTY_FLAG_LIST \ X(ImagePlaneOffsets, 0) \ X(TransmitPower, 1) \ diff --git a/build.c b/build.c @@ -21,6 +21,7 @@ #include <stdio.h> #define BeamformerMaxComputeShaderStages 1 +#define BeamformerViewPlaneTag_Count 1 #include "beamformer_parameters.h" global char *g_argv0; diff --git a/generated/beamformer.meta.c b/generated/beamformer.meta.c @@ -74,10 +74,12 @@ typedef enum { } BeamformerInterpolationMode; typedef enum { - BeamformerFilterKind_Kaiser = 0, - BeamformerFilterKind_MatchedChirp = 1, - BeamformerFilterKind_Count, -} BeamformerFilterKind; + BeamformerViewPlaneTag_XZ = 0, + BeamformerViewPlaneTag_YZ = 1, + BeamformerViewPlaneTag_XY = 2, + BeamformerViewPlaneTag_Arbitrary = 3, + BeamformerViewPlaneTag_Count, +} BeamformerViewPlaneTag; typedef enum { BeamformerAcquisitionKind_FORCES = 0, @@ -97,6 +99,56 @@ typedef enum { } BeamformerAcquisitionKind; typedef enum { + BeamformerFilterKind_Kaiser = 0, + BeamformerFilterKind_MatchedChirp = 1, + BeamformerFilterKind_Count, +} BeamformerFilterKind; + +typedef enum { + BeamformerPanelKind_Nil = 0, + BeamformerPanelKind_Split = 1, + BeamformerPanelKind_TabGroup = 2, + BeamformerPanelKind_ComputeBarGraph = 3, + BeamformerPanelKind_ComputeStats = 4, + BeamformerPanelKind_FrameViewLive = 5, + BeamformerPanelKind_FrameViewCopy = 6, + BeamformerPanelKind_FrameViewXPlane = 7, + BeamformerPanelKind_LiveImagingControls = 8, + BeamformerPanelKind_ParameterListing = 9, + BeamformerPanelKind_Count, +} BeamformerPanelKind; + +typedef enum { + BeamformerRegisterSlot_String = 0, + BeamformerRegisterSlot_TreeNode = 1, + BeamformerRegisterSlot_Window = 2, + BeamformerRegisterSlot_Frame = 3, + BeamformerRegisterSlot_FrameView = 4, + BeamformerRegisterSlot_SplitAxis = 5, + BeamformerRegisterSlot_SplitLeftTree = 6, + BeamformerRegisterSlot_SplitRightTree = 7, + BeamformerRegisterSlot_DropTargetTree = 8, + BeamformerRegisterSlot_DropChildIndex = 9, + BeamformerRegisterSlot_Count, +} BeamformerRegisterSlot; + +typedef enum { + BeamformerCommandKind_Nil = 0, + BeamformerCommandKind_CloseTab = 1, + BeamformerCommandKind_FocusTab = 2, + BeamformerCommandKind_MoveTab = 3, + BeamformerCommandKind_OpenTab = 4, + BeamformerCommandKind_SplitTree = 5, + BeamformerCommandKind_Count, +} BeamformerCommandKind; + +typedef enum { + BeamformerPanelFlags_List = 1 << 0, + BeamformerPanelFlags_NeedsFrame = 1 << 1, + BeamformerPanelFlags_HasSettings = 1 << 2, +} BeamformerPanelFlags; + +typedef enum { BeamformerShaderKind_Decode = 0, BeamformerShaderKind_Filter = 1, BeamformerShaderKind_Demodulate = 2, @@ -398,6 +450,32 @@ typedef struct { u16 transmit_receive_orientations[BeamformerMaxChannelCount]; } BeamformerDASArrayParameters; +typedef struct { + str8 display; + str8 string; + str8 description; + BeamformerPanelFlags flags; +} BeamformerPanelInfo; + +typedef struct { + str8 string; + u64 tree_node; + u64 window; + u64 frame; + u64 frame_view; + u64 split_axis; + u64 split_left_tree; + u64 split_right_tree; + u64 drop_target_tree; + u64 drop_child_index; +} BeamformerRegisters; + +typedef struct { + str8 string; + str8 display; + str8 description; +} BeamformerCommandInfo; + typedef union { BeamformerDecodeBakeParameters Decode; BeamformerFilterBakeParameters Filter; @@ -469,6 +547,13 @@ read_only global str8 beamformer_contrast_mode_strings[] = { str8_comp("A1S2"), }; +read_only global str8 beamformer_view_plane_tag_strings[] = { + str8_comp("XZ"), + str8_comp("YZ"), + str8_comp("XY"), + str8_comp("Arbitrary"), +}; + read_only global u8 beamformer_acquisition_kind_has_fixed_transmits[] = { 1, 0, @@ -485,20 +570,20 @@ read_only global u8 beamformer_acquisition_kind_has_fixed_transmits[] = { 0, }; -read_only global s8 beamformer_acquisition_kind_strings[] = { - s8_comp("FORCES"), - s8_comp("UFORCES"), - s8_comp("HERCULES"), - s8_comp("VLS"), - s8_comp("TPW"), - s8_comp("UHERCULES"), - s8_comp("RACES"), - s8_comp("EPIC-FORCES"), - s8_comp("EPIC-UFORCES"), - s8_comp("EPIC-UHERCULES"), - s8_comp("Flash"), - s8_comp("HERO-PA"), - s8_comp("ULM"), +read_only global str8 beamformer_acquisition_kind_strings[] = { + str8_comp("FORCES"), + str8_comp("UFORCES"), + str8_comp("HERCULES"), + str8_comp("VLS"), + str8_comp("TPW"), + str8_comp("UHERCULES"), + str8_comp("RACES"), + str8_comp("EPIC-FORCES"), + str8_comp("EPIC-UFORCES"), + str8_comp("EPIC-UHERCULES"), + str8_comp("Flash"), + str8_comp("HERO-PA"), + str8_comp("ULM"), }; read_only global s8 beamformer_filter_kind_strings[] = { @@ -516,9 +601,38 @@ read_only global s8 beamformer_shader_resource_kind_strings[] = { s8_comp("Buffer"), }; -read_only global s8 game_shader_buffer_slot_strings[] = { - s8_comp("BeamformedData"), - s8_comp("PingPong"), +read_only global BeamformerPanelInfo beamformer_panel_infos[] = { + {str8_comp(""), str8_comp("nil"), str8_comp(""), (0*BeamformerPanelFlags_List)|(0*BeamformerPanelFlags_NeedsFrame)|(0*BeamformerPanelFlags_HasSettings)}, + {str8_comp(""), str8_comp("split"), str8_comp(""), (0*BeamformerPanelFlags_List)|(0*BeamformerPanelFlags_NeedsFrame)|(0*BeamformerPanelFlags_HasSettings)}, + {str8_comp(""), str8_comp("group"), str8_comp(""), (0*BeamformerPanelFlags_List)|(0*BeamformerPanelFlags_NeedsFrame)|(0*BeamformerPanelFlags_HasSettings)}, + {str8_comp("Compute Bar Graph"), str8_comp("compute_bar_graph"), str8_comp("Bar graph showing portions of compute occupied by each stage."), (1*BeamformerPanelFlags_List)|(0*BeamformerPanelFlags_NeedsFrame)|(0*BeamformerPanelFlags_HasSettings)}, + {str8_comp("Compute Stats"), str8_comp("compute_stats"), str8_comp("Average stats about beamforming computations."), (1*BeamformerPanelFlags_List)|(0*BeamformerPanelFlags_NeedsFrame)|(0*BeamformerPanelFlags_HasSettings)}, + {str8_comp("Frame View"), str8_comp("frame_view_live"), str8_comp("Latest frame with selected tag."), (1*BeamformerPanelFlags_List)|(0*BeamformerPanelFlags_NeedsFrame)|(1*BeamformerPanelFlags_HasSettings)}, + {str8_comp("Frame View (Copy)"), str8_comp("frame_view_copy"), str8_comp("Copy of an old frame. Useful for comparisons."), (0*BeamformerPanelFlags_List)|(1*BeamformerPanelFlags_NeedsFrame)|(1*BeamformerPanelFlags_HasSettings)}, + {str8_comp("3D X-Plane"), str8_comp("frame_view_xplane"), str8_comp("3D Cross Plane View."), (1*BeamformerPanelFlags_List)|(0*BeamformerPanelFlags_NeedsFrame)|(1*BeamformerPanelFlags_HasSettings)}, + {str8_comp("Live Controls"), str8_comp("live_controls"), str8_comp("Imaging system controls."), (1*BeamformerPanelFlags_List)|(0*BeamformerPanelFlags_NeedsFrame)|(0*BeamformerPanelFlags_HasSettings)}, + {str8_comp("Parameter Listing"), str8_comp("parameters_listing"), str8_comp("Editable parameter set used for beamforming."), (1*BeamformerPanelFlags_List)|(0*BeamformerPanelFlags_NeedsFrame)|(1*BeamformerPanelFlags_HasSettings)}, +}; + +#define beamformer_registers_init_literal \ + .string = beamformer_registers()->string, \ + .tree_node = beamformer_registers()->tree_node, \ + .window = beamformer_registers()->window, \ + .frame = beamformer_registers()->frame, \ + .frame_view = beamformer_registers()->frame_view, \ + .split_axis = beamformer_registers()->split_axis, \ + .split_left_tree = beamformer_registers()->split_left_tree, \ + .split_right_tree = beamformer_registers()->split_right_tree, \ + .drop_target_tree = beamformer_registers()->drop_target_tree, \ + .drop_child_index = beamformer_registers()->drop_child_index, \ + +read_only global BeamformerCommandInfo beamformer_command_infos[] = { + {0}, + {str8_comp("close_tab"), str8_comp("Close Tab"), str8_comp("Closes currently active tab.")}, + {str8_comp("focus_tab"), str8_comp("Focus Tab"), str8_comp("Focus the currently selected tab in its group.")}, + {str8_comp("move_tab"), str8_comp("Move Tab"), str8_comp("Moves an exisiting tab to a different group/split.")}, + {str8_comp("open_tab"), str8_comp("Open Tab"), str8_comp("Opens a new tab with a specified view.")}, + {str8_comp("split_tree"), str8_comp("Split Tree"), str8_comp("Splits the UI into two new splits.")}, }; read_only global s8 beamformer_shader_names[] = { diff --git a/intrinsics.c b/intrinsics.c @@ -220,3 +220,11 @@ typedef __m128i u32x4; #define store_fence _mm_sfence #endif + +function force_inline f32 +inf32(void) +{ + union {u32 u; f32 f;} result; + result.u = 0x7F800000u; + return result.f; +} diff --git a/main_linux.c b/main_linux.c @@ -303,6 +303,26 @@ os_window_create(u8 *title, i64 title_length, i32 width, i32 height) return result; } +BEAMFORMER_IMPORT u8 * +os_get_clipboard_text(i64 *length) +{ + str8 result = str8_from_c_str((char *)GetClipboardText()); + if (length) *length = result.length; + return result.data; +} + +BEAMFORMER_IMPORT void +os_set_clipboard_text(u8 *data, i64 length) +{ + DeferLoop(take_lock(&os_linux_context.arena_lock, -1), release_lock(&os_linux_context.arena_lock)) + { + Arena scratch = os_linux_context.arena; + str8 string = {.data = data, .length = Min(length, arena_capacity(&scratch, u8) - 1)}; + str8 safe_string = push_str8(&scratch, string); + SetClipboardText((char *)safe_string.data); + } +} + function OSLibrary load_library(char *name, char *temp_name, u32 flags) { diff --git a/main_w32.c b/main_w32.c @@ -343,6 +343,26 @@ os_window_create(u8 *title, i64 title_length, i32 width, i32 height) return result; } +BEAMFORMER_IMPORT u8 * +os_get_clipboard_text(i64 *length) +{ + str8 result = str8_from_c_str((char *)GetClipboardText()); + if (length) *length = result.length; + return result.data; +} + +BEAMFORMER_IMPORT void +os_set_clipboard_text(u8 *data, i64 length) +{ + DeferLoop(take_lock(&os_w32_context.arena_lock, -1), release_lock(&os_w32_context.arena_lock)) + { + Arena scratch = os_w32_context.arena; + str8 string = {.data = data, .length = Min(length, arena_capacity(&scratch, u8) - 1)}; + str8 safe_string = push_str8(&scratch, string); + SetClipboardText((char *)safe_string.data); + } +} + #if BEAMFORMER_RENDERDOC_HOOKS function OSLibrary get_module(char *name) diff --git a/math.c b/math.c @@ -424,16 +424,13 @@ m4_equal(m4 a, m4 b) return result; } -function m4 -m4_identity(void) -{ - m4 result; - result.c[0] = (v4){{1, 0, 0, 0}}; - result.c[1] = (v4){{0, 1, 0, 0}}; - result.c[2] = (v4){{0, 0, 1, 0}}; - result.c[3] = (v4){{0, 0, 0, 1}}; - return result; -} +#define m4_identity() \ + (m4){.E = { \ + 1, 0, 0, 0, \ + 0, 1, 0, 0, \ + 0, 0, 1, 0, \ + 0, 0, 0, 1, \ + }} function v4 m4_row(m4 a, u32 row) @@ -567,6 +564,54 @@ m4_mul_v3(m4 a, v3 v) return result; } +function v2 +rect_uv(v2 p, Rect r) +{ + v2 result = v2_div(v2_sub(p, r.pos), r.size); + return result; +} + +function v2 +rect_uv_ndc(v2 p, Rect r) +{ + v2 uv = rect_uv(p, r); + v2 result = v2_sub(v2_scale(uv, 2.f), (v2){{1.f, 1.f}}); + return result; +} + +function Rect +rect_intersect(Rect a, Rect b) +{ + v2 ae = v2_add(a.pos, a.size); + v2 be = v2_add(b.pos, b.size); + + Rect result = {0}; + result.pos.x = Max(a.pos.x, b.pos.x); + result.pos.y = Max(a.pos.y, b.pos.y); + result.size.x = Min(ae.x, be.x) - result.pos.x; + result.size.y = Min(ae.y, be.y) - result.pos.y; + return result; +} + +function Rect +rect_squish_centered(Rect a, v2 pct) +{ + v2 delta_size = v2_mul(a.size, pct); + Rect result; + result.pos = v2_add(a.pos, v2_scale(delta_size, 0.5f)); + result.size = v2_add(a.size, v2_scale(delta_size, -1.f)); + return result; +} + +function Rect +rect_shrink_centered(Rect a, v2 px) +{ + Rect result; + result.pos = v2_add(a.pos, v2_scale(px, 0.5f)); + result.size = v2_add(a.size, v2_scale(px, -1.f)); + return result; +} + function m4 orthographic_projection(f32 n, f32 f, f32 t, f32 r) { @@ -871,6 +916,35 @@ das_transform(v3 min_coordinate, v3 max_coordinate, iv3 *points) return result; } +function v3 +plane_normal_from_transform(m4 transform) +{ + v3 U = v3_normalize(transform.c[0].xyz); + v3 V = v3_normalize(transform.c[1].xyz); + v3 result = cross(V, U); + return result; +} + +function f32 +plane_offset_from_transform(m4 transform) +{ + f32 result = v3_dot(plane_normal_from_transform(transform), transform.c[3].xyz); + return result; +} + +function void +plane_corners_from_transform(m4 transform, v2 *min, v2 *max) +{ + v3 U = v3_normalize(transform.c[0].xyz); + v3 V = v3_normalize(transform.c[1].xyz); + + v3 min_3d = m4_mul_v3(transform, (v3){{0.f, 0.f, 0.f}}); + v3 max_3d = m4_mul_v3(transform, (v3){{1.f, 1.f, 1.f}}); + + if (min) *min = (v2){{v3_dot(U, min_3d), v3_dot(V, min_3d)}}; + if (max) *max = (v2){{v3_dot(U, max_3d), v3_dot(V, max_3d)}}; +} + function v2 plane_uv(v3 point, v3 U, v3 V) { diff --git a/ui.c b/ui.c @@ -1,67 +1,44 @@ /* See LICENSE for license details. */ /* TODO(rnp): - * [ ]: bug: draw_view_ruler() needs to use a ray intersection instead of clamping - * [ ]: bug: plane rotation and offset position only work if plane is Z aligned - * [ ]: refactor: ui and views need to store current uv coordinates for expected transform - * [ ]: refactor: there shouldn't need to be if 1d checks all over - * [ ]: refactor: ui kind of needs to be mostly thrown away - * - want all drawing to be immediate mode - * - only layout information should be retained - * - leaf nodes of layout have a kind associated which - * instructs the builder code on how to build the view - * - ui items (currently called Variables) are stored in a hash - * table and are looked up for state information at frame building time - * - removed/recycled when last building frame index is less than drawing frame index - * - building: - * - loop over tiled layout tree and floating layout tree as is currently done - * - this will build current frame ui draw tree - * - for each view use a stack structure with grouping, similar to how tables are made - * - more general though: sub groups contain a draw axis (x or y) - * - each ui item gets looked up in the hash table for previous frame drawing info - * - this can then be used to construct a "ui comm" which contains relevant info - * about how that ui item is being interacted with - * - drawing: - * - must be separated into layout constraint solving and rendering - * - layout constraint solving handles sizing, clipping, etc. - * - this will need multiple passes per subgroup to allow for autosizing - * - pay attention to the fixed size points in the hierarchy. fixed size - * items are complete once their children are complete. - * - rendering simply uses the rect/clipping regions produced by layout - * to send draw commands - * [ ]: bug: resizing live view causes texture to jump around - * [ ]: bug: group at end of parameter listing - * [ ]: refactor: ui should be in its own thread and that thread should only be concerned with the ui - * [ ]: refactor: remove all the excessive measure_texts (cell drawing, hover_interaction in params table) - * [ ]: refactor: move remaining fragment shader stuff into ui - * [ ]: refactor: scale table to rect - * [ ]: scroll bar for views that don't have enough space - * [ ]: allow views to collapse to just their title bar - * - title bar struct with expanded. Check when pushing onto draw stack; if expanded - * do normal behaviour else make size title bar size and ignore the splits fraction. - * [ ]: enforce a minimum region size or allow regions themselves to scroll - * [ ]: refactor: add_variable_no_link() - * [ ]: refactor: draw_text_limited should clamp to rect and measure text itself - * [ ]: draw the ui with a post-order traversal instead of pre-order traversal - * [ ]: consider V_HOVER_GROUP and use that to implement submenus - * [ ]: menu's need to support nested groups - * [ ]: don't redraw on every refresh; instead redraw on mouse movement/event or when a new frame - * arrives. For animations the ui can have a list of "timers" which while active will - * do a redraw on every refresh until completed. - * [ ]: show full non-truncated string on hover - * [ ]: refactor: hovered element type and show hovered element in full even when truncated - * [ ]: bug: cross-plane view with different dimensions for each plane - * [ ]: refactor: make table_skip_rows useful - * [ ]: refactor: better method of grouping variables for views such as FrameView/ComputeStatsView + * [ ]: bug: context_menu_close on live parameters acquisition jumps to left corner of beamformer + * [ ]: bug: flickering x-scroll bar on switch from ComputeBarGraph to other + * [ ]: word scan for text input + * [ ]: animation state + * [ ]: tooltips + * [ ]: extra copy view settings + * [ ]: refactor: all drag overlay floating elements can be children of the drag_root. + * as long as we layout before chaining them on there won't be an issue. + * [ ]: refactor: move context menu building code next to normal ui building. + * keeping this localized makes it easier to add new panels. + * [ ]: refactor: can the scroll container just use the ViewScroll flags like the tab bar? + * [ ]: refactor: it would be nice to have some table building helpers + * + * [ ]: refactor: cross plane view for non XZ/YZ planes. math needs to be cleaned up + * to support this. + * - model transform needs to first rotate so that Z is normal, then scale, the rotate from Z to Y. + * - ideally the hardcoded +0.25f rotation for YZ should just be a consequence of the math + * [ ]: command window + * [ ]: 3D data view + * - add extra view controls, change view without recompute + * - to start just have starting plane/normal, plane uvs, rotation, and offset + * - confirmation on recompute + * [ ]: rich highlighting for parameters -> X-Plane link + * + * [ ]: multi-os windows + * [ ]: ui color configuration at runtime */ #include "assets/generated/assets.c" +#define NIL_COLOUR (v4){{0.76f, 0.00f, 0.65f, 1.0f}} #define BG_COLOUR (v4){{0.15f, 0.12f, 0.13f, 1.0f}} #define FG_COLOUR (v4){{0.92f, 0.88f, 0.78f, 1.0f}} #define FOCUSED_COLOUR (v4){{0.86f, 0.28f, 0.21f, 1.0f}} #define HOVERED_COLOUR (v4){{0.11f, 0.50f, 0.59f, 1.0f}} +#define SELECTION_COLOUR (v4){{0.07f, 0.37f, 0.90f, 0.5f}} #define RULER_COLOUR (v4){{1.00f, 0.70f, 0.00f, 1.0f}} #define BORDER_COLOUR v4_lerp(FG_COLOUR, BG_COLOUR, 0.85f) +#define NODE_SPLIT_COLOUR (v4){{0.6f, 0.6f, 0.6f, 0.5f}} #define FRAME_VIEW_BB_COLOUR (v4){{0.92f, 0.88f, 0.78f, 1.0f}} #define FRAME_VIEW_BB_FRACTION 0.007f @@ -70,6 +47,11 @@ #define MENU_PLUS_COLOUR (v4){{0.33f, 0.42f, 1.00f, 1.00f}} #define MENU_CLOSE_COLOUR FOCUSED_COLOUR +#define UI_NODE_PAD 8.f +#define UI_BORDER_THICK 4.f + +#define UI_HASH_TABLE_COUNT 4096 + read_only global v4 g_colour_palette[] = { {{0.32f, 0.20f, 0.50f, 1.00f}}, {{0.14f, 0.39f, 0.61f, 1.00f}}, @@ -85,10 +67,10 @@ read_only global v4 g_colour_palette[] = { #define TABLE_CELL_PAD_HEIGHT 2.0f #define TABLE_CELL_PAD_WIDTH 8.0f -#define RULER_TEXT_PAD 10.0f +#define RULER_TEXT_PAD 6.0f #define RULER_TICK_LENGTH 20.0f -#define UI_SPLIT_HANDLE_THICK 8.0f +#define UI_SPLIT_HANDLE_THICK 5.0f #define UI_REGION_PAD 32.0f /* TODO(rnp) smooth scroll */ @@ -97,346 +79,290 @@ read_only global v4 g_colour_palette[] = { #define LISTING_LINE_PAD 6.0f #define TITLE_BAR_PAD 6.0f -typedef struct v2_sll { - struct v2_sll *next; - v2 v; -} v2_sll; - -typedef struct { - f32 t; - f32 scale; -} UIBlinker; +typedef enum { + UINodeFlag_MouseClickable = 1ull << 0, + UINodeFlag_KeyboardClickable = 1ull << 1, + UINodeFlag_DropSite = 1ull << 2, + UINodeFlag_ClickToFocus = 1ull << 3, + UINodeFlag_Scroll = 1ull << 4, + UINodeFlag_FocusHot = 1ull << 5, + UINodeFlag_FocusActive = 1ull << 6, + UINodeFlag_FocusHotDisabled = 1ull << 7, + UINodeFlag_FocusActiveDisabled = 1ull << 8, + UINodeFlag_Disabled = 1ull << 9, + + UINodeFlag_FloatingX = 1ull << 10, + UINodeFlag_FloatingY = 1ull << 11, + UINodeFlag_FixedWidth = 1ull << 12, + UINodeFlag_FixedHeight = 1ull << 13, + UINodeFlag_AllowOverflowX = 1ull << 14, + UINodeFlag_AllowOverflowY = 1ull << 15, + + // NOTE(rnp): for scrollable containers + UINodeFlag_ViewScrollX = 1ull << 16, + UINodeFlag_ViewScrollY = 1ull << 17, + + UINodeFlag_DrawDropShadow = 1ull << 18, + UINodeFlag_DrawBackgroundBlur = 1ull << 19, + UINodeFlag_DrawBackground = 1ull << 20, + UINodeFlag_DrawBorder = 1ull << 21, + UINodeFlag_DrawText = 1ull << 22, + UINodeFlag_DrawHotEffects = 1ull << 23, + UINodeFlag_DrawActiveEffects = 1ull << 24, + UINodeFlag_DrawOverlay = 1ull << 25, + UINodeFlag_Clip = 1ull << 26, + UINodeFlag_DisableTextTrunc = 1ull << 27, + UINodeFlag_DisableFocusBorder = 1ull << 28, + UINodeFlag_DisableFocusOverlay = 1ull << 29, + + UINodeFlag_TextInput = 1ull << 30, + UINodeFlag_TextInputNumeric = 1ull << 31, + UINodeFlag_TextInputClearOnStart = 1ull << 32, + + UINodeFlag_CustomDraw = 1ull << 33, + + // TODO(rnp): hack: when text is not drawn with raylib do something smarter + UINodeFlag_IconText = 1ull << 34, + + UINodeFlag_Clickable = UINodeFlag_MouseClickable|UINodeFlag_KeyboardClickable, + UINodeFlag_Floating = UINodeFlag_FloatingX|UINodeFlag_FloatingY, + UINodeFlag_FixedSize = UINodeFlag_FixedWidth|UINodeFlag_FixedHeight, + UINodeFlag_AllowOverflow = UINodeFlag_AllowOverflowX|UINodeFlag_AllowOverflowY, + UINodeFlag_DisableFocusEffects = UINodeFlag_DisableFocusBorder|UINodeFlag_DisableFocusOverlay, + UINodeFlag_ViewScroll = UINodeFlag_ViewScrollX|UINodeFlag_ViewScrollY, +} UINodeFlags; + +typedef struct UINodeFlagsNode UINodeFlagsNode; +struct UINodeFlagsNode {UINodeFlagsNode *next; UINodeFlags v;}; + +typedef struct Axis2Node Axis2Node; +struct Axis2Node {Axis2Node *next; Axis2 v;}; -typedef struct BeamformerUI BeamformerUI; -typedef struct Variable Variable; +typedef enum { + UISizeKind_Nil, + UISizeKind_Pixels, + UISizeKind_TextContent, + UISizeKind_PercentOfParent, + UISizeKind_ChildrenSum, +} UISizeKind; typedef struct { - u8 buf[128]; - i32 count; - i32 cursor; - b32 numeric; - UIBlinker cursor_blink; - Font *font, *hot_font; - Variable *container; -} InputState; + UISizeKind kind; + f32 value; + f32 strictness; +} UISize; + +typedef struct UISizeNode UISizeNode; +struct UISizeNode {UISizeNode *next; UISize v;}; typedef enum { - RulerState_None, - RulerState_Start, - RulerState_Hold, -} RulerState; + UIAlign_Left, + UIAlign_Right, + UIAlign_Center, + UIAlign_Count, +} UIAlign; -typedef struct { - v3 start; - v3 end; - RulerState state; -} Ruler; +typedef struct UIAlignNode UIAlignNode; +struct UIAlignNode {UIAlignNode *next; UIAlign v;}; -typedef enum { - SB_LATERAL, - SB_AXIAL, -} ScaleBarDirection; +typedef struct {u64 value;} UINodeKey; -typedef struct { - f32 *min_value, *max_value; - v2_sll *savepoint_stack; - v2 scroll_scale; - f32 zoom_starting_coord; - ScaleBarDirection direction; -} ScaleBar; +typedef struct UINode UINode; -typedef struct { f32 val, scale; } scaled_f32; +#define UI_CUSTOM_DRAW_FUNCTION(name) void name(UINode *node, Rect node_rect) +typedef UI_CUSTOM_DRAW_FUNCTION(UICustomDrawFunction); -typedef enum { - RSD_VERTICAL, - RSD_HORIZONTAL, -} RegionSplitDirection; +struct UINode { + UINode *parent; + UINode *first_child; + UINode *last_child; + UINode *previous_sibling; + UINode *next_sibling; -typedef struct { - Variable *left; - Variable *right; - f32 fraction; - RegionSplitDirection direction; -} RegionSplit; - -#define COMPUTE_STATS_VIEW_LIST \ - X(Average, "Average") \ - X(Bar, "Bar") - -#define X(kind, ...) ComputeStatsViewKind_ ##kind, -typedef enum {COMPUTE_STATS_VIEW_LIST ComputeStatsViewKind_Count} ComputeStatsViewKind; -#undef X + u32 child_count; -typedef struct { - ComputeShaderStats *compute_shader_stats; - Variable *cycler; - ComputeStatsViewKind kind; - UIBlinker blink; -} ComputeStatsView; + UINodeFlags flags; + str8 string; + // NOTE(rnp): desired sizing info from build step + union { + struct { + UISize semantic_width; + UISize semantic_height; + }; + UISize semantic_size[Axis2_Count]; + }; -typedef struct { - b32 *processing; - f32 *progress; - f32 display_t; - f32 display_t_velocity; -} ComputeProgressBar; + union { + struct { + UIAlign alignment_x; + UIAlign alignment_y; + }; + UIAlign alignment[Axis2_Count]; + }; -typedef enum { - VT_NULL, - VT_B32, - VT_F32, - VT_I32, - VT_U32, - VT_GROUP, - VT_CYCLER, - VT_SCALED_F32, - VT_BEAMFORMER_VARIABLE, - VT_BEAMFORMER_FRAME_VIEW, - VT_COMPUTE_STATS_VIEW, - VT_COMPUTE_PROGRESS_BAR, - VT_LIVE_CONTROLS_VIEW, - VT_LIVE_CONTROLS_STRING, - VT_SCALE_BAR, - VT_UI_BUTTON, - VT_UI_MENU, - VT_UI_REGION_SPLIT, - VT_UI_TEXT_BOX, - VT_UI_VIEW, - VT_X_PLANE_SHIFT, -} VariableType; + UIAlign text_alignment; -typedef enum { - VariableGroupKind_List, - /* NOTE(rnp): special group for vectors with components - * stored in separate memory locations */ - VariableGroupKind_Vector, -} VariableGroupKind; + Axis2 child_layout_axis; + f32 font_size; -typedef struct { - VariableGroupKind kind; - b32 expanded; - Variable *first; - Variable *last; - Variable *container; -} VariableGroup; + u64 last_frame_active_index; + UINodeKey key; + UINode *hash_prev; + UINode *hash_next; -typedef enum { - UIViewFlag_CustomText = 1 << 0, - UIViewFlag_Floating = 1 << 1, -} UIViewFlags; + // NOTE(rnp): recomputed every frame before drawing. also + // used on next frame for mouse collision detection. + f32 computed_position[Axis2_Count]; + f32 computed_size[Axis2_Count]; -typedef struct { - Variable *child; - Variable *close; - Variable *menu; - Rect rect; - UIViewFlags flags; -} UIView; - -/* X(id, text) */ -#define FRAME_VIEW_BUTTONS \ - X(FV_COPY_HORIZONTAL, "Copy Horizontal") \ - X(FV_COPY_VERTICAL, "Copy Vertical") - -#define GLOBAL_MENU_BUTTONS \ - X(GM_OPEN_VIEW_RIGHT, "Open View Right") \ - X(GM_OPEN_VIEW_BELOW, "Open View Below") - -#define X(id, text) UI_BID_ ##id, -typedef enum { - UI_BID_VIEW_CLOSE, - GLOBAL_MENU_BUTTONS - FRAME_VIEW_BUTTONS - UI_BID_ACQUISITION_KIND_FIRST, - UI_BID_ACQUISITION_KIND_LAST = UI_BID_ACQUISITION_KIND_FIRST + BeamformerAcquisitionKind_Count - 1, -} UIButtonID; -#undef X + v2 text_size; -typedef struct { - s8 *labels; - u32 *state; - u32 cycle_length; -} VariableCycler; + // NOTE(rnp): persistent data + f32 active_t; + f32 hot_t; -typedef struct { - s8 suffix; - f32 display_scale; - f32 scroll_scale; - v2 limits; - f32 *store; -} BeamformerVariable; + v2 view_scroll_offset; -typedef struct { - v3 start_point; - v3 end_point; -} XPlaneShift; + v4 bg_colour; -typedef enum { - V_INPUT = 1 << 0, - V_TEXT = 1 << 1, - V_RADIO_BUTTON = 1 << 2, - V_EXTRA_ACTION = 1 << 3, - V_HIDES_CURSOR = 1 << 4, - V_LIVE_CONTROL = 1 << 28, - V_CAUSES_COMPUTE = 1 << 29, - V_UPDATE_VIEW = 1 << 30, -} VariableFlags; - -struct Variable { - s8 name; - union { - void *generic; - BeamformerVariable beamformer_variable; - ComputeProgressBar compute_progress_bar; - ComputeStatsView compute_stats_view; - RegionSplit region_split; - ScaleBar scale_bar; - UIButtonID button; - UIView view; - VariableCycler cycler; - VariableGroup group; - XPlaneShift x_plane_shift; - scaled_f32 scaled_real32; - b32 bool32; - i32 signed32; - u32 unsigned32; - f32 real32; - }; - Variable *next; - Variable *parent; - VariableFlags flags; - VariableType type; + v4 text_colour; + v4 text_outline_colour; + f32 text_outline_thickness; - f32 hover_t; - f32 name_width; + v4 border_colour; + f32 border_thickness; + + UICustomDrawFunction *custom_draw_function; + void *custom_draw_context; }; -#define BEAMFORMER_FRAME_VIEW_KIND_LIST \ - X(Latest, "Latest") \ - X(3DXPlane, "3D X-Plane") \ - X(Copy, "Copy") \ +typedef struct {UINode *first, *last;} UINodeHashBucket; + +typedef struct UIParentNode UIParentNode; +struct UIParentNode {UIParentNode *next; UINode *v;}; typedef enum { - #define X(kind, ...) BeamformerFrameViewKind_##kind, - BEAMFORMER_FRAME_VIEW_KIND_LIST - #undef X - BeamformerFrameViewKind_Count, -} BeamformerFrameViewKind; + UIMouseButtonKind_Left, + UIMouseButtonKind_Middle, + UIMouseButtonKind_Right, + UIMouseButtonKind_Count, +} UIMouseButtonKind; -typedef struct BeamformerFrameView BeamformerFrameView; -struct BeamformerFrameView { - BeamformerFrameViewKind kind; - b32 dirty; - BeamformerFrameView *prev, *next; +typedef enum { + UISignalFlag_LeftPressed = (1 << 0), + UISignalFlag_MiddlePressed = (1 << 1), + UISignalFlag_RightPressed = (1 << 2), - // NOTE(rnp): for FrameViewKindCopy - GPUBuffer copy_buffer; + UISignalFlag_LeftDragging = (1 << 3), + UISignalFlag_MiddleDragging = (1 << 4), + UISignalFlag_RightDragging = (1 << 5), - GPUImage colour_image; - // NOTE(rnp): temporary, on w32 we must hold onto this when importing vulkan data to OpenGL - OSHandle export_handle; - u32 memory_object; - u32 texture; + UISignalFlag_LeftDoubleDragging = (1 << 6), + UISignalFlag_MiddleDoubleDragging = (1 << 7), + UISignalFlag_RightDoubleDragging = (1 << 8), - /* NOTE(rnp): any pointers to variables are added to the menu and will - * be put onto the freelist if the view is closed. */ + UISignalFlag_LeftTripleDragging = (1 << 9), + UISignalFlag_MiddleTripleDragging = (1 << 10), + UISignalFlag_RightTripleDragging = (1 << 11), - Variable *kind_cycler; - Variable *log_scale; - Variable threshold; - Variable dynamic_range; - Variable gamma; + UISignalFlag_LeftReleased = (1 << 12), + UISignalFlag_MiddleReleased = (1 << 13), + UISignalFlag_RightReleased = (1 << 14), - union { - /* BeamformerFrameViewKind_Latest/BeamformerFrameViewKind_Copy */ - struct { - Variable lateral_scale_bar; - Variable axial_scale_bar; - Variable *lateral_scale_bar_active; - Variable *axial_scale_bar_active; - /* NOTE(rnp): selects which plane to use */ - Variable *cycler; - u32 cycler_state; + UISignalFlag_LeftClicked = (1 << 15), + UISignalFlag_MiddleClicked = (1 << 16), + UISignalFlag_RightClicked = (1 << 17), - Ruler ruler; + UISignalFlag_LeftDoubleClicked = (1 << 18), + UISignalFlag_MiddleDoubleClicked = (1 << 19), + UISignalFlag_RightDoubleClicked = (1 << 20), - v3 min_coordinate; - v3 max_coordinate; + UISignalFlag_LeftTripleClicked = (1 << 21), + UISignalFlag_MiddleTripleClicked = (1 << 22), + UISignalFlag_RightTripleClicked = (1 << 23), - BeamformerFrame frame; - }; + UISignalFlag_ScrolledX = (1 << 24), + UISignalFlag_ScrolledY = (1 << 25), - /* BeamformerFrameViewKind_3DXPlane */ - struct { - Variable x_plane_shifts[2]; - Variable *demo; - f32 rotation; - v3 hit_test_point; - }; - }; -}; + UISignalFlag_KeyboardPressed = (1 << 26), -typedef struct BeamformerLiveControlsView BeamformerLiveControlsView; -struct BeamformerLiveControlsView { - Variable acquisition_menu; - Variable transmit_power; - Variable tgc_control_points[countof(((BeamformerLiveImagingParameters *)0)->tgc_control_points)]; - Variable save_button; - Variable stop_button; - Variable save_text; - UIBlinker save_button_blink; - u32 hot_field_flag; - u32 active_field_flag; -}; + UISignalFlag_Hovering = (1 << 27), -typedef enum { - InteractionKind_None, - InteractionKind_Nop, - InteractionKind_Auto, - InteractionKind_Button, - InteractionKind_Drag, - InteractionKind_AcquisitionMenu, - InteractionKind_Menu, - InteractionKind_Ruler, - InteractionKind_Scroll, - InteractionKind_Set, - InteractionKind_Text, -} InteractionKind; + UISignalFlag_TextCommit = (1 << 28), + + UISignalFlag_Scrolled = UISignalFlag_ScrolledX|UISignalFlag_ScrolledY, + UISignalFlag_Pressed = UISignalFlag_LeftPressed|UISignalFlag_KeyboardPressed, + UISignalFlag_Released = UISignalFlag_LeftReleased, + UISignalFlag_Clicked = UISignalFlag_LeftClicked|UISignalFlag_KeyboardPressed, + UISignalFlag_DoubleClicked = UISignalFlag_LeftDoubleClicked, + UISignalFlag_TripleClicked = UISignalFlag_LeftTripleClicked, + UISignalFlag_Dragging = UISignalFlag_LeftDragging, +} UISignalFlags; typedef struct { - InteractionKind kind; - union { - void *generic; - Variable *var; - }; - Rect rect; -} Interaction; + UINode *node; + v2 scroll; + str8 string; + UISignalFlags flags; +} UISignal; + +typedef struct { + UINodeKey node_key; + UINodeKey next_node_key; + UINodeKey last_node_key; + + i16 cursor; + i16 mark; + i16 count; + i16 last_count; + b32 numeric; + b32 changed; + // TODO(rnp): animation key + BeamformerUIBlinker blinker; + u8 buffer[256]; + u8 last_buffer[256]; +} UITextInputState; + +typedef struct F32Node F32Node; +struct F32Node {F32Node *next; f32 v;}; + +typedef struct V4Node V4Node; +struct V4Node {V4Node *next; v4 v;}; + +#define UI_STACK_LIST \ + X(Axis2Node, child_layout_axis, Axis2, 0) \ + X(F32Node, font_size, f32, 0) \ + X(F32Node, border_thickness, f32, UI_BORDER_THICK) \ + X(F32Node, text_outline_thickness, f32, 0) \ + X(UINodeFlagsNode, flags, UINodeFlags, 0) \ + X(UIParentNode, parent, UINode *, (&ui_node_nil)) \ + X(UISizeNode, semantic_height, UISize, {0}) \ + X(UISizeNode, semantic_width, UISize, {0}) \ + X(UIAlignNode, alignment_y, UIAlign, 0) \ + X(UIAlignNode, alignment_x, UIAlign, 0) \ + X(UIAlignNode, text_alignment, UIAlign, UIAlign_Left) \ + X(V4Node, text_colour, v4, FG_COLOUR) \ + X(V4Node, text_outline_colour, v4, NIL_COLOUR) \ + X(V4Node, border_colour, v4, NIL_COLOUR) \ + X(V4Node, bg_colour, v4, NIL_COLOUR) \ -#define auto_interaction(r, v) (Interaction){.kind = InteractionKind_Auto, .var = v, .rect = r} -struct BeamformerUI { +typedef struct { + u64 current_frame_index; Arena arena; + v2 current_mouse; + v2 last_mouse; + u64 input_consumed[countof(((BeamformerInput *)0)->event_queue) / 64]; + static_assert(countof(((BeamformerInput *)0)->event_queue) % 64 == 0, ""); + Font font; Font small_font; - v2 last_mouse; - - Variable *regions; - Variable *variable_freelist; - - Variable floating_widget_sentinal; - BeamformerFrameView *view_first; BeamformerFrameView *view_last; BeamformerFrameView *view_freelist; - Interaction interaction; - Interaction hot_interaction; - Interaction next_interaction; - - InputState text_input_state; - VulkanHandle pipelines[BeamformerShaderKind_RenderCount]; OSHandle render_semaphores_export[2]; @@ -447,23 +373,58 @@ struct BeamformerUI { GPUImage render_3d_depth_image; RenderModel unit_cube_model; - v2_sll *scale_bar_savepoint_freelist; + BeamformerFrame latest_plane[BeamformerViewPlaneTag_Count]; - BeamformerFrame latest_plane[BeamformerViewPlaneTag_Count + 1]; - b32 latest_plane_valid[BeamformerViewPlaneTag_Count + 1]; - - BeamformerUIParameters params; - b32 flush_params; + BeamformerUIParameters parameters; + b32 flush_parameters; u32 selected_parameter_block; - v2 min_coordinate; - v2 max_coordinate; + // TODO(rnp): this should be per parameter block f32 off_axis_position; f32 beamform_plane; - BeamformerSharedMemory * shared_memory; - BeamformerCtx * beamformer_context; -}; + BeamformerUIPanel *tree; + BeamformerUIPanel *tree_node_freelist; + + // NOTE(rnp): context menu + UINode *context_menu_root; + UINodeKey context_menu_anchor_key; + UINodeKey context_menu_next_anchor_key; + BeamformerUIPanel *context_menu_panel; + BeamformerUIPanel *context_menu_next_panel; + f32 context_menu_open_t; + b32 context_menu_state_changed; + + // NOTE(rnp): drag info + UINodeKey drop_target_key; // alway a stable node + UINode *drop_target_node; // may point to a transient node + UINode *drag_root; + UINode *drag_overlay_root; + UINode *drag_overlay_edges_root; + UINode *drag_overlay_tab_root; + BeamformerUIPanel *drag_panel; + f32 drag_open_t; + b32 drag_end; + + // NOTE(rnp): User Interaction + UINodeKey hot_node_key; + UINodeKey active_node_key[UIMouseButtonKind_Count]; + // TODO(rnp): click timestamp history (double/triple press) + + // NOTE(rnp): Builder State + UINode *node_freelist; + UINode *root_node; + Arena build_arenas[2]; + TempArena build_arena_savepoints[2]; + // NOTE(rnp): Builder Stacks + #define X(type, name, ...) struct {type *top; type *free; u64 count;} name##_node_stack; + UI_STACK_LIST + #undef X + + UINodeHashBucket node_hash_table[UI_HASH_TABLE_COUNT]; + + UITextInputState text_input_state; +} BeamformerUI; typedef enum { TF_NONE = 0, @@ -489,486 +450,338 @@ typedef struct { TextFlags flags; } TextSpec; -typedef enum { - TRK_CELLS, - TRK_TABLE, -} TableRowKind; - -typedef enum { - TableCellKind_None, - TableCellKind_Variable, - TableCellKind_VariableGroup, -} TableCellKind; - -typedef struct { - s8 text; - union { - i64 integer; - Variable *var; - void *generic; - }; - TableCellKind kind; - f32 width; -} TableCell; - -typedef struct { - void *data; - TableRowKind kind; -} TableRow; - -typedef struct Table { - TableRow *data; - da_count count; - da_count capacity; - - /* NOTE(rnp): counted by columns */ - TextAlignment *alignment; - f32 *widths; - - v4 border_colour; - f32 column_border_thick; - f32 row_border_thick; - v2 size; - v2 cell_pad; - - /* NOTE(rnp): row count including nested tables */ - i32 rows; - i32 columns; - - struct Table *parent; -} Table; - -typedef struct { - Table *table; - i32 row_index; -} TableStackFrame; +global BeamformerUI *ui_context; +global BeamformerInput *beamformer_input; -typedef struct { - TableStackFrame *data; - da_count count; - da_count capacity; -} TableStack; +read_only global UINode ui_node_nil = { + .parent = &ui_node_nil, + .first_child = &ui_node_nil, + .last_child = &ui_node_nil, + .previous_sibling = &ui_node_nil, + .next_sibling = &ui_node_nil, +}; -typedef enum { - TIK_ROWS, - TIK_CELLS, -} TableIteratorKind; +#define X(type, name, _t, impl) read_only global type ui_##name##_node_nil = {.v = impl}; +UI_STACK_LIST +#undef X -typedef struct { - TableStack stack; - TableStackFrame frame; +#define ui_node_is_nil(n) ((n) == 0 || (n) == &ui_node_nil) +#define ui_build_arena() (ui_context->build_arenas + (ui_context->current_frame_index % countof(ui_context->build_arenas))) + +#define UIStackPushBody(name_upper, name_lower, type, new_value) \ + name_upper *node = SLLPop(ui_context->name_lower##_node_stack.free, next); \ + if (!node) node = push_struct_no_zero(ui_build_arena(), name_upper); \ + node->v = new_value; \ + type result = ui_context->name_lower##_node_stack.top->v; \ + SLLStackPush(ui_context->name_lower##_node_stack.top, node, next); \ + ui_context->name_lower##_node_stack.count++; \ + return result + +#define UIStackPopBody(name_upper, name_lower, type) \ + name_upper *node = ui_context->name_lower##_node_stack.top; \ + type result = node->v; \ + if (node != &ui_##name_lower##_node_nil) { \ + node = SLLPop(ui_context->name_lower##_node_stack.top, next); \ + SLLStackPush(ui_context->name_lower##_node_stack.free, node, next); \ + } \ + return result + +#define UIAlign(v) DeferLoop(ui_push_alignment(UIAlign_##v), ui_pop_alignment()) +#define UIAxisAlign(axis, v) DeferLoop(ui_push_axis_alignment(axis, UIAlign_##v), ui_pop_axis_alignment(axis)) +#define UIAxisSize(axis, v) DeferLoop(ui_push_axis_size(axis, v), ui_pop_axis_size(axis)) +#define UIBorderColour(v) DeferLoop(ui_push_border_colour(v), ui_pop_border_colour()) +#define UIBorderThickness(v) DeferLoop(ui_push_border_thickness(v), ui_pop_border_thickness()) +#define UIBGColour(v) DeferLoop(ui_push_bg_colour(v), ui_pop_bg_colour()) +#define UIChildLayoutAxis(v) DeferLoop(ui_push_child_layout_axis(v), ui_pop_child_layout_axis()) +#define UIFlags(v) DeferLoop(ui_push_flags(v), ui_pop_flags()) +#define UIFontSize(v) DeferLoop(ui_push_font_size(v), ui_pop_font_size()) +#define UIParent(v) DeferLoop(ui_push_parent(v), ui_pop_parent()) +#define UIPrefHeight(v) DeferLoop(ui_push_semantic_height(v), ui_pop_semantic_height()) +#define UIPrefWidth(v) DeferLoop(ui_push_semantic_width(v), ui_pop_semantic_width()) +#define UISize(v) DeferLoop(ui_push_size(v), ui_pop_size()) +#define UITextAlign(v) DeferLoop(ui_push_text_alignment(UIAlign_##v), ui_pop_text_alignment()) +#define UITextOutlineColour(v) DeferLoop(ui_push_text_outline_colour(v), ui_pop_text_outline_colour()) +#define UITextOutlineThickness(v) DeferLoop(ui_push_text_outline_thickness(v), ui_pop_text_outline_thickness()) +#define UITextColour(v) DeferLoop(ui_push_text_colour(v), ui_pop_text_colour()) + +#define UIScroll(axis) DeferLoop(ui_scroll_begin(axis), ui_scroll_end()) + +#define X(type, name, value_type, ...) \ + function value_type ui_push_##name(value_type v) {UIStackPushBody(type, name, value_type, v);} \ + function value_type ui_pop_##name(void) {UIStackPopBody(type, name, value_type);} \ + function value_type ui_top_##name(void) {return ui_context->name##_node_stack.top->v;} +UI_STACK_LIST +#undef X - TableRow *row; - i16 column; - i16 sub_table_depth; +#define ui_size(k, v, s) (UISize){.kind = UISizeKind_##k, .value = (v), .strictness = (s)} +#define ui_em(value, strictness) ui_size(Pixels, (value) * ui_top_font_size(), (strictness)) +#define ui_px(value, strictness) ui_size(Pixels, (value), (strictness)) +#define ui_pct(value, strictness) ui_size(PercentOfParent, (value), (strictness)) +#define ui_children_sum(strictness) ui_size(ChildrenSum, 0.f, (strictness)) +#define ui_text_dim(padding, strictness) ui_size(TextContent, (padding), (strictness)) - TableIteratorKind kind; +#define ui_node_key_zero() (UINodeKey){0} - f32 start_x; - TextAlignment alignment; - Rect cell_rect; -} TableIterator; +#define ui_spacer(flags) ui_build_node_from_key(flags, ui_node_key_zero()) +#define ui_padw(v) UIPrefWidth(ui_px(v, 1.f)) ui_spacer(0) +#define ui_padh(v) UIPrefHeight(ui_px(v, 1.f)) ui_spacer(0) +#define ui_pads(v) UISize(ui_px(v, 1.f)) ui_spacer(0) -function Vector2 -rl_v2(v2 a) -{ - Vector2 result = {a.x, a.y}; - return result; -} +#define ui_dragging(s) (!!(s.flags & UISignalFlag_Dragging)) +#define ui_released(s) (!!(s.flags & UISignalFlag_Released)) +#define ui_pressed(s) (!!(s.flags & UISignalFlag_Pressed)) +#define ui_scrolled(s) (!!(s.flags & UISignalFlag_Scrolled)) -function Rectangle -rl_rect(Rect a) +function UIAlign +ui_push_axis_alignment(Axis2 axis, UIAlign v) { - Rectangle result = {a.pos.x, a.pos.y, a.size.w, a.size.h}; + UIAlign result = 0; + switch (axis) { + case Axis2_X:{result = ui_push_alignment_x(v);}break; + case Axis2_Y:{result = ui_push_alignment_y(v);}break; + InvalidDefaultCase; + } return result; } -function BeamformerViewPlaneTag -ui_plane_layout_from_normal(v3 normal) +function UIAlign +ui_pop_axis_alignment(Axis2 axis) { - BeamformerViewPlaneTag result = BeamformerViewPlaneTag_Arbitrary; - b32 has_x = !f32_equal(normal.x, 0.0f); - b32 has_y = !f32_equal(normal.y, 0.0f); - b32 has_z = !f32_equal(normal.z, 0.0f); - if ((has_x + has_y + has_z) == 1) { - if (has_x) result = BeamformerViewPlaneTag_YZ; - if (has_y) result = BeamformerViewPlaneTag_XZ; - if (has_z) result = BeamformerViewPlaneTag_XY; - assert(result != BeamformerViewPlaneTag_Arbitrary); + UIAlign result = 0; + switch (axis) { + case Axis2_X:{result = ui_pop_alignment_x();}break; + case Axis2_Y:{result = ui_pop_alignment_y();}break; + InvalidDefaultCase; } return result; } -function f32 -ui_blinker_update(UIBlinker *b, f32 scale) +function UIAlign +ui_push_alignment(UIAlign v) { - b->t += b->scale * dt_for_frame; - if (b->t >= 1.0f) b->scale = -scale; - if (b->t <= 0.0f) b->scale = scale; - f32 result = b->t; + UIAlign result = ui_push_axis_alignment(ui_top_child_layout_axis(), v); return result; } -function v2 -measure_glyph(Font font, u32 glyph) +function UIAlign +ui_pop_alignment(void) { - assert(glyph >= 0x20); - v2 result = {.y = (f32)font.baseSize}; - /* NOTE: assumes font glyphs are ordered ASCII */ - result.x = (f32)font.glyphs[glyph - 0x20].advanceX; - if (result.x == 0) - result.x = (font.recs[glyph - 0x20].width + (f32)font.glyphs[glyph - 0x20].offsetX); + UIAlign result = ui_pop_axis_alignment(ui_top_child_layout_axis()); return result; } -function v2 -measure_text(Font font, s8 text) +function UISize +ui_push_axis_size(Axis2 axis, UISize v) { - v2 result = {.y = (f32)font.baseSize}; - for (iz i = 0; i < text.len; i++) - result.x += measure_glyph(font, text.data[i]).x; + UISize result = {0}; + switch (axis) { + case Axis2_X:{result = ui_push_semantic_width(v); }break; + case Axis2_Y:{result = ui_push_semantic_height(v);}break; + InvalidDefaultCase; + } return result; } -function s8 -clamp_text_to_width(Font font, s8 text, f32 limit) +function UISize +ui_pop_axis_size(Axis2 axis) { - s8 result = text; - f32 width = 0; - for (iz i = 0; i < text.len; i++) { - f32 next = measure_glyph(font, text.data[i]).w; - if (width + next > limit) { - result.len = i; - break; - } - width += next; + UISize result = {0}; + switch (axis) { + case Axis2_X:{result = ui_pop_semantic_width(); }break; + case Axis2_Y:{result = ui_pop_semantic_height();}break; + InvalidDefaultCase; } return result; } -function v2 -align_text_in_rect(s8 text, Rect r, Font font) +function UISize +ui_push_size(UISize v) { - v2 size = measure_text(font, text); - v2 pos = v2_add(r.pos, v2_scale(v2_sub(r.size, size), 0.5)); - v2 result = clamp_v2_rect(pos, r); + UISize result = ui_push_axis_size(ui_top_child_layout_axis(), v); return result; } -function Texture -make_raylib_texture(BeamformerFrameView *v) +function UISize +ui_pop_size(void) { - Texture result; - result.id = v->texture; - result.width = v->colour_image.width; - result.height = v->colour_image.height; - result.mipmaps = v->colour_image.mip_map_levels; - result.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; + UISize result = ui_pop_axis_size(ui_top_child_layout_axis()); return result; } -function void -stream_append_variable(Stream *s, Variable *var) +#define ui_node_key_nil(k) (ui_node_key_equal((k), ui_node_key_zero())) +#define ui_node_hot(n) (ui_node_key_equal((n)->key, ui_context->hot_node_key)) +function b32 +ui_node_key_equal(UINodeKey a, UINodeKey b) { - switch (var->type) { - case VT_UI_BUTTON: - case VT_GROUP:{ stream_append_s8(s, var->name); }break; - case VT_F32:{ stream_append_f64(s, var->real32, 100); }break; - case VT_B32:{ stream_append_s8(s, var->bool32 ? s8("True") : s8("False")); }break; - case VT_SCALED_F32:{ stream_append_f64(s, var->scaled_real32.val, 100); }break; - case VT_BEAMFORMER_VARIABLE:{ - BeamformerVariable *bv = &var->beamformer_variable; - stream_append_f64(s, *bv->store * bv->display_scale, 100); - }break; - case VT_CYCLER:{ - u32 index = *var->cycler.state; - if (var->cycler.labels) stream_append_s8(s, var->cycler.labels[index]); - else stream_append_u64(s, index); - }break; - case VT_LIVE_CONTROLS_STRING:{ - BeamformerLiveImagingParameters *lip = var->generic; - stream_append_s8(s, (s8){.data = (u8 *)lip->save_name_tag, .len = lip->save_name_tag_length}); - if (lip->save_name_tag_length <= 0) stream_append_s8(s, s8("Tag...")); - }break; - InvalidDefaultCase; - } + b32 result = a.value == b.value; + return result; } -function void -stream_append_variable_group(Stream *s, Variable *var) +function UINodeKey +ui_node_ancestor_key(void) { - switch (var->type) { - case VT_GROUP:{ - switch (var->group.kind) { - case VariableGroupKind_Vector:{ - Variable *v = var->group.first; - stream_append_s8(s, s8("{")); - while (v) { - stream_append_variable(s, v); - v = v->next; - if (v) stream_append_s8(s, s8(", ")); - } - stream_append_s8(s, s8("}")); - }break; - InvalidDefaultCase; - } - }break; - InvalidDefaultCase; - } + UINode *node = ui_top_parent(); + while (!ui_node_is_nil(node) && ui_node_key_equal(node->key, ui_node_key_zero())) + node = node->parent; + UINodeKey result = node->key; + return result; } -function s8 -push_acquisition_kind(Stream *s, BeamformerAcquisitionKind kind, u32 transmit_count, BeamformerContrastMode contrast_mode) +function Rect +ui_node_rect(UINode *node) { - s8 name = beamformer_acquisition_kind_strings[kind]; - b32 fixed_transmits = beamformer_acquisition_kind_has_fixed_transmits[kind]; - if (kind >= BeamformerAcquisitionKind_Count || kind < 0) { - fixed_transmits = 0; - name = s8("Invalid"); - } - - stream_append_s8(s, name); - if (!fixed_transmits) { - stream_append_byte(s, '-'); - stream_append_u64(s, transmit_count); - } - - if (contrast_mode != BeamformerContrastMode_None) - stream_append_s8s(s, s8(" ("), s8_from_str8(beamformer_contrast_mode_strings[contrast_mode]), s8(")")); - - return stream_to_s8(s); + Rect result = {0}; + result.size = (v2){{node->computed_size[0], node->computed_size[1]}}; + result.pos = (v2){{node->computed_position[0], node->computed_position[1]}}; + return result; } -function s8 -push_custom_view_title(Stream *s, Variable *var) +function f32 +ui_alignment_correction(UIAlign alignment, f32 delta) { - switch (var->type) { - case VT_COMPUTE_STATS_VIEW:{ - stream_append_s8(s, s8("Compute Stats: ")); - stream_append_variable(s, var->compute_stats_view.cycler); - }break; - case VT_COMPUTE_PROGRESS_BAR:{ - stream_append_s8(s, s8("Compute Progress: ")); - stream_append_f64(s, 100 * *var->compute_progress_bar.progress, 100); - stream_append_byte(s, '%'); - } break; - case VT_BEAMFORMER_FRAME_VIEW:{ - BeamformerFrameView *bv = var->generic; - stream_append_s8(s, s8("Frame View")); - switch (bv->kind) { - case BeamformerFrameViewKind_Copy:{ stream_append_s8(s, s8(": Copy [")); }break; - case BeamformerFrameViewKind_Latest:{ - #define X(plane, id, pretty) s8_comp(": " pretty " ["), - read_only local_persist s8 labels[BeamformerViewPlaneTag_Count + 1] = { - BEAMFORMER_VIEW_PLANE_TAG_LIST - s8_comp(": Live [") - }; - #undef X - stream_append_s8(s, labels[*bv->cycler->cycler.state % (BeamformerViewPlaneTag_Count + 1)]); - }break; - case BeamformerFrameViewKind_3DXPlane:{ stream_append_s8(s, s8(": 3D X-Plane")); }break; - InvalidDefaultCase; - } - if (bv->kind != BeamformerFrameViewKind_3DXPlane) { - stream_append_hex_u64(s, bv->frame.id); - stream_append_byte(s, ']'); - } - }break; + f32 result = 0; + switch (alignment) { InvalidDefaultCase; + case UIAlign_Left:{ result = 0; }break; + case UIAlign_Center:{result = 0.5f * delta;}break; + case UIAlign_Right:{ result = delta; }break; } - return stream_to_s8(s); + return result; } -#define table_new(a, init, ...) table_new_(a, init, arg_list(TextAlignment, ##__VA_ARGS__)) -function Table * -table_new_(Arena *a, i32 initial_capacity, TextAlignment *alignment, i32 columns) +function v2 +ui_node_text_position(UINode *node) { - Table *result = push_struct(a, Table); - da_reserve(a, result, initial_capacity); - result->columns = columns; - result->alignment = push_array(a, TextAlignment, columns); - result->widths = push_array(a, f32, columns); - result->cell_pad = (v2){{TABLE_CELL_PAD_WIDTH, TABLE_CELL_PAD_HEIGHT}}; - mem_copy(result->alignment, alignment, sizeof(*alignment) * (u32)columns); + Rect r = ui_node_rect(node); + v2 result = r.pos; + result.x += ui_alignment_correction(node->text_alignment, r.size.x - node->text_size.x); + result.y += (r.size.y - node->text_size.y) / 2.f; return result; } -function i32 -table_skip_rows(Table *t, f32 draw_height, f32 text_height) +function void +ui_disable_cursor(void) { - i32 max_rows = (i32)(draw_height / (text_height + t->cell_pad.h)); - i32 result = t->rows - MIN(t->rows, max_rows); - return result; + HideCursor(); + DisableCursor(); + /* wtf raylib */ + SetMousePosition((i32)ui_context->current_mouse.x, (i32)ui_context->current_mouse.y); } -function TableIterator * -table_iterator_new(Table *table, TableIteratorKind kind, Arena *a, i32 starting_row, v2 at, Font *font) +function void +ui_enable_cursor(void) { - TableIterator *result = push_struct(a, TableIterator); - result->kind = kind; - result->frame.table = table; - result->frame.row_index = starting_row; - result->start_x = at.x; - result->cell_rect.size.h = (f32)font->baseSize; - result->cell_rect.pos = v2_add(at, v2_scale(table->cell_pad, 0.5f)); - result->cell_rect.pos.y += (f32)(starting_row - 1) * (result->cell_rect.size.h + table->cell_pad.h + table->row_border_thick); - da_reserve(a, &result->stack, 4); - return result; + EnableCursor(); } -function void * -table_iterator_next(TableIterator *it, Arena *a) +function Vector2 +rl_v2(v2 a) { - void *result = 0; - - if (!it->row || it->kind == TIK_ROWS) { - for (;;) { - TableRow *row = it->frame.table->data + it->frame.row_index++; - if (it->frame.row_index <= it->frame.table->count) { - if (row->kind == TRK_TABLE) { - *da_push(a, &it->stack) = it->frame; - it->frame = (TableStackFrame){.table = row->data}; - it->sub_table_depth++; - } else { - result = row; - break; - } - } else if (it->stack.count) { - it->frame = it->stack.data[--it->stack.count]; - it->sub_table_depth--; - } else { - break; - } - } - Table *t = it->frame.table; - it->row = result; - it->column = 0; - it->cell_rect.pos.x = it->start_x + t->cell_pad.w / 2 + - it->cell_rect.size.h * it->sub_table_depth; - it->cell_rect.pos.y += it->cell_rect.size.h + t->row_border_thick + t->cell_pad.h; - } - - if (it->row && it->kind == TIK_CELLS) { - Table *t = it->frame.table; - i32 column = it->column++; - it->cell_rect.pos.x += column > 0 ? it->cell_rect.size.w + t->cell_pad.w : 0; - it->cell_rect.size.w = t->widths[column]; - it->alignment = t->alignment[column]; - result = (TableCell *)it->row->data + column; - - if (it->column == t->columns) - it->row = 0; - } + Vector2 result = {a.x, a.y}; + return result; +} +function Rectangle +rl_rect(Rect a) +{ + Rectangle result = {a.pos.x, a.pos.y, a.size.w, a.size.h}; return result; } function f32 -table_width(Table *t) +beamformer_ui_blinker_update(BeamformerUIBlinker *b, f32 scale) { - f32 result = 0; - i32 valid = 0; - for (i32 i = 0; i < t->columns; i++) { - result += t->widths[i]; - if (t->widths[i] > 0) valid++; - } - result += t->cell_pad.w * (f32)valid; - result += MAX(0, ((f32)valid - 1)) * t->column_border_thick; + b->t += b->scale * dt_for_frame; + if (b->t >= 1.0f) b->scale = -scale; + if (b->t <= 0.0f) b->scale = scale; + f32 result = b->t; return result; } function v2 -table_extent(Table *t, Arena arena, Font *font) +measure_glyph(Font font, u32 glyph) { - TableIterator *it = table_iterator_new(t, TIK_ROWS, &arena, 0, (v2){0}, font); - for (TableRow *row = table_iterator_next(it, &arena); - row; - row = table_iterator_next(it, &arena)) - { - for (i32 i = 0; i < it->frame.table->columns; i++) { - TableCell *cell = (TableCell *)row->data + i; - if (!cell->text.len && cell->var && cell->var->flags & V_RADIO_BUTTON) { - cell->width = (f32)font->baseSize; - } else { - cell->width = measure_text(*font, cell->text).w; - } - it->frame.table->widths[i] = MAX(cell->width, it->frame.table->widths[i]); - } - } - - t->size = (v2){.x = table_width(t), .y = it->cell_rect.pos.y - t->cell_pad.h / 2}; - v2 result = t->size; + assert(glyph >= 0x20); + v2 result = {.y = (f32)font.baseSize}; + /* NOTE: assumes font glyphs are ordered ASCII */ + result.x = (f32)font.glyphs[glyph - 0x20].advanceX; + if (result.x == 0) + result.x = (font.recs[glyph - 0x20].width + (f32)font.glyphs[glyph - 0x20].offsetX); return result; } function v2 -table_cell_align(TableCell *cell, TextAlignment align, Rect r) +measure_text_tight(Font font, str8 text) { - v2 result = r.pos; - if (r.size.w >= cell->width) { - switch (align) { - case TextAlignment_Left:{}break; - case TextAlignment_Right:{ result.x += (r.size.w - cell->width); }break; - case TextAlignment_Center:{ result.x += (r.size.w - cell->width) / 2; }break; - } + v2 result = {0}; + for (i64 i = 0; i < text.length; i++) { + assert(text.data[i] >= 0x20); + u8 glyph = text.data[i] - 0x20; + result.x += font.recs[glyph].width; + result.y = Max(font.recs[glyph].height, result.y); } return result; } -function TableCell -table_variable_cell(Arena *a, Variable *var) +function v2 +measure_text(Font font, str8 text) { - TableCell result = {.var = var, .kind = TableCellKind_Variable}; - if ((var->flags & V_RADIO_BUTTON) == 0) { - Stream text = arena_stream(*a); - stream_append_variable(&text, var); - result.text = arena_stream_commit(a, &text); - } + v2 result = {.y = (f32)font.baseSize}; + for (i64 i = 0; i < text.length; i++) + result.x += measure_glyph(font, text.data[i]).x; return result; } -function TableRow * -table_push_row(Table *t, Arena *a, TableRowKind kind) +function str8 +clamp_text_to_width(Font font, str8 text, f32 limit) { - TableRow *result = da_push(a, t); - if (kind == TRK_CELLS) { - result->data = push_array(a, TableCell, t->columns); - /* NOTE(rnp): do not increase rows for an empty subtable */ - t->rows++; + str8 result = text; + f32 width = 0; + for (i64 i = 0; i < text.length; i++) { + f32 next = measure_glyph(font, text.data[i]).w; + if (width + next > limit) { + result.length = i; + break; + } + width += next; } - result->kind = kind; return result; } -function TableRow * -table_push_parameter_row(Table *t, Arena *a, s8 label, Variable *var, s8 suffix) +function Texture +make_raylib_texture(BeamformerFrameView *v) { - assert(t->columns >= 3); - TableRow *result = table_push_row(t, a, TRK_CELLS); - TableCell *cells = result->data; - - cells[0].text = label; - cells[1] = table_variable_cell(a, var); - cells[2].text = suffix; - + Texture result; + result.id = v->texture; + result.width = v->colour_image.width; + result.height = v->colour_image.height; + result.mipmaps = v->colour_image.mip_map_levels; + result.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; return result; } -#define table_begin_subtable(t, a, ...) table_begin_subtable_(t, a, arg_list(TextAlignment, ##__VA_ARGS__)) -function Table * -table_begin_subtable_(Table *table, Arena *a, TextAlignment *alignment, i32 columns) +function str8 +push_acquisition_kind(Arena *arena, BeamformerAcquisitionKind kind, u32 transmit_count, BeamformerContrastMode contrast_mode) { - TableRow *row = table_push_row(table, a, TRK_TABLE); - Table *result = row->data = table_new_(a, 0, alignment, columns); - result->parent = table; - return result; -} + str8 name = str8("Invalid"); + b32 fixed_transmits = 0; + if Between(kind, 0, BeamformerAcquisitionKind_Count - 1) { + name = beamformer_acquisition_kind_strings[kind]; + fixed_transmits = beamformer_acquisition_kind_has_fixed_transmits[kind]; + } -function Table * -table_end_subtable(Table *table) -{ - Table *result = table->parent ? table->parent : table; - return result; + Stream sb = arena_stream(*arena); + stream_append_s8(&sb, s8_from_str8(name)); + if (!fixed_transmits) { + stream_append_byte(&sb, '-'); + stream_append_u64(&sb, transmit_count); + } + + if (contrast_mode != BeamformerContrastMode_None) + stream_append_s8s(&sb, s8(" ("), s8_from_str8(beamformer_contrast_mode_strings[contrast_mode]), s8(")")); + + s8 result = arena_stream_commit(arena, &sb); + return str8_from_s8(result); } function void @@ -1017,600 +830,97 @@ resize_frame_view(BeamformerFrameView *view, uv2 dim) } function void -ui_beamformer_frame_view_release_subresources(BeamformerUI *ui, BeamformerFrameView *bv, BeamformerFrameViewKind kind) +beamformer_ui_frame_view_release_subresources(BeamformerFrameView *bv, BeamformerFrameViewKind kind) { if (kind == BeamformerFrameViewKind_Copy) vk_buffer_release(&bv->copy_buffer); - - if (kind != BeamformerFrameViewKind_3DXPlane) { - if (bv->axial_scale_bar.scale_bar.savepoint_stack) - SLLPushFreelist(bv->axial_scale_bar.scale_bar.savepoint_stack, ui->scale_bar_savepoint_freelist); - if (bv->lateral_scale_bar.scale_bar.savepoint_stack) - SLLPushFreelist(bv->lateral_scale_bar.scale_bar.savepoint_stack, ui->scale_bar_savepoint_freelist); - } } function void -ui_variable_free(BeamformerUI *ui, Variable *var) +beamformer_ui_frame_view_copy_frame(BeamformerFrameView *new, BeamformerFrameView *old) { - if (var) { - var->parent = 0; - while (var) { - if (var->type == VT_GROUP) { - var = var->group.first; - } else { - if (var->type == VT_BEAMFORMER_FRAME_VIEW) { - /* TODO(rnp): instead there should be a way of linking these up */ - BeamformerFrameView *bv = var->generic; - ui_beamformer_frame_view_release_subresources(ui, bv, bv->kind); - DLLRemove(0, ui->view_first, ui->view_last, bv, next, prev); - SLLPushFreelist(bv, ui->view_freelist); - } + memory_copy(&new->frame, &old->frame, sizeof(old->frame)); - Variable *dead = var; - if (var->next) { - var = var->next; - } else { - var = var->parent; - /* NOTE(rnp): when we assign parent here we have already - * released the children. Assign type so we don't loop */ - if (var) var->type = VT_NULL; - } - SLLPushFreelist(dead, ui->variable_freelist); - } - } - } -} - -function void -ui_variable_free_group_items(BeamformerUI *ui, Variable *group) -{ - assert(group->type == VT_GROUP); - /* NOTE(rnp): prevent traversal back to us */ - group->group.last->parent = 0; - ui_variable_free(ui, group->group.first); - group->group.first = group->group.last = 0; -} + iv3 points = new->frame.points; + i64 frame_size = points.x * points.y * points.z * beamformer_data_kind_byte_size[new->frame.data_kind]; -function void -ui_view_free(BeamformerUI *ui, Variable *view) -{ - assert(view->type == VT_UI_VIEW); - ui_variable_free(ui, view->view.child); - ui_variable_free(ui, view->view.close); - ui_variable_free(ui, view->view.menu); - ui_variable_free(ui, view); -} + Stream sb = arena_stream(ui_context->arena); + stream_append_s8(&sb, s8("Frame Copy [")); + stream_append_hex_u64(&sb, new->frame.id); + stream_append_s8(&sb, s8("]")); + stream_append_byte(&sb, 0); -function Variable * -fill_variable(Variable *var, Variable *group, s8 name, u32 flags, VariableType type, Font font) -{ - var->flags = flags; - var->type = type; - var->name = name; - var->parent = group; - var->name_width = measure_text(font, name).x; - - if (group && group->type == VT_GROUP) { - if (group->group.last) group->group.last = group->group.last->next = var; - else group->group.last = group->group.first = var; - } + GPUBufferAllocateInfo allocate_info = { + .size = frame_size, + .flags = VulkanUsageFlag_TransferDestination, + .label = stream_to_str8(&sb), + }; + vk_buffer_allocate(&new->copy_buffer, &allocate_info); - return var; + GPUBuffer *backlog = beamformer_context->compute_context.backlog.buffer; + VulkanHandle cmd = vk_command_begin(VulkanTimeline_Compute); + vk_command_wait_timeline(cmd, VulkanTimeline_Compute, old->frame.timeline_valid_value); + vk_command_copy_buffer(cmd, &new->copy_buffer, backlog, old->frame.buffer_offset, frame_size); + new->frame.timeline_valid_value = vk_command_end(cmd, (VulkanHandle){0}, (VulkanHandle){0}); } -function Variable * -add_variable(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, u32 flags, - VariableType type, Font font) +function BeamformerFrameView * +beamformer_ui_frame_view_new(BeamformerFrameViewKind kind) { - Variable *result = SLLPopFreelist(ui->variable_freelist); - if (!result) result = push_struct_no_zero(arena, Variable); + BeamformerFrameView *old = (BeamformerFrameView *)beamformer_registers()->frame_view; + BeamformerFrameView *result = SLLPopFreelist(ui_context->view_freelist); + if (!result) result = push_struct_no_zero(&ui_context->arena, typeof(*result)); zero_struct(result); - return fill_variable(result, group, name, flags, type, font); -} - -function Variable * -add_variable_group(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, VariableGroupKind kind, Font font) -{ - Variable *result = add_variable(ui, group, arena, name, V_INPUT, VT_GROUP, font); - result->group.kind = kind; - return result; -} - -function Variable * -end_variable_group(Variable *group) -{ - assert(group->type == VT_GROUP); - return group->parent; -} - -function void -fill_variable_cycler(Variable *cycler, u32 *store, s8 *labels, u32 cycle_count) -{ - cycler->cycler.cycle_length = cycle_count; - cycler->cycler.state = store; - cycler->cycler.labels = labels; -} - -function Variable * -add_variable_cycler(BeamformerUI *ui, Variable *group, Arena *arena, u32 flags, Font font, s8 name, - u32 *store, s8 *labels, u32 cycle_count) -{ - Variable *result = add_variable(ui, group, arena, name, V_INPUT|flags, VT_CYCLER, font); - fill_variable_cycler(result, store, labels, cycle_count); - return result; -} - -function Variable * -add_button(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, UIButtonID id, - u32 flags, Font font) -{ - Variable *result = add_variable(ui, group, arena, name, V_INPUT|flags, VT_UI_BUTTON, font); - result->button = id; - return result; -} - -function Variable * -add_ui_split(BeamformerUI *ui, Variable *parent, Arena *arena, s8 name, f32 fraction, - RegionSplitDirection direction, Font font) -{ - Variable *result = add_variable(ui, parent, arena, name, V_HIDES_CURSOR, VT_UI_REGION_SPLIT, font); - result->region_split.direction = direction; - result->region_split.fraction = fraction; - return result; -} - -function Variable * -add_global_menu_to_group(BeamformerUI *ui, Arena *arena, Variable *group) -{ - #define X(id, text) add_button(ui, group, arena, s8(text), UI_BID_ ##id, 0, ui->small_font); - GLOBAL_MENU_BUTTONS - #undef X - return group; -} - -function Variable * -add_global_menu(BeamformerUI *ui, Arena *arena, Variable *parent) -{ - Variable *result = add_variable_group(ui, 0, arena, s8(""), VariableGroupKind_List, ui->small_font); - result->parent = parent; - return add_global_menu_to_group(ui, arena, result); -} - -function Variable * -add_ui_view(BeamformerUI *ui, Variable *parent, Arena *arena, s8 name, u32 view_flags, b32 menu, b32 closable) -{ - Variable *result = add_variable(ui, parent, arena, name, 0, VT_UI_VIEW, ui->small_font); - UIView *view = &result->view; - view->flags = view_flags; - if (menu) view->menu = add_global_menu(ui, arena, result); - if (closable) { - view->close = add_button(ui, 0, arena, s8(""), UI_BID_VIEW_CLOSE, 0, ui->small_font); - /* NOTE(rnp): we do this explicitly so that close doesn't end up in the view group */ - view->close->parent = result; - } - return result; -} - -function Variable * -add_floating_view(BeamformerUI *ui, Arena *arena, VariableType type, v2 at, Variable *child, b32 closable) -{ - Variable *result = add_ui_view(ui, 0, arena, s8(""), UIViewFlag_Floating, 0, closable); - result->type = type; - result->view.rect.pos = at; - result->view.child = child; - - result->parent = &ui->floating_widget_sentinal; - result->next = ui->floating_widget_sentinal.next; - result->next->parent = result; - ui->floating_widget_sentinal.next = result; - return result; -} - -function void -fill_beamformer_variable(Variable *var, s8 suffix, f32 *store, v2 limits, f32 display_scale, f32 scroll_scale) -{ - BeamformerVariable *bv = &var->beamformer_variable; - bv->suffix = suffix; - bv->store = store; - bv->display_scale = display_scale; - bv->scroll_scale = scroll_scale; - bv->limits = limits; -} - -function void -add_beamformer_variable(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, s8 suffix, f32 *store, - v2 limits, f32 display_scale, f32 scroll_scale, u32 flags, Font font) -{ - Variable *var = add_variable(ui, group, arena, name, flags, VT_BEAMFORMER_VARIABLE, font); - fill_beamformer_variable(var, suffix, store, limits, display_scale, scroll_scale); -} - -function Variable * -add_beamformer_parameters_view(Variable *parent, BeamformerCtx *ctx) -{ - BeamformerUI *ui = ctx->ui; - BeamformerUIParameters *bp = &ui->params; - - v2 v2_inf = {.x = -F32_INFINITY, .y = F32_INFINITY}; - - /* TODO(rnp): this can be closable once we have a way of opening new views */ - Variable *result = add_ui_view(ui, parent, &ui->arena, s8("Parameters"), 0, 1, 0); - Variable *group = result->view.child = add_variable(ui, result, &ui->arena, s8(""), 0, - VT_GROUP, ui->font); - - add_beamformer_variable(ui, group, &ui->arena, s8("Sampling Frequency:"), s8("[MHz]"), - &bp->sampling_frequency, (v2){0}, 1e-6f, 0, 0, ui->font); - - add_beamformer_variable(ui, group, &ui->arena, s8("Demodulation Frequency:"), s8("[MHz]"), - &bp->demodulation_frequency, (v2){.y = 100e6f}, 1e-6f, 0, 0, ui->font); - - add_beamformer_variable(ui, group, &ui->arena, s8("Speed of Sound:"), s8("[m/s]"), - &bp->speed_of_sound, (v2){.y = 1e6f}, 1.0f, 10.0f, - V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); - - group = add_variable_group(ui, group, &ui->arena, s8("Lateral Extent:"), - VariableGroupKind_Vector, ui->font); - { - add_beamformer_variable(ui, group, &ui->arena, s8("Min:"), s8("[mm]"), - &ui->min_coordinate.x, v2_inf, 1e3f, 0.5e-3f, - V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); - - add_beamformer_variable(ui, group, &ui->arena, s8("Max:"), s8("[mm]"), - &ui->max_coordinate.x, v2_inf, 1e3f, 0.5e-3f, - V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); - } - group = end_variable_group(group); - - group = add_variable_group(ui, group, &ui->arena, s8("Axial Extent:"), - VariableGroupKind_Vector, ui->font); - { - add_beamformer_variable(ui, group, &ui->arena, s8("Min:"), s8("[mm]"), - &ui->min_coordinate.y, v2_inf, 1e3f, 0.5e-3f, - V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); - - add_beamformer_variable(ui, group, &ui->arena, s8("Max:"), s8("[mm]"), - &ui->max_coordinate.y, v2_inf, 1e3f, 0.5e-3f, - V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); - } - group = end_variable_group(group); - - add_beamformer_variable(ui, group, &ui->arena, s8("Off Axis Position:"), s8("[mm]"), - &ui->off_axis_position, v2_inf, 1e3f, 0.5e-3f, - V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); - - add_beamformer_variable(ui, group, &ui->arena, s8("Beamform Plane:"), s8(""), - &ui->beamform_plane, (v2){{0, 1.0f}}, 1.0f, 0.025f, - V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); - - add_beamformer_variable(ui, group, &ui->arena, s8("F#:"), s8(""), &bp->f_number, (v2){.y = 1e3f}, - 1, 0.1f, V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); - - add_variable_cycler(ui, group, &ui->arena, V_CAUSES_COMPUTE, ui->font, s8("Interpolation:"), - &bp->interpolation_mode, beamformer_interpolation_mode_strings, - countof(beamformer_interpolation_mode_strings)); + DLLInsertLast(0, ui_context->view_first, ui_context->view_last, result, next, prev); - read_only local_persist s8 true_false_labels[] = {s8_comp("False"), s8_comp("True")}; - add_variable_cycler(ui, group, &ui->arena, V_CAUSES_COMPUTE, ui->font, s8("Coherency Weighting:"), - &bp->coherency_weighting, true_false_labels, countof(true_false_labels)); + result->export_handle.value[0] = OSInvalidHandleValue; - return result; -} + result->kind = kind; + result->dirty = 1; -function void -ui_beamformer_frame_view_convert(BeamformerUI *ui, Arena *arena, Variable *view, Variable *menu, - BeamformerFrameViewKind kind, BeamformerFrameView *old, b32 log_scale) -{ - assert(menu->group.first == menu->group.last && menu->group.first == 0); - assert(view->type == VT_BEAMFORMER_FRAME_VIEW); - - BeamformerFrameView *bv = view->generic; - bv->kind = kind; - bv->dirty = 1; - - fill_variable(&bv->dynamic_range, view, s8("Dynamic Range:"), V_INPUT|V_TEXT|V_UPDATE_VIEW, - VT_F32, ui->small_font); - fill_variable(&bv->threshold, view, s8("Threshold:"), V_INPUT|V_TEXT|V_UPDATE_VIEW, - VT_F32, ui->small_font); - fill_variable(&bv->gamma, view, s8("Gamma:"), V_INPUT|V_TEXT|V_UPDATE_VIEW, - VT_SCALED_F32, ui->small_font); - - bv->dynamic_range.real32 = old? old->dynamic_range.real32 : 50.0f; - bv->threshold.real32 = old? old->threshold.real32 : 55.0f; - bv->gamma.scaled_real32.val = old? old->gamma.scaled_real32.val : 1.0f; - bv->gamma.scaled_real32.scale = old? old->gamma.scaled_real32.scale : 0.05f; - bv->min_coordinate = old ? m4_mul_v4(old->frame.voxel_transform, (v4){{0.0f, 0.0f, 0.0f, 1.0f}}).xyz - : (v3){0}; - bv->max_coordinate = old ? m4_mul_v4(old->frame.voxel_transform, (v4){{1.0f, 1.0f, 1.0f, 1.0f}}).xyz - : (v3){0}; - - #define X(_t, pretty) s8_comp(pretty), - read_only local_persist s8 kind_labels[] = {BEAMFORMER_FRAME_VIEW_KIND_LIST}; - #undef X - bv->kind_cycler = add_variable_cycler(ui, menu, arena, V_EXTRA_ACTION, ui->small_font, - s8("Kind:"), (u32 *)&bv->kind, kind_labels, countof(kind_labels)); + result->log_scale = old? old->log_scale : 0; + result->dynamic_range = old? old->dynamic_range : 50.0f; + result->threshold = old? old->threshold : 55.0f; + result->gamma = old? old->gamma : 1.0f; /* TODO(rnp): this is quite dumb. what we actually want is to render directly * into the view region with the appropriate size for that region (scissor) */ - resize_frame_view(bv, (uv2){{FRAME_VIEW_RENDER_TARGET_SIZE}}); + resize_frame_view(result, (uv2){{FRAME_VIEW_RENDER_TARGET_SIZE}}); switch (kind) { - case BeamformerFrameViewKind_3DXPlane:{ - view->flags |= V_HIDES_CURSOR; - glTextureParameteri(bv->texture, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTextureParameteri(bv->texture, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - fill_variable(bv->x_plane_shifts + 0, view, s8("XZ Shift"), V_INPUT|V_HIDES_CURSOR, - VT_X_PLANE_SHIFT, ui->small_font); - fill_variable(bv->x_plane_shifts + 1, view, s8("YZ Shift"), V_INPUT|V_HIDES_CURSOR, - VT_X_PLANE_SHIFT, ui->small_font); - bv->demo = add_variable(ui, menu, arena, s8("Demo Mode"), V_INPUT|V_RADIO_BUTTON, VT_B32, ui->small_font); - }break; default:{ - view->flags &= ~(u32)V_HIDES_CURSOR; - fill_variable(&bv->lateral_scale_bar, view, s8(""), V_INPUT, VT_SCALE_BAR, ui->small_font); - fill_variable(&bv->axial_scale_bar, view, s8(""), V_INPUT, VT_SCALE_BAR, ui->small_font); - ScaleBar *lateral = &bv->lateral_scale_bar.scale_bar; - ScaleBar *axial = &bv->axial_scale_bar.scale_bar; - lateral->direction = SB_LATERAL; - axial->direction = SB_AXIAL; - lateral->scroll_scale = (v2){{-0.5e-3f, 0.5e-3f}}; - axial->scroll_scale = (v2){{ 0, 1.0e-3f}}; - lateral->zoom_starting_coord = F32_INFINITY; - axial->zoom_starting_coord = F32_INFINITY; - b32 copy = kind == BeamformerFrameViewKind_Copy; - v3 N = (v3){.y = 1.0f}; - if (old) N = cross(old->frame.voxel_transform.c[0].xyz, old->frame.voxel_transform.c[1].xyz); - - BeamformerViewPlaneTag plane = ui_plane_layout_from_normal(v3_normalize(N)); - switch (plane) { - case BeamformerViewPlaneTag_XY:{ - lateral->min_value = copy ? &bv->min_coordinate.x : &ui->min_coordinate.x; - lateral->max_value = copy ? &bv->max_coordinate.x : &ui->max_coordinate.x; - axial->min_value = copy ? &bv->min_coordinate.y : &ui->min_coordinate.y; - axial->max_value = copy ? &bv->max_coordinate.y : &ui->max_coordinate.y; - }break; - - case BeamformerViewPlaneTag_XZ:{ - lateral->min_value = copy ? &bv->min_coordinate.x : &ui->min_coordinate.x; - lateral->max_value = copy ? &bv->max_coordinate.x : &ui->max_coordinate.x; - axial->min_value = copy ? &bv->min_coordinate.z : &ui->min_coordinate.y; - axial->max_value = copy ? &bv->max_coordinate.z : &ui->max_coordinate.y; - }break; - - case BeamformerViewPlaneTag_YZ:{ - lateral->min_value = copy ? &bv->min_coordinate.y : &ui->min_coordinate.x; - lateral->max_value = copy ? &bv->max_coordinate.y : &ui->max_coordinate.x; - axial->min_value = copy ? &bv->min_coordinate.z : &ui->min_coordinate.y; - axial->max_value = copy ? &bv->max_coordinate.z : &ui->max_coordinate.y; - }break; - - default:{ - lateral->min_value = copy ? &bv->min_coordinate.x : &ui->min_coordinate.x; - lateral->max_value = copy ? &bv->max_coordinate.x : &ui->max_coordinate.x; - axial->min_value = copy ? &bv->min_coordinate.z : &ui->min_coordinate.y; - axial->max_value = copy ? &bv->max_coordinate.z : &ui->max_coordinate.y; - }break; - } - - #define X(id, text) add_button(ui, menu, arena, s8(text), UI_BID_ ##id, 0, ui->small_font); - FRAME_VIEW_BUTTONS - #undef X - - bv->axial_scale_bar_active = add_variable(ui, menu, arena, s8("Axial Scale Bar"), - V_INPUT|V_RADIO_BUTTON, VT_B32, ui->small_font); - bv->lateral_scale_bar_active = add_variable(ui, menu, arena, s8("Lateral Scale Bar"), - V_INPUT|V_RADIO_BUTTON, VT_B32, ui->small_font); - - if (kind == BeamformerFrameViewKind_Latest) { - bv->axial_scale_bar_active->bool32 = 1; - bv->lateral_scale_bar_active->bool32 = 1; - bv->axial_scale_bar.flags |= V_CAUSES_COMPUTE; - bv->lateral_scale_bar.flags |= V_CAUSES_COMPUTE; - } + result->scale_bar_active[0] = copy ? old->scale_bar_active[0] : 1; + result->scale_bar_active[1] = copy ? old->scale_bar_active[1] : 1; }break; - } - - bv->log_scale = add_variable(ui, menu, arena, s8("Log Scale"), - V_INPUT|V_UPDATE_VIEW|V_RADIO_BUTTON, VT_B32, ui->small_font); - bv->log_scale->bool32 = log_scale; - - switch (kind) { - case BeamformerFrameViewKind_Latest:{ - #define X(_type, _id, pretty) s8_comp(pretty), - read_only local_persist s8 labels[] = {BEAMFORMER_VIEW_PLANE_TAG_LIST s8_comp("Any")}; - #undef X - bv->cycler = add_variable_cycler(ui, menu, arena, 0, ui->small_font, s8("Live:"), - &bv->cycler_state, labels, countof(labels)); - bv->cycler_state = BeamformerViewPlaneTag_Count; + case BeamformerFrameViewKind_3DXPlane:{ + glTextureParameteri(result->texture, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTextureParameteri(result->texture, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + result->demo = 1; + result->plane_drag_index = -1; + result->plane_active[BeamformerViewPlaneTag_XZ] = 1; + result->plane_active[BeamformerViewPlaneTag_YZ] = 1; }break; - default:{}break; } - add_global_menu_to_group(ui, arena, menu); -} - -function BeamformerFrameView * -ui_beamformer_frame_view_new(BeamformerUI *ui, Arena *arena) -{ - BeamformerFrameView *result = SLLPopFreelist(ui->view_freelist); - if (!result) result = push_struct_no_zero(arena, typeof(*result)); - zero_struct(result); - result->export_handle.value[0] = OSInvalidHandleValue; - DLLInsertLast(0, ui->view_first, ui->view_last, result, next, prev); - return result; -} - -function Variable * -add_beamformer_frame_view(BeamformerUI *ui, Variable *parent, Arena *arena, - BeamformerFrameViewKind kind, b32 closable, BeamformerFrameView *old) -{ - /* TODO(rnp): this can be always closable once we have a way of opening new views */ - Variable *result = add_ui_view(ui, parent, arena, s8(""), UIViewFlag_CustomText, 1, closable); - Variable *var = result->view.child = add_variable(ui, result, arena, s8(""), 0, - VT_BEAMFORMER_FRAME_VIEW, ui->small_font); - Variable *menu = result->view.menu = add_variable_group(ui, 0, arena, s8(""), - VariableGroupKind_List, ui->small_font); - menu->parent = result; - var->generic = ui_beamformer_frame_view_new(ui, arena); - ui_beamformer_frame_view_convert(ui, arena, var, menu, kind, old, old? old->log_scale->bool32 : 0); - return result; -} - -function Variable * -add_compute_progress_bar(Variable *parent, BeamformerCtx *ctx) -{ - BeamformerUI *ui = ctx->ui; - /* TODO(rnp): this can be closable once we have a way of opening new views */ - Variable *result = add_ui_view(ui, parent, &ui->arena, s8(""), UIViewFlag_CustomText, 1, 0); - result->view.child = add_variable(ui, result, &ui->arena, s8(""), 0, - VT_COMPUTE_PROGRESS_BAR, ui->small_font); - ComputeProgressBar *bar = &result->view.child->compute_progress_bar; - bar->progress = &ctx->compute_context.processing_progress; - bar->processing = &ctx->compute_context.processing_compute; - - return result; -} - -function Variable * -add_compute_stats_view(BeamformerUI *ui, Variable *parent, Arena *arena, BeamformerCtx *ctx) -{ - /* TODO(rnp): this can be closable once we have a way of opening new views */ - Variable *result = add_ui_view(ui, parent, arena, s8(""), UIViewFlag_CustomText, 0, 0); - result->view.child = add_variable(ui, result, &ui->arena, s8(""), 0, - VT_COMPUTE_STATS_VIEW, ui->small_font); - - Variable *menu = result->view.menu = add_variable_group(ui, 0, arena, s8(""), - VariableGroupKind_List, ui->small_font); - menu->parent = result; - - #define X(_k, label) s8_comp(label), - read_only local_persist s8 labels[] = {COMPUTE_STATS_VIEW_LIST}; - #undef X - - ComputeStatsView *csv = &result->view.child->compute_stats_view; - csv->compute_shader_stats = ctx->compute_shader_stats; - csv->cycler = add_variable_cycler(ui, menu, arena, 0, ui->small_font, s8("Stats View:"), - (u32 *)&csv->kind, labels, countof(labels)); - add_global_menu_to_group(ui, arena, menu); - return result; -} - -function Variable * -add_live_controls_view(BeamformerUI *ui, Variable *parent, Arena *arena) -{ - BeamformerLiveImagingParameters *lip = &ui->shared_memory->live_imaging_parameters; - /* TODO(rnp): this can be closable once we have a way of opening new views */ - Variable *result = add_ui_view(ui, parent, &ui->arena, s8("Live Controls"), 0, 1, 0); - result->view.child = add_variable(ui, result, &ui->arena, s8(""), 0, - VT_LIVE_CONTROLS_VIEW, ui->small_font); - Variable *view = result->view.child; - BeamformerLiveControlsView *lv = view->generic = push_struct(arena, typeof(*lv)); - - Variable *amenu = &lv->acquisition_menu; - fill_variable(amenu, view, s8(""), V_INPUT, VT_GROUP, ui->small_font); - amenu->group.kind = VariableGroupKind_List; - - fill_variable(&lv->transmit_power, view, s8(""), V_INPUT|V_LIVE_CONTROL, - VT_BEAMFORMER_VARIABLE, ui->small_font); - fill_beamformer_variable(&lv->transmit_power, s8(""), &lip->transmit_power, (v2){{0, 1.0f}}, 100.0f, 0.05f); - - for (u32 i = 0; i < countof(lv->tgc_control_points); i++) { - Variable *v = lv->tgc_control_points + i; - fill_variable(v, view, s8(""), V_INPUT|V_LIVE_CONTROL, VT_BEAMFORMER_VARIABLE, ui->small_font); - fill_beamformer_variable(v, s8(""), lip->tgc_control_points + i, (v2){{0, 1.0f}}, 0, 0.05f); + if (kind == BeamformerFrameViewKind_Copy) { + assert(old != 0); + beamformer_ui_frame_view_copy_frame(result, old); } - fill_variable(&lv->stop_button, view, s8("Stop Imaging"), V_INPUT|V_LIVE_CONTROL, - VT_B32, ui->small_font); - - read_only local_persist s8 save_labels[] = {s8_comp("Save Data"), s8_comp("Saving...")}; - fill_variable(&lv->save_button, view, s8("Save Data"), V_INPUT|V_LIVE_CONTROL, - VT_CYCLER, ui->small_font); - fill_variable_cycler(&lv->save_button, &lip->save_active, save_labels, countof(save_labels)); - - fill_variable(&lv->save_text, view, s8(""), V_INPUT|V_TEXT|V_LIVE_CONTROL, - VT_LIVE_CONTROLS_STRING, ui->small_font); - lv->save_text.generic = lip; + if (kind == BeamformerFrameViewKind_Latest) + result->view_plane = BeamformerViewPlaneTag_Count; return result; } -function Variable * -ui_split_region(BeamformerUI *ui, Variable *region, Variable *split_side, RegionSplitDirection direction) -{ - Variable *result = add_ui_split(ui, region, &ui->arena, s8(""), 0.5, direction, ui->small_font); - if (split_side == region->region_split.left) { - region->region_split.left = result; - } else { - region->region_split.right = result; - } - split_side->parent = result; - result->region_split.left = split_side; - return result; -} - -function void -ui_add_live_frame_view(BeamformerUI *ui, Variable *view, RegionSplitDirection direction, - BeamformerFrameViewKind kind) -{ - Variable *region = view->parent; - assert(region->type == VT_UI_REGION_SPLIT); - assert(view->type == VT_UI_VIEW); - Variable *new_region = ui_split_region(ui, region, view, direction); - new_region->region_split.right = add_beamformer_frame_view(ui, new_region, &ui->arena, kind, 1, 0); -} - -function void -ui_beamformer_frame_view_copy_frame(BeamformerUI *ui, BeamformerFrameView *new, BeamformerFrameView *old) -{ - mem_copy(&new->frame, &old->frame, sizeof(old->frame)); - - iv3 points = new->frame.points; - i64 frame_size = points.x * points.y * points.z * beamformer_data_kind_byte_size[new->frame.data_kind]; - - Stream sb = arena_stream(ui->arena); - stream_append_s8(&sb, s8("Frame Copy [")); - stream_append_hex_u64(&sb, new->frame.id); - stream_append_s8(&sb, s8("]")); - stream_append_byte(&sb, 0); - - GPUBufferAllocateInfo allocate_info = { - .size = frame_size, - .flags = VulkanUsageFlag_TransferDestination, - .label = stream_to_str8(&sb), - }; - vk_buffer_allocate(&new->copy_buffer, &allocate_info); - - GPUBuffer *backlog = ui->beamformer_context->compute_context.backlog.buffer; - VulkanHandle cmd = vk_command_begin(VulkanTimeline_Compute); - vk_command_wait_timeline(cmd, VulkanTimeline_Compute, old->frame.timeline_valid_value); - vk_command_copy_buffer(cmd, &new->copy_buffer, backlog, old->frame.buffer_offset, frame_size); - new->frame.timeline_valid_value = vk_command_end(cmd, (VulkanHandle){0}, (VulkanHandle){0}); -} - -function void -ui_copy_frame(BeamformerUI *ui, Variable *view, RegionSplitDirection direction) -{ - Variable *region = view->parent; - assert(region->type == VT_UI_REGION_SPLIT); - assert(view->type == VT_UI_VIEW); - - BeamformerFrameView *old = view->view.child->generic; - Variable *new_region = ui_split_region(ui, region, view, direction); - new_region->region_split.right = add_beamformer_frame_view(ui, new_region, &ui->arena, - BeamformerFrameViewKind_Copy, 1, old); - - BeamformerFrameView *bv = new_region->region_split.right->view.child->generic; - ui_beamformer_frame_view_copy_frame(ui, bv, old); -} - function v3 -beamformer_frame_view_plane_size(BeamformerUI *ui, BeamformerFrameView *view) +x_plane_display_size(BeamformerFrame *frame) { - assert(view->kind == BeamformerFrameViewKind_3DXPlane); v3 result = {0}; - result.xy = v2_sub(ui->max_coordinate, ui->min_coordinate); + v2 min_2d, max_2d; + plane_corners_from_transform(frame->voxel_transform, &min_2d, &max_2d); + result.xy = v2_sub(max_2d, min_2d); result.x = Max(1e-3f, result.x); result.y = Max(1e-3f, result.y); result.z = Max(1e-3f, result.z); @@ -1626,72 +936,62 @@ x_plane_rotation_for_view_plane(BeamformerFrameView *view, BeamformerViewPlaneTa return result; } -function v2 -normalized_p_in_rect(Rect r, v2 p, b32 invert_y) -{ - v2 result = v2_div(v2_scale(v2_sub(p, r.pos), 2.0f), r.size); - if (invert_y) result = (v2){{result.x - 1.0f, 1.0f - result.y}}; - else result = v2_sub(result, (v2){{1.0f, 1.0f}}); - return result; -} - function v3 -x_plane_position(BeamformerUI *ui) +x_plane_position(BeamformerFrame *frame) { - f32 y_min = ui->min_coordinate.y; - f32 y_max = ui->max_coordinate.y; + v2 min_2d, max_2d; + plane_corners_from_transform(frame->voxel_transform, &min_2d, &max_2d); + f32 y_min = min_2d.y; + f32 y_max = max_2d.y; v3 result = {.y = y_min + (y_max - y_min) / 2}; return result; } function v3 -offset_x_plane_position(BeamformerUI *ui, BeamformerFrameView *view, BeamformerViewPlaneTag tag) +x_plane_offset_position(BeamformerFrameView *view, BeamformerFrame *frame, BeamformerViewPlaneTag tag) { - BeamformerLiveImagingParameters *li = &ui->shared_memory->live_imaging_parameters; + BeamformerLiveImagingParameters *li = &beamformer_context->shared_memory->live_imaging_parameters; m4 x_rotation = m4_rotation_about_y(x_plane_rotation_for_view_plane(view, tag)); v3 Z = x_rotation.c[2].xyz; v3 offset = v3_scale(Z, li->image_plane_offsets[tag]); - v3 result = v3_add(x_plane_position(ui), offset); + v3 result = v3_add(x_plane_position(frame), offset); return result; } function v3 -camera_for_x_plane_view(BeamformerUI *ui, BeamformerFrameView *view) +x_plane_camera(BeamformerFrame *frame) { - v3 size = beamformer_frame_view_plane_size(ui, view); - v3 target = x_plane_position(ui); - f32 dist = v2_magnitude(XY(size)); + v3 size = x_plane_display_size(frame); + v3 target = x_plane_position(frame); + f32 dist = v2_magnitude(size.xy); v3 result = v3_add(target, (v3){{dist, -0.5f * size.y * tan_f32(50.0f * PI / 180.0f), dist}}); return result; } function m4 -view_matrix_for_x_plane_view(BeamformerUI *ui, BeamformerFrameView *view, v3 camera) +x_plane_view_matrix(BeamformerFrame *frame, v3 camera) { - assert(view->kind == BeamformerFrameViewKind_3DXPlane); - m4 result = camera_look_at(camera, x_plane_position(ui)); + m4 result = camera_look_at(camera, x_plane_position(frame)); return result; } function m4 -projection_matrix_for_x_plane_view(BeamformerFrameView *view) +x_plane_projection_matrix(f32 aspect) { - assert(view->kind == BeamformerFrameViewKind_3DXPlane); - f32 aspect = (f32)view->colour_image.width / (f32)view->colour_image.height; m4 result = perspective_projection(10e-3f, 500e-3f, 45.0f * PI / 180.0f, aspect); return result; } function ray -ray_for_x_plane_view(BeamformerUI *ui, BeamformerFrameView *view, v2 uv) +x_plane_raycast(BeamformerFrameView *view, BeamformerFrame *frame, v2 uv) { assert(view->kind == BeamformerFrameViewKind_3DXPlane); - ray result = {.origin = camera_for_x_plane_view(ui, view)}; + ray result = {.origin = x_plane_camera(frame)}; v4 ray_clip = {{uv.x, uv.y, -1.0f, 1.0f}}; /* TODO(rnp): combine these so we only do one matrix inversion */ - m4 proj_m = projection_matrix_for_x_plane_view(view); - m4 view_m = view_matrix_for_x_plane_view(ui, view, result.origin); + m4 proj_m = x_plane_projection_matrix((f32)view->colour_image.width / (f32)view->colour_image.height); + m4 view_m = x_plane_view_matrix(frame, result.origin); m4 proj_inv = m4_inverse(proj_m); m4 view_inv = m4_inverse(view_m); @@ -1703,130 +1003,111 @@ ray_for_x_plane_view(BeamformerUI *ui, BeamformerFrameView *view, v2 uv) return result; } -function BeamformerViewPlaneTag -view_plane_tag_from_x_plane_shift(BeamformerFrameView *view, Variable *x_plane_shift) -{ - assert(BETWEEN(x_plane_shift, view->x_plane_shifts + 0, view->x_plane_shifts + 1)); - BeamformerViewPlaneTag result = BeamformerViewPlaneTag_XZ; - if (x_plane_shift == view->x_plane_shifts + 1) - result = BeamformerViewPlaneTag_YZ; - return result; -} - function void -render_single_xplane(BeamformerUI *ui, BeamformerFrameView *view, Variable *x_plane_shift, - f32 rotation_turns, v3 translate, BeamformerViewPlaneTag tag, - VulkanHandle command, BeamformerRenderBeamformedPushConstants *pc) -{ - GPUBuffer *beamformed_buffer = ui->beamformer_context->compute_context.backlog.buffer; - pc->input_data = 0; - if (ui->latest_plane_valid[tag]) { - BeamformerFrame *f = ui->latest_plane + tag; - pc->input_data = beamformed_buffer->gpu_pointer + f->buffer_offset; - pc->input_size_x = f->points.x; - pc->input_size_y = f->points.y; - pc->input_size_z = f->points.z; - pc->data_kind = f->data_kind; - vk_command_wait_timeline(command, VulkanTimeline_Compute, f->timeline_valid_value); - } - - v3 camera = camera_for_x_plane_view(ui, view); - v3 scale = beamformer_frame_view_plane_size(ui, view); - - m4 model_transform = y_aligned_volume_transform(scale, translate, rotation_turns); - m4 view_m = view_matrix_for_x_plane_view(ui, view, camera); - m4 projection = projection_matrix_for_x_plane_view(view); - - //pc->mvp_matrix = m4_mul(m4_mul(model_transform, view_m), projection); - pc->mvp_matrix = m4_mul(projection, m4_mul(view_m, model_transform)); - pc->bounding_box_colour = v4_lerp(FG_COLOUR, HOVERED_COLOUR, x_plane_shift->hover_t); - pc->bounding_box_fraction = FRAME_VIEW_BB_FRACTION; - +render_single_xplane(BeamformerFrameView *view, BeamformerFrame *frame, v3 translate, f32 rotation_turns, + VulkanHandle command, BeamformerRenderBeamformedPushConstants *pc, m4 vp_m, b32 drag_plane) +{ + GPUBuffer *beamformed_buffer = beamformer_context->compute_context.backlog.buffer; + pc->input_data = frame->timeline_valid_value ? beamformed_buffer->gpu_pointer + frame->buffer_offset : 0; + pc->input_size_x = frame->points.x; + pc->input_size_y = frame->points.y; + pc->input_size_z = frame->points.z; + pc->data_kind = frame->data_kind; + pc->mvp_matrix = m4_mul(vp_m, y_aligned_volume_transform(x_plane_display_size(frame), translate, rotation_turns)); + + vk_command_wait_timeline(command, VulkanTimeline_Compute, frame->timeline_valid_value); vk_command_push_constants(command, 0, sizeof(*pc), pc); - vk_command_draw(command, &ui->unit_cube_model.model); + vk_command_draw(command, &ui_context->unit_cube_model.model); - XPlaneShift *xp = &x_plane_shift->x_plane_shift; - v3 xp_delta = v3_sub(xp->end_point, xp->start_point); - if (!f32_equal(v3_magnitude(xp_delta), 0)) { + v3 xp_delta = v3_sub(view->hit_test_point, view->hit_start_point); + if (drag_plane && !f32_equal(v3_magnitude_squared(xp_delta), 0)) { m4 x_rotation = m4_rotation_about_y(rotation_turns); v3 Z = x_rotation.c[2].xyz; - v3 f = v3_scale(Z, v3_dot(Z, v3_sub(xp->end_point, xp->start_point))); - - /* TODO(rnp): there is no reason to compute the rotation matrix again */ - model_transform = y_aligned_volume_transform(scale, v3_add(f, translate), rotation_turns); + v3 f = v3_scale(Z, v3_dot(Z, xp_delta)); - pc->mvp_matrix = m4_mul(projection, m4_mul(view_m, model_transform)); + pc->mvp_matrix = m4_mul(vp_m, y_aligned_volume_transform(x_plane_display_size(frame), v3_add(f, translate), rotation_turns)); pc->bounding_box_colour = HOVERED_COLOUR; pc->bounding_box_fraction = 1.0f; pc->input_data = 0; vk_command_push_constants(command, 0, sizeof(*pc), pc); - vk_command_draw(command, &ui->unit_cube_model.model); + vk_command_draw(command, &ui_context->unit_cube_model.model); } } function void -render_3D_xplane(BeamformerUI *ui, BeamformerFrameView *view, VulkanHandle command, BeamformerRenderBeamformedPushConstants *pc) +render_3d_xplane(BeamformerFrameView *view, VulkanHandle command, BeamformerRenderBeamformedPushConstants *pc) { - if (view->demo->bool32) { + if (view->demo) { view->rotation += dt_for_frame * 0.125f; - if (view->rotation > 1.0f) view->rotation -= 1.0f; + if (view->rotation > 1.f) view->rotation -= 1.f; } - v3 model_translate = offset_x_plane_position(ui, view, BeamformerViewPlaneTag_XZ); - render_single_xplane(ui, view, view->x_plane_shifts + 0, - x_plane_rotation_for_view_plane(view, BeamformerViewPlaneTag_XZ), - model_translate, BeamformerViewPlaneTag_XZ, command, pc); - model_translate = offset_x_plane_position(ui, view, BeamformerViewPlaneTag_YZ); - model_translate.y -= 0.0001f; - render_single_xplane(ui, view, view->x_plane_shifts + 1, - x_plane_rotation_for_view_plane(view, BeamformerViewPlaneTag_YZ), - model_translate, BeamformerViewPlaneTag_YZ, command, pc); + u32 largest_plane_index = 0; + f32 largest_magnitude = 0.f; + for EachElement(view->plane_active, plane) if (view->plane_active[plane]) { + BeamformerFrame *frame = ui_context->latest_plane + plane; + f32 m = v3_magnitude_squared(x_plane_display_size(frame)); + if (largest_magnitude < m) { + largest_magnitude = m; + largest_plane_index = plane; + } + } + + BeamformerFrame *frame = ui_context->latest_plane + largest_plane_index; + m4 projection = x_plane_projection_matrix((f32)view->colour_image.width / (f32)view->colour_image.height); + m4 view_m = camera_look_at(x_plane_camera(frame), x_plane_position(frame)); + m4 vp_m = m4_mul(projection, view_m); + + for EachElement(view->plane_active, plane) { + frame = ui_context->latest_plane + plane; + if (view->plane_active[plane] && frame->timeline_valid_value) { + pc->bounding_box_fraction = FRAME_VIEW_BB_FRACTION; + pc->bounding_box_colour = v4_lerp(FG_COLOUR, HOVERED_COLOUR, view->hot_t[plane]); + f32 rotation = x_plane_rotation_for_view_plane(view, plane); + v3 translate = x_plane_offset_position(view, frame, plane); + render_single_xplane(view, frame, translate, rotation, command, pc, vp_m, (i32)plane == view->plane_drag_index); + } + } } function void -render_2D_plane(BeamformerUI *ui, BeamformerFrameView *view, VulkanHandle command, BeamformerRenderBeamformedPushConstants *pc) +render_2d_plane(BeamformerFrameView *view, VulkanHandle command, BeamformerRenderBeamformedPushConstants *pc) { m4 view_m = m4_identity(); m4 model = m4_scale((v3){{2.0f, 2.0f, 0.0f}}); m4 projection = orthographic_projection(0, 1, 1, 1); - GPUBuffer *beamformed_buffer = ui->beamformer_context->compute_context.backlog.buffer; + GPUBuffer *beamformed_buffer = beamformer_context->compute_context.backlog.buffer; pc->mvp_matrix = m4_mul(m4_mul(model, view_m), projection); - pc->input_data = beamformed_buffer->gpu_pointer + view->frame.buffer_offset, - pc->input_size_x = view->frame.points.x, - pc->input_size_y = view->frame.points.y, - pc->input_size_z = view->frame.points.z, - pc->data_kind = view->frame.data_kind, + pc->input_data = beamformed_buffer->gpu_pointer + view->frame.buffer_offset; + pc->input_size_x = view->frame.points.x; + pc->input_size_y = view->frame.points.y; + pc->input_size_z = view->frame.points.z; + pc->data_kind = view->frame.data_kind; vk_command_wait_timeline(command, VulkanTimeline_Compute, view->frame.timeline_valid_value); vk_command_push_constants(command, 0, sizeof(*pc), pc); - vk_command_draw(command, &ui->unit_cube_model.model); -} - -function b32 -frame_view_ready_to_present(BeamformerUI *ui, BeamformerFrameView *view) -{ - b32 result = view->colour_image.width > 0 || view->colour_image.height > 0; - return result; + vk_command_draw(command, &ui_context->unit_cube_model.model); } function b32 view_update(BeamformerUI *ui, BeamformerFrameView *view) { if (view->kind == BeamformerFrameViewKind_Latest) { - u32 index = *view->cycler->cycler.state; - view->dirty |= view->frame.timeline_valid_value != ui->latest_plane[index].timeline_valid_value; - mem_copy(&view->frame, ui->latest_plane + index, sizeof(view->frame)); - if (view->dirty) { - view->min_coordinate = m4_mul_v4(view->frame.voxel_transform, (v4){{0.0f, 0.0f, 0.0f, 1.0f}}).xyz; - view->max_coordinate = m4_mul_v4(view->frame.voxel_transform, (v4){{1.0f, 1.0f, 1.0f, 1.0f}}).xyz; - } + BeamformerFrame *frame; + if (view->view_plane == BeamformerViewPlaneTag_Count) + frame = beamformer_frame_from_index(beamformer_registers()->frame); + else + frame = ui->latest_plane + view->view_plane; + + view->dirty |= view->frame.timeline_valid_value != frame->timeline_valid_value; + memory_copy(&view->frame, frame, sizeof(view->frame)); } /* TODO(rnp): x-z or y-z */ // TODO(rnp): how to track this now? use pipeline handle value? - view->dirty |= ui->beamformer_context->render_shader_updated; + view->dirty |= beamformer_context->render_shader_updated; view->dirty |= view->kind == BeamformerFrameViewKind_3DXPlane; b32 result = view->dirty; @@ -1840,9 +1121,9 @@ update_frame_views(BeamformerUI *ui, Rect window) if (view_update(ui, view)) { BeamformerRenderBeamformedPushConstants pc = { .bounding_box_colour = FRAME_VIEW_BB_COLOUR, - .db_cutoff = view->log_scale->bool32 ? view->dynamic_range.real32 : 0, - .threshold = view->threshold.real32, - .gamma = view->gamma.scaled_real32.val, + .db_cutoff = view->log_scale ? view->dynamic_range : 0, + .threshold = view->threshold, + .gamma = view->gamma, .positions = ui->unit_cube_model.model.gpu_pointer, .normals = ui->unit_cube_model.model.gpu_pointer + ui->unit_cube_model.normals_offset, }; @@ -1857,9 +1138,9 @@ update_frame_views(BeamformerUI *ui, Rect window) vk_command_viewport(cmd, view->colour_image.width, view->colour_image.height, 0, 0, 0.0f, 1.0f); vk_command_scissor(cmd, view->colour_image.width, view->colour_image.height, 0, 0); if (view->kind == BeamformerFrameViewKind_3DXPlane) { - render_3D_xplane(ui, view, cmd, &pc); + render_3d_xplane(view, cmd, &pc); } else { - render_2D_plane(ui, view, cmd, &pc); + render_2d_plane(view, cmd, &pc); } vk_command_end_rendering(cmd); vk_command_end(cmd, ui->render_semaphores[0], ui->render_semaphores[1]); @@ -1867,7 +1148,6 @@ update_frame_views(BeamformerUI *ui, Rect window) glWaitSemaphoreEXT(ui->render_semaphores_gl[1], 0, 0, 1, &view->texture, (GLenum[]){GL_LAYOUT_COLOR_ATTACHMENT_EXT}); //end_renderdoc_capture(); - view->dirty = 0; } } @@ -1881,19 +1161,36 @@ colour_from_normalized(v4 rgba) return result; } -function Color -fade(Color a, f32 visibility) +function void +draw_text_tight(Font font, str8 text, v2 pos, Color colour) { - a.a = (u8)((f32)a.a * visibility); - return a; + v2 off = v2_floor(pos); + for (i64 i = 0; i < text.length; i++) { + /* NOTE: assumes font glyphs are ordered ASCII */ + i32 idx = text.data[i] - 0x20; + Rectangle dst = { + off.x, off.y, + font.recs[idx].width, + font.recs[idx].height, + }; + Rectangle src = { + font.recs[idx].x, + font.recs[idx].y, + font.recs[idx].width, + font.recs[idx].height, + }; + DrawTexturePro(font.texture, src, dst, (Vector2){0}, 0, colour); + + off.x += (f32)font.recs[idx].width; + } } function v2 -draw_text_base(Font font, s8 text, v2 pos, Color colour) +draw_text_base(Font font, str8 text, v2 pos, Color colour) { v2 off = v2_floor(pos); f32 glyph_pad = (f32)font.glyphPadding; - for (iz i = 0; i < text.len; i++) { + for (i64 i = 0; i < text.length; i++) { /* NOTE: assumes font glyphs are ordered ASCII */ i32 idx = text.data[i] - 0x20; Rectangle dst = { @@ -1920,7 +1217,7 @@ draw_text_base(Font font, s8 text, v2 pos, Color colour) /* NOTE(rnp): expensive but of the available options in raylib this gives the best results */ function v2 -draw_outlined_text(s8 text, v2 pos, TextSpec *ts) +draw_outlined_text(str8 text, v2 pos, TextSpec *ts) { f32 ow = ts->outline_thick; Color outline = colour_from_normalized(ts->outline_colour); @@ -1936,7 +1233,7 @@ draw_outlined_text(s8 text, v2 pos, TextSpec *ts) } function v2 -draw_text(s8 text, v2 pos, TextSpec *ts) +draw_text(str8 text, v2 pos, TextSpec *ts) { if (ts->flags & TF_ROTATED) { rlPushMatrix(); @@ -1947,15 +1244,15 @@ draw_text(s8 text, v2 pos, TextSpec *ts) v2 result = measure_text(*ts->font, text); /* TODO(rnp): the size of this should be stored for each font */ - s8 ellipsis = s8("..."); + str8 ellipsis = str8("..."); b32 clamped = ts->flags & TF_LIMITED && result.w > ts->limits.size.w; if (clamped) { f32 ellipsis_width = measure_text(*ts->font, ellipsis).x; if (ellipsis_width < ts->limits.size.w) { text = clamp_text_to_width(*ts->font, text, ts->limits.size.w - ellipsis_width); } else { - text.len = 0; - ellipsis.len = 0; + text.length = 0; + ellipsis.length = 0; } } @@ -1975,2116 +1272,3691 @@ draw_text(s8 text, v2 pos, TextSpec *ts) return result; } -function Rect -extend_rect_centered(Rect r, v2 delta) +function b32 +point_in_rect(v2 p, Rect r) { - r.size.w += delta.x; - r.size.h += delta.y; - r.pos.x -= delta.x / 2; - r.pos.y -= delta.y / 2; - return r; + v2 end = v2_add(r.pos, r.size); + b32 result = Between(p.x, r.pos.x, end.x) & Between(p.y, r.pos.y, end.y); + return result; } -function Rect -shrink_rect_centered(Rect r, v2 delta) +function v3 +world_point_from_plane_uv(m4 world, v2 uv) { - delta.x = MIN(delta.x, r.size.w); - delta.y = MIN(delta.y, r.size.h); - r.size.w -= delta.x; - r.size.h -= delta.y; - r.pos.x += delta.x / 2; - r.pos.y += delta.y / 2; - return r; + v3 U = world.c[0].xyz; + v3 V = world.c[1].xyz; + v3 min = world.c[3].xyz; + v3 result = v3_add(v3_add(v3_scale(U, uv.x), v3_scale(V, uv.y)), min); + return result; } -function Rect -scale_rect_centered(Rect r, v2 scale) +function v2 +screen_point_to_world_2d(v2 p, v2 screen_min, v2 screen_max, v2 world_min, v2 world_max) { - Rect or = r; - r.size.w *= scale.x; - r.size.h *= scale.y; - r.pos.x += (or.size.w - r.size.w) / 2; - r.pos.y += (or.size.h - r.size.h) / 2; - return r; -} + v2 pixels_to_m = v2_div(v2_sub(world_max, world_min), v2_sub(screen_max, screen_min)); + v2 result = v2_add(v2_mul(v2_sub(p, screen_min), pixels_to_m), world_min); + return result; +} -function b32 -interactions_equal(Interaction a, Interaction b) +function v2 +world_point_to_screen_2d(v2 p, v2 world_min, v2 world_max, v2 screen_min, v2 screen_max) { - b32 result = (a.kind == b.kind) && (a.generic == b.generic); + v2 m_to_pixels = v2_div(v2_sub(screen_max, screen_min), v2_sub(world_max, world_min)); + v2 result = v2_add(v2_mul(v2_sub(p, world_min), m_to_pixels), screen_min); return result; } -function b32 -interaction_is_sticky(Interaction a) +function void +draw_view_ruler(BeamformerFrameView *view, Arena a, Rect view_rect, TextSpec ts) { - b32 result = a.kind == InteractionKind_Text || a.kind == InteractionKind_Ruler; - return result; + // TODO(rnp): merge this into draw function, tons of duplicate code + v2 vr_max_p = v2_add(view_rect.pos, view_rect.size); + + v3 U = view->frame.voxel_transform.c[0].xyz; + v3 V = view->frame.voxel_transform.c[1].xyz; + v3 min = view->frame.voxel_transform.c[3].xyz; + + v3 end = view->ruler.end; + if (view->ruler.state != RulerState_Hold) + end = world_point_from_plane_uv(view->frame.voxel_transform, rect_uv(ui_context->current_mouse, view_rect)); + + v2 start_uv = plane_uv(v3_sub(view->ruler.start, min), U, V); + v2 end_uv = plane_uv(v3_sub(end, min), U, V); + + v2 start_p = v2_add(view_rect.pos, v2_mul(start_uv, view_rect.size)); + v2 end_p = v2_add(view_rect.pos, v2_mul(end_uv, view_rect.size)); + + b32 start_in_bounds = point_in_rect(start_p, view_rect); + b32 end_in_bounds = point_in_rect(end_p, view_rect); + + // TODO(rnp): this should be a ray intersection not a clamp + start_p = clamp_v2_rect(start_p, view_rect); + end_p = clamp_v2_rect(end_p, view_rect); + + Color rl_colour = colour_from_normalized(ts.colour); + DrawLineEx(rl_v2(end_p), rl_v2(start_p), 2, rl_colour); + if (start_in_bounds) DrawCircleV(rl_v2(start_p), 3, rl_colour); + if (end_in_bounds) DrawCircleV(rl_v2(end_p), 3, rl_colour); + + Stream buf = arena_stream(a); + stream_append_f64(&buf, 1e3 * v3_magnitude(v3_sub(end, view->ruler.start)), 100); + stream_append_s8(&buf, s8(" mm")); + + s8 s = stream_to_s8(&buf); + v2 txt_p = start_p; + v2 txt_s = measure_text(*ts.font, str8_from_s8(s)); + v2 pixel_delta = v2_sub(start_p, end_p); + if (pixel_delta.y < 0) txt_p.y -= txt_s.y; + if (pixel_delta.x < 0) txt_p.x -= txt_s.x; + if (txt_p.x < view_rect.pos.x) txt_p.x = view_rect.pos.x; + if (txt_p.x + txt_s.x > vr_max_p.x) txt_p.x -= (txt_p.x + txt_s.x) - vr_max_p.x; + + draw_text(str8_from_s8(s), txt_p, &ts); } -function b32 -interaction_is_hot(BeamformerUI *ui, Interaction a) +function void +ui_event_consume(BeamformerInput *input, BeamformerInputEvent *current) +{ + BeamformerUI *ui = ui_context; + BeamformerInputEvent *last = input->event_queue + input->event_count - 1; + if Between(current, input->event_queue, last) { + u64 index = current - input->event_queue; + u64 bin = index / (sizeof(ui->input_consumed[0]) * 8); + u64 bit = index % (sizeof(ui->input_consumed[0]) * 8); + ui->input_consumed[bin] |= (1 << bit); + } +} + +function BeamformerInputEvent * +ui_event_next(BeamformerInput *input, BeamformerInputEvent *current) { - b32 result = interactions_equal(ui->hot_interaction, a); + BeamformerUI *ui = ui_context; + BeamformerInputEvent *result = 0, *last = input->event_queue + input->event_count - 1; + + current++; + current = Max(current, input->event_queue); + + for (; !result && Between(current, input->event_queue, last); current++) { + u64 index = current - input->event_queue; + u64 bin = index / (sizeof(ui->input_consumed[0]) * 8); + u64 bit = index % (sizeof(ui->input_consumed[0]) * 8); + + if (!(ui->input_consumed[bin] & (1 << bit)) && + (current->kind == BeamformerInputEventKind_ButtonPress || + current->kind == BeamformerInputEventKind_ButtonRelease || + current->kind == BeamformerInputEventKind_MouseScroll)) + { + result = current; + } + } return result; } -function b32 -point_in_rect(v2 p, Rect r) +function UINode * +ui_node_from_key(UINodeKey key) { - v2 end = v2_add(r.pos, r.size); - b32 result = Between(p.x, r.pos.x, end.x) & Between(p.y, r.pos.y, end.y); + UINodeHashBucket *hb = ui_context->node_hash_table + (key.value % UI_HASH_TABLE_COUNT); + UINode *result = &ui_node_nil; + + for (UINode *b = hb->first; !ui_node_is_nil(b); b = b->hash_next) { + if (ui_node_key_equal(b->key, key)) { + result = b; + break; + } + } + return result; } -function v2 -rect_uv(v2 p, Rect r) +function str8 +ui_draw_part_from_key_string(str8 string) { - v2 result = v2_div(v2_sub(p, r.pos), r.size); + str8 result = string; + i64 index = str8_find_needle(string, str8("##"), 0); + if (index < string.length) + result.length = index; return result; } -function v3 -world_point_from_plane_uv(m4 world, v2 uv) +function str8 +ui_hash_part_from_key_string(str8 string) { - v3 U = world.c[0].xyz; - v3 V = world.c[1].xyz; - v3 min = world.c[3].xyz; - v3 result = v3_add(v3_add(v3_scale(U, uv.x), v3_scale(V, uv.y)), min); + str8 result = string; + // NOTE(rnp): for xxx###yyy only use the ###yyy otherwise the whole string is hashed + i64 index = str8_find_needle(string, str8("###"), 0); + if (index < string.length) + result = str8_skip(string, index); return result; } -function v2 -screen_point_to_world_2d(v2 p, v2 screen_min, v2 screen_max, v2 world_min, v2 world_max) +function UINodeKey +ui_key_from_string(str8 string, UINodeKey seed) { - v2 pixels_to_m = v2_div(v2_sub(world_max, world_min), v2_sub(screen_max, screen_min)); - v2 result = v2_add(v2_mul(v2_sub(p, screen_min), pixels_to_m), world_min); + UINodeKey result = {0}; + if (string.length > 0) { + str8 hash_string = ui_hash_part_from_key_string(string); + result.value = u64_hash_from_str8_seed(hash_string, seed.value); + } return result; } -function v2 -world_point_to_screen_2d(v2 p, v2 world_min, v2 world_max, v2 screen_min, v2 screen_max) +function Font +ui_font_for_node(UINode *node) { - v2 m_to_pixels = v2_div(v2_sub(screen_max, screen_min), v2_sub(world_max, world_min)); - v2 result = v2_add(v2_mul(v2_sub(p, world_min), m_to_pixels), screen_min); + Font result = node->font_size > 28.0f ? ui_context->font : ui_context->small_font; return result; } function b32 -hover_interaction(BeamformerUI *ui, v2 mouse, Interaction interaction) +ui_number_conversion_f64(str8 s, f64 *out_value) +{ + b32 result = 0; + NumberConversion number = number_from_str8(s); + if (number.result == NumberConversionResult_Success) { + result = 1; + if (number.kind == NumberConversionKind_Float) + *out_value = number.F64; + else + *out_value = (f64)number.S64; + } + return result; +} + +function iv2 +ui_text_input_cursor_range(void) +{ + UITextInputState *tis = &ui_context->text_input_state; + iv2 range; + range.x = Min(tis->cursor, tis->mark); + range.y = Max(tis->cursor, tis->mark); + return range; +} + +function str8 +ui_text_input_string(void) { - Variable *var = interaction.var; - b32 result = point_in_rect(mouse, interaction.rect); - if (result) ui->next_interaction = interaction; - if (interaction_is_hot(ui, interaction)) var->hover_t += HOVER_SPEED * dt_for_frame; - else var->hover_t -= HOVER_SPEED * dt_for_frame; - var->hover_t = CLAMP01(var->hover_t); + UITextInputState *tis = &ui_context->text_input_state; + str8 result = {.data = tis->buffer, .length = tis->count}; return result; } -function void -draw_close_button(BeamformerUI *ui, Variable *close, v2 mouse, Rect r, v2 x_scale) +function str8 +ui_text_input_last_string(void) { - assert(close->type == VT_UI_BUTTON); - hover_interaction(ui, mouse, auto_interaction(r, close)); - - Color colour = colour_from_normalized(v4_lerp(MENU_CLOSE_COLOUR, FG_COLOUR, close->hover_t)); - r = scale_rect_centered(r, x_scale); - DrawLineEx(rl_v2(r.pos), rl_v2(v2_add(r.pos, r.size)), 4, colour); - DrawLineEx(rl_v2(v2_add(r.pos, (v2){.x = r.size.w})), - rl_v2(v2_add(r.pos, (v2){.y = r.size.h})), 4, colour); + UITextInputState *tis = &ui_context->text_input_state; + str8 result = {.data = tis->last_buffer, .length = tis->last_count}; + return result; } function Rect -draw_title_bar(BeamformerUI *ui, Arena arena, Variable *ui_view, Rect r, v2 mouse) +ui_text_input_rect(void) { - assert(ui_view->type == VT_UI_VIEW); - UIView *view = &ui_view->view; - - s8 title = ui_view->name; - if (view->flags & UIViewFlag_CustomText) { - Stream buf = arena_stream(arena); - push_custom_view_title(&buf, ui_view->view.child); - title = arena_stream_commit(&arena, &buf); - } - - Rect result, title_rect; - cut_rect_vertical(r, (f32)ui->small_font.baseSize + TITLE_BAR_PAD, &title_rect, &result); - cut_rect_vertical(result, LISTING_LINE_PAD, 0, &result); + Rect result = ui_node_rect(ui_node_from_key(ui_context->text_input_state.node_key)); + f32 text_box_slop = 4.0f; + result.pos.x -= text_box_slop; + result.size.x += 2 * text_box_slop; + return result; +} - DrawRectangleRec(rl_rect(title_rect), BLACK); +function i32 +ui_text_input_index_from_point(f32 point) +{ + i32 result = 0; - title_rect = shrink_rect_centered(title_rect, (v2){.x = 1.5f * TITLE_BAR_PAD}); - DrawRectangleRounded(rl_rect(title_rect), 0.5f, 0, fade(colour_from_normalized(BG_COLOUR), 0.55f)); - title_rect = shrink_rect_centered(title_rect, (v2){.x = 3.0f * TITLE_BAR_PAD}); + // TODO(rnp): visible range, extended virtual rect which exactly fits the visible text + UITextInputState *tis = &ui_context->text_input_state; + Rect r = ui_text_input_rect(); - if (view->close) { - Rect close; - cut_rect_horizontal(title_rect, title_rect.size.w - title_rect.size.h, &title_rect, &close); - draw_close_button(ui, view->close, mouse, close, (v2){{0.4f, 0.4f}}); - } + Font font = ui_font_for_node(ui_node_from_key(tis->node_key)); - if (view->menu) { - Rect menu; - cut_rect_horizontal(title_rect, title_rect.size.w - title_rect.size.h, &title_rect, &menu); - Interaction interaction = {.kind = InteractionKind_Menu, .var = view->menu, .rect = menu}; - hover_interaction(ui, mouse, interaction); - - Color colour = colour_from_normalized(v4_lerp(MENU_PLUS_COLOUR, FG_COLOUR, view->menu->hover_t)); - menu = shrink_rect_centered(menu, (v2){{14.0f, 14.0f}}); - DrawLineEx(rl_v2(v2_add(menu.pos, (v2){.x = menu.size.w / 2})), - rl_v2(v2_add(menu.pos, (v2){.x = menu.size.w / 2, .y = menu.size.h})), 4, colour); - DrawLineEx(rl_v2(v2_add(menu.pos, (v2){.y = menu.size.h / 2})), - rl_v2(v2_add(menu.pos, (v2){.x = menu.size.w, .y = menu.size.h / 2})), 4, colour); + /* NOTE: extra offset to help with putting a cursor at idx 0 */ + f32 pct = Clamp01((point - r.pos.x) / r.size.w); + f32 x_off = 10.0f, x_bounds = r.size.w * pct; + for (; result < tis->count && x_off < x_bounds; result++) { + /* NOTE: assumes font glyphs are ordered ASCII */ + i32 idx = tis->buffer[result] - 0x20; + x_off += (f32)font.glyphs[idx].advanceX; + if (font.glyphs[idx].advanceX == 0) + x_off += font.recs[idx].width; } - v2 title_pos = title_rect.pos; - title_pos.y += 0.5f * TITLE_BAR_PAD; - TextSpec text_spec = {.font = &ui->small_font, .flags = TF_LIMITED, .colour = FG_COLOUR, - .limits.size = title_rect.size}; - draw_text(title, title_pos, &text_spec); - return result; } -/* TODO(rnp): once this has more callers decide if it would be better for this to take - * an orientation rather than force CCW/right-handed */ function void -draw_ruler(BeamformerUI *ui, Arena arena, v2 start_point, v2 end_point, - f32 start_value, f32 end_value, f32 *markers, u32 marker_count, - u32 segments, s8 suffix, v4 marker_colour, v4 txt_colour) +ui_text_input_end(void) { - b32 draw_plus = SIGN(start_value) != SIGN(end_value); - - end_point = v2_sub(end_point, start_point); - f32 rotation = atan2_f32(end_point.y, end_point.x) * 180 / PI; - - rlPushMatrix(); - rlTranslatef(start_point.x, start_point.y, 0); - rlRotatef(rotation, 0, 0, 1); - - f32 inc = v2_magnitude(end_point) / (f32)segments; - f32 value_inc = (end_value - start_value) / (f32)segments; - f32 value = start_value; - - Stream buf = arena_stream(arena); - v2 sp = {0}, ep = {.y = RULER_TICK_LENGTH}; - v2 tp = {{(f32)ui->small_font.baseSize / 2.0f, ep.y + RULER_TEXT_PAD}}; - TextSpec text_spec = {.font = &ui->small_font, .rotation = 90.0f, .colour = txt_colour, .flags = TF_ROTATED}; - Color rl_txt_colour = colour_from_normalized(txt_colour); - - for (u32 j = 0; j <= segments; j++) { - DrawLineEx(rl_v2(sp), rl_v2(ep), 3, rl_txt_colour); + UITextInputState *tis = &ui_context->text_input_state; - stream_reset(&buf, 0); - if (draw_plus && value > 0) stream_append_byte(&buf, '+'); - stream_append_f64(&buf, value, Abs(value_inc) < 1 ? 100 : 10); - stream_append_s8(&buf, suffix); - draw_text(stream_to_s8(&buf), tp, &text_spec); - - value += value_inc; - sp.x += inc; - ep.x += inc; - tp.x += inc; - } + UINode *next_node = ui_node_from_key(tis->next_node_key); + str8 new_input_string = str8(""); + if ((next_node->flags & UINodeFlag_TextInputClearOnStart) == 0) + new_input_string = ui_draw_part_from_key_string(next_node->string); - Color rl_marker_colour = colour_from_normalized(marker_colour); - ep.y += RULER_TICK_LENGTH; - for (u32 i = 0; i < marker_count; i++) { - if (markers[i] < F32_INFINITY) { - ep.x = sp.x = markers[i]; - DrawLineEx(rl_v2(sp), rl_v2(ep), 3, rl_marker_colour); - DrawCircleV(rl_v2(ep), 3, rl_marker_colour); - } - } + tis->cursor = tis->mark = 0; + tis->last_count = tis->count; + tis->count = Min(new_input_string.length, countof(tis->buffer)); + tis->numeric = (next_node->flags & UINodeFlag_TextInputNumeric) != 0; + memory_copy(tis->last_buffer, tis->buffer, tis->last_count); + memory_copy(tis->buffer, new_input_string.data, tis->count); - rlPopMatrix(); + tis->last_node_key = tis->node_key; + tis->node_key = ui_node_key_zero(); } function void -do_scale_bar(BeamformerUI *ui, Arena arena, Variable *scale_bar, v2 mouse, Rect draw_rect, - f32 start_value, f32 end_value, s8 suffix) +ui_text_input_insert(str8 text) { - assert(scale_bar->type == VT_SCALE_BAR); - ScaleBar *sb = &scale_bar->scale_bar; - - v2 txt_s = measure_text(ui->small_font, s8("-288.8 mm")); - - Rect tick_rect = draw_rect; - v2 start_pos = tick_rect.pos; - v2 end_pos = tick_rect.pos; - v2 relative_mouse = v2_sub(mouse, tick_rect.pos); - - f32 markers[2]; - u32 marker_count = 1; - - v2 world_zoom_point = {{sb->zoom_starting_coord, sb->zoom_starting_coord}}; - v2 screen_zoom_point = world_point_to_screen_2d(world_zoom_point, - (v2){{*sb->min_value, *sb->min_value}}, - (v2){{*sb->max_value, *sb->max_value}}, - (v2){0}, tick_rect.size); - u32 tick_count; - if (sb->direction == SB_AXIAL) { - tick_rect.size.x = RULER_TEXT_PAD + RULER_TICK_LENGTH + txt_s.x; - tick_count = (u32)(tick_rect.size.y / (1.5f * (f32)ui->small_font.baseSize)); - start_pos.y += tick_rect.size.y; - markers[0] = tick_rect.size.y - screen_zoom_point.y; - markers[1] = tick_rect.size.y - relative_mouse.y; - } else { - tick_rect.size.y = RULER_TEXT_PAD + RULER_TICK_LENGTH + txt_s.x; - tick_count = (u32)(tick_rect.size.x / (1.5f * (f32)ui->small_font.baseSize)); - end_pos.x += tick_rect.size.x; - markers[0] = screen_zoom_point.x; - markers[1] = relative_mouse.x; - } + UITextInputState *tis = &ui_context->text_input_state; + iv2 cursor_range = ui_text_input_cursor_range(); + i64 bytes_after_cursor = tis->count - cursor_range.y; + i64 remaining_length = ((i32)countof(tis->buffer) - cursor_range.x) - bytes_after_cursor; + i64 truncated_length = Min(remaining_length, text.length); - if (hover_interaction(ui, mouse, auto_interaction(tick_rect, scale_bar))) - marker_count = 2; + memory_move(tis->buffer + cursor_range.x + truncated_length, + tis->buffer + cursor_range.y, bytes_after_cursor); + memory_copy(tis->buffer + cursor_range.x, text.data, truncated_length); - draw_ruler(ui, arena, start_pos, end_pos, start_value, end_value, markers, marker_count, - tick_count, suffix, RULER_COLOUR, v4_lerp(FG_COLOUR, HOVERED_COLOUR, scale_bar->hover_t)); + tis->count -= cursor_range.y - cursor_range.x; + tis->count += truncated_length; + tis->cursor = tis->mark = cursor_range.x + truncated_length; } -function v2 -draw_radio_button(BeamformerUI *ui, Variable *var, v2 at, v2 mouse, v4 base_colour, f32 size) +function b32 +ui_text_input_update(BeamformerInput *input) { - assert(var->type == VT_B32); - b32 value = var->bool32; + UITextInputState *tis = &ui_context->text_input_state; - v2 result = (v2){.x = size, .y = size}; - Rect hover_rect = {.pos = at, .size = result}; - hover_rect.pos.y += 1; - hover_interaction(ui, mouse, auto_interaction(hover_rect, var)); + Arena scratch = *ui_build_arena(); + Stream sb = arena_stream(scratch); - hover_rect = shrink_rect_centered(hover_rect, (v2){{8.0f, 8.0f}}); - Rect inner = shrink_rect_centered(hover_rect, (v2){{4.0f, 4.0f}}); - v4 fill = v4_lerp(value? base_colour : (v4){0}, HOVERED_COLOUR, var->hover_t); - DrawRectangleRoundedLinesEx(rl_rect(hover_rect), 0.2f, 0, 2, colour_from_normalized(base_colour)); - DrawRectangleRec(rl_rect(inner), colour_from_normalized(fill)); + enum { + DeltaPicksSide = (1 << 0), + WordScan = (1 << 1), + Delete = (1 << 2), + KeepMark = (1 << 3), + Copy = (1 << 4), + Paste = (1 << 5), + }; - return result; -} + i32 delta = 0; + u32 flags = 0; -function f32 -draw_variable_slider(BeamformerUI *ui, Variable *var, Rect r, f32 fill, v4 fill_colour, v2 mouse) -{ - f32 border_thick = 3.0f; - f32 bar_height_frac = 0.8f; - v2 bar_size = {{6.0f, bar_height_frac * r.size.y}}; + b32 result = 0; - Rect inner = shrink_rect_centered(r, (v2){{2.0f * border_thick, // NOTE(rnp): raylib jank - MAX(0, 2.0f * (r.size.y - bar_size.y))}}); - Rect filled = inner; - filled.size.w *= fill; + // NOTE(rnp): first pass, non uniform inputs + for (BeamformerInputEvent *event = ui_event_next(input, 0); + event; + event = ui_event_next(input, event)) + { + b32 taken = 0; - Rect bar; - bar.pos = v2_add(r.pos, (v2){{fill * (r.size.w - bar_size.w), (1 - bar_height_frac) * 0.5f * r.size.y}}); - bar.size = bar_size; - v4 bar_colour = v4_lerp(FG_COLOUR, FOCUSED_COLOUR, var->hover_t); + BeamformerInputModifiers mods = event->modifiers; + if (event->kind == BeamformerInputEventKind_ButtonPress) { + if (mods & BeamformerInputModifier_Control) + flags |= WordScan; - hover_interaction(ui, mouse, auto_interaction(inner, var)); + if (mods & BeamformerInputModifier_Shift) + flags |= KeepMark; - DrawRectangleRec(rl_rect(filled), colour_from_normalized(fill_colour)); - DrawRectangleRoundedLinesEx(rl_rect(inner), 0.2f, 0, border_thick, BLACK); - DrawRectangleRounded(rl_rect(bar), 0.6f, 1, colour_from_normalized(bar_colour)); + switch (event->button_id) { + default:{}break; + case BeamformerButtonID_Escape: + case BeamformerButtonID_Enter: + { + taken = 1; + result = 1; + }break; - return r.size.y; -} + case BeamformerButtonID_A: if (mods & BeamformerInputModifier_Control) { + tis->cursor = 0; + tis->mark = tis->count; + taken = 1; + }break; -function v2 -draw_fancy_button(BeamformerUI *ui, Variable *var, s8 label, Rect r, v4 border_colour, v2 mouse, TextSpec ts) -{ - assert((f32)ts.font->baseSize <= r.size.h * 0.8f); - f32 pad = 0.1f * r.size.h; + case BeamformerButtonID_C: if (mods & BeamformerInputModifier_Control) { + flags |= Copy; + taken = 1; + }break; - v2 shadow_off = {{2.5f, 3.0f}}; - f32 border_thick = 3.0f; - v2 border_size = v2_add((v2){{pad + 2.0f * border_thick, pad}}, shadow_off); + case BeamformerButtonID_V: if (mods & BeamformerInputModifier_Control) { + flags |= Paste; + taken = 1; + }break; - Rect border = shrink_rect_centered(r, border_size); - Rect inner = shrink_rect_centered(border, (v2){{pad, pad}}); + case BeamformerButtonID_X: if (mods & BeamformerInputModifier_Control) { + flags |= Copy|Delete|KeepMark; + taken = 1; + }break; - ts.limits.size = inner.size; - hover_interaction(ui, mouse, auto_interaction(inner, var)); + case BeamformerButtonID_Backspace:{ + delta -= 1; + flags |= Delete|KeepMark; + taken = 1; + }break; - border.pos = v2_add(border.pos, shadow_off); + case BeamformerButtonID_Delete:{ + delta += 1; + flags |= Delete|KeepMark; + taken = 1; + }break; - DrawRectangleRoundedLinesEx(rl_rect(border), 0.6f, 0, border_thick, fade(BLACK, 0.8f)); - border.pos = v2_sub(border.pos, shadow_off); - DrawRectangleRounded(rl_rect(border), 0.6f, 1, colour_from_normalized(BG_COLOUR)); - DrawRectangleRoundedLinesEx(rl_rect(border), 0.6f, 0, border_thick, colour_from_normalized(border_colour)); + case BeamformerButtonID_Left:{ + delta -= 1; + flags |= DeltaPicksSide; + taken = 1; + }break; - /* TODO(rnp): teach draw_text() about alignment */ - v2 at = align_text_in_rect(label, inner, *ts.font); - at = v2_add(at, (v2){{3.0f, 3.0f}}); - v4 base_colour = ts.colour; - ts.colour = (v4){{0, 0, 0, 0.8f}}; - draw_text(label, at, &ts); + case BeamformerButtonID_Right:{ + delta += 1; + flags |= DeltaPicksSide; + taken = 1; + }break; - at = v2_sub(at, (v2){{3.0f, 3.0f}}); - ts.colour = v4_lerp(base_colour, HOVERED_COLOUR, var->hover_t); - draw_text(label, at, &ts); + } - v2 result = v2_add(r.size, border_size); - return result; -} + if (!taken && event->codepoint) { + u32 cp = event->codepoint; + taken = !tis->numeric || (Between(cp, '0', '9') || (cp == '.') || (cp == '-' && tis->cursor == 0)); + if (taken) stream_append_codepoint(&sb, event->codepoint); + } + } -function v2 -draw_variable(BeamformerUI *ui, Arena arena, Variable *var, v2 at, v2 mouse, v4 base_colour, TextSpec text_spec) -{ - v2 result; - if (var->flags & V_RADIO_BUTTON) { - result = draw_radio_button(ui, var, at, mouse, base_colour, (f32)text_spec.font->baseSize); - } else { - Stream buf = arena_stream(arena); - stream_append_variable(&buf, var); - s8 text = arena_stream_commit(&arena, &buf); - result = measure_text(*text_spec.font, text); - - if (var->flags & V_INPUT) { - Rect text_rect = {.pos = at, .size = result}; - text_rect = extend_rect_centered(text_rect, (v2){.x = 8}); - if (hover_interaction(ui, mouse, auto_interaction(text_rect, var)) && (var->flags & V_TEXT)) - ui->text_input_state.hot_font = text_spec.font; - text_spec.colour = v4_lerp(base_colour, HOVERED_COLOUR, var->hover_t); + if (taken) ui_event_consume(input, event); + } + + if (flags & Paste) { + str8 string; + string.data = os_get_clipboard_text(&string.length); + for (i64 it = 0; it < string.length; it++) { + u8 cp = string.data[it]; + if (!tis->numeric || (Between(cp, '0', '9') || (cp == '.') || (cp == '-' && tis->cursor == 0))) + stream_append_byte(&sb, cp); } + } + + if (flags & Copy) { + str8 string = ui_text_input_string(); + os_set_clipboard_text(string.data, string.length); + } - draw_text(text, at, &text_spec); + if ((flags & Delete) && tis->mark != tis->cursor) + delta = 0; + + // TODO(rnp): word selection + tis->mark += delta; + tis->mark = Clamp(tis->mark, 0, tis->count); + + if (!(flags & KeepMark) && delta) { + i32 new_cursor = tis->mark; + if (flags & DeltaPicksSide) { + if (delta < 0) new_cursor = Min(tis->mark, tis->cursor); + if (delta > 0) new_cursor = Max(tis->mark, tis->cursor); + } + tis->mark = tis->cursor = new_cursor; } + + if ((flags & Delete) || sb.widx) + ui_text_input_insert(stream_to_str8(&sb)); + + if (flags || delta || sb.widx) + tis->blinker.t = 1.0; + return result; } function void -draw_table_cell(BeamformerUI *ui, Arena arena, TableCell *cell, Rect cell_rect, - TextAlignment alignment, TextSpec ts, v2 mouse) +ui_context_menu_close(void) { - f32 x_off = cell_rect.pos.x; - v2 cell_at = table_cell_align(cell, alignment, cell_rect); - ts.limits.size.w -= (cell_at.x - x_off); - cell_rect.size.w = MIN(ts.limits.size.w, cell_rect.size.w); - - /* TODO(rnp): push truncated text for hovering */ - switch (cell->kind) { - case TableCellKind_None:{ draw_text(cell->text, cell_at, &ts); }break; - case TableCellKind_Variable:{ - if (cell->var->flags & V_INPUT) { - draw_variable(ui, arena, cell->var, cell_at, mouse, ts.colour, ts); - } else if (cell->text.len) { - draw_text(cell->text, cell_at, &ts); - } - }break; - case TableCellKind_VariableGroup:{ - Variable *v = cell->var->group.first; - f32 dw = draw_text(s8("{"), cell_at, &ts).x; - while (v) { - cell_at.x += dw; - ts.limits.size.w -= dw; - dw = draw_variable(ui, arena, v, cell_at, mouse, ts.colour, ts).x; - - v = v->next; - if (v) { - cell_at.x += dw; - ts.limits.size.w -= dw; - dw = draw_text(s8(", "), cell_at, &ts).x; - } - } - cell_at.x += dw; - ts.limits.size.w -= dw; - draw_text(s8("}"), cell_at, &ts); - }break; + ui_context->context_menu_next_anchor_key = ui_node_key_zero(); + ui_context->context_menu_state_changed = 1; +} + +function void +ui_context_menu_open(UINodeKey anchor_node_key, BeamformerUIPanel *panel) +{ + if (ui_node_key_equal(ui_context->context_menu_anchor_key, anchor_node_key)) { + ui_context_menu_close(); + } else { + ui_context->context_menu_next_anchor_key = anchor_node_key; + ui_context->context_menu_next_panel = panel; + ui_context->context_menu_state_changed = 1; + ui_context->context_menu_open_t = 0; } } function void -draw_table_borders(Table *t, Rect r, f32 line_height) +ui_drag_end(void) { - if (t->column_border_thick > 0) { - v2 start = {.x = r.pos.x, .y = r.pos.y + t->cell_pad.h / 2}; - v2 end = start; - end.y += t->size.y - t->cell_pad.y; - for (i32 i = 0; i < t->columns - 1; i++) { - f32 dx = t->widths[i] + t->cell_pad.w + t->column_border_thick; - start.x += dx; - end.x += dx; - if (t->widths[i + 1] > 0) - DrawLineEx(rl_v2(start), rl_v2(end), t->column_border_thick, fade(BLACK, 0.8f)); - } + if ((beamformer_registers()->split_left_tree != beamformer_registers()->split_right_tree) && + ui_context->drag_panel) + { + beamformer_command(beamformer_command_infos[BeamformerCommandKind_SplitTree].string, + .tree_node = (u64)ui_context->drag_panel); + } else if (beamformer_registers()->drop_target_tree && ui_context->drag_panel) { + beamformer_command(beamformer_command_infos[BeamformerCommandKind_MoveTab].string, + .tree_node = (u64)ui_context->drag_panel); } + ui_context->drag_panel = 0; + ui_context->drag_end = 0; +} - if (t->row_border_thick > 0) { - v2 start = {.x = r.pos.x + t->cell_pad.w / 2, .y = r.pos.y}; - v2 end = start; - end.x += t->size.x - t->cell_pad.x; - for (i32 i = 0; i < t->rows - 1; i++) { - f32 dy = line_height + t->cell_pad.y + t->row_border_thick; - start.y += dy; - end.y += dy; - DrawLineEx(rl_v2(start), rl_v2(end), t->row_border_thick, fade(BLACK, 0.8f)); - } +function void +ui_drag_begin(BeamformerUIPanel *panel) +{ + if (!ui_context->drag_panel) { + ui_context->drag_panel = panel; + ui_context->drag_open_t = 0; + ui_context->drop_target_key = ui_node_key_zero(); + beamformer_registers()->drop_target_tree = 0; } } function v2 -draw_table(BeamformerUI *ui, Arena arena, Table *table, Rect draw_rect, TextSpec ts, v2 mouse, b32 skip_rows) +ui_node_final_position(UINode *node) { - ts.flags |= TF_LIMITED; - - v2 result = {.x = table_width(table)}; - i32 row_index = skip_rows? table_skip_rows(table, draw_rect.size.h, (f32)ts.font->baseSize) : 0; - TableIterator *it = table_iterator_new(table, TIK_CELLS, &arena, row_index, draw_rect.pos, ts.font); - for (TableCell *cell = table_iterator_next(it, &arena); - cell; - cell = table_iterator_next(it, &arena)) - { - ts.limits.size.w = draw_rect.size.w - (it->cell_rect.pos.x - it->start_x); - draw_table_cell(ui, arena, cell, it->cell_rect, it->alignment, ts, mouse); - } - draw_table_borders(table, draw_rect, (f32)ts.font->baseSize); - result.y = it->cell_rect.pos.y - draw_rect.pos.y - table->cell_pad.h / 2.0f; + v2 result = ui_node_rect(node).pos; + for (UINode *p = node->parent; !ui_node_is_nil(p); p = p->parent) + if (p->flags & UINodeFlag_ViewScroll) + result = v2_sub(result, p->view_scroll_offset); return result; } -function void -draw_view_ruler(BeamformerFrameView *view, Arena a, Rect view_rect, TextSpec ts) +function UISignal +ui_signal_from_node(UINode *node) { - v2 vr_max_p = v2_add(view_rect.pos, view_rect.size); - - v3 U = view->frame.voxel_transform.c[0].xyz; - v3 V = view->frame.voxel_transform.c[1].xyz; - v3 min = view->frame.voxel_transform.c[3].xyz; + BeamformerUI *ui = ui_context; + BeamformerInput *input = beamformer_input; - v2 start_uv = plane_uv(v3_sub(view->ruler.start, min), U, V); - v2 end_uv = plane_uv(v3_sub(view->ruler.end, min), U, V); + UISignal result = {.node = node}; + Rect nr = ui_node_rect(node); - v2 start_p = v2_add(view_rect.pos, v2_mul(start_uv, view_rect.size)); - v2 end_p = v2_add(view_rect.pos, v2_mul(end_uv, view_rect.size)); + // NOTE(rnp): use the last mouse as this matches what the user saw when they positioned + v2 mouse = ui->last_mouse; - b32 start_in_bounds = point_in_rect(start_p, view_rect); - b32 end_in_bounds = point_in_rect(end_p, view_rect); + // NOTE(rnp): apply offset + nr.pos = ui_node_final_position(node); - // TODO(rnp): this should be a ray intersection not a clamp - start_p = clamp_v2_rect(start_p, view_rect); - end_p = clamp_v2_rect(end_p, view_rect); + // NOTE(rnp): apply clipping + for (UINode *p = node->parent; !ui_node_is_nil(p); p = p->parent) + if (p->flags & UINodeFlag_Clip) + nr = rect_intersect(nr, ui_node_rect(p)); - Color rl_colour = colour_from_normalized(ts.colour); - DrawLineEx(rl_v2(end_p), rl_v2(start_p), 2, rl_colour); - if (start_in_bounds) DrawCircleV(rl_v2(start_p), 3, rl_colour); - if (end_in_bounds) DrawCircleV(rl_v2(end_p), 3, rl_colour); + // NOTE(rnp): filter when node is under context menu + b32 context_menu_descendent = 0; + for (UINode *p = node->parent; !ui_node_is_nil(p); p = p->parent) + if (p == ui->context_menu_root) + context_menu_descendent = 1; - Stream buf = arena_stream(a); - stream_append_f64(&buf, 1e3 * v3_magnitude(v3_sub(view->ruler.end, view->ruler.start)), 100); - stream_append_s8(&buf, s8(" mm")); + Rect filter_rect = {0}; + if (!context_menu_descendent && !ui_node_key_nil(ui->context_menu_anchor_key)) + filter_rect = ui_node_rect(ui->context_menu_root); - v2 txt_p = start_p; - v2 txt_s = measure_text(*ts.font, stream_to_s8(&buf)); - v2 pixel_delta = v2_sub(start_p, end_p); - if (pixel_delta.y < 0) txt_p.y -= txt_s.y; - if (pixel_delta.x < 0) txt_p.x -= txt_s.x; - if (txt_p.x < view_rect.pos.x) txt_p.x = view_rect.pos.x; - if (txt_p.x + txt_s.x > vr_max_p.x) txt_p.x -= (txt_p.x + txt_s.x) - vr_max_p.x; + b32 disabled = (node->flags & UINodeFlag_Disabled) != 0; + b32 collides = point_in_rect(mouse, nr) && !point_in_rect(mouse, filter_rect); - draw_text(stream_to_s8(&buf), txt_p, &ts); -} + result.flags |= collides * UISignalFlag_Hovering; -function v2 -draw_frame_view_controls(BeamformerUI *ui, Arena arena, BeamformerFrameView *view, Rect vr, v2 mouse) -{ - TextSpec text_spec = {.font = &ui->small_font, .flags = TF_LIMITED|TF_OUTLINED, - .colour = RULER_COLOUR, .outline_thick = 1, .outline_colour.a = 1, - .limits.size.x = vr.size.w}; - - Table *table = table_new(&arena, 3, TextAlignment_Left, TextAlignment_Left, TextAlignment_Left); - table_push_parameter_row(table, &arena, view->gamma.name, &view->gamma, s8("")); - table_push_parameter_row(table, &arena, view->threshold.name, &view->threshold, s8("")); - if (view->log_scale->bool32) - table_push_parameter_row(table, &arena, view->dynamic_range.name, &view->dynamic_range, s8("[dB]")); - - Rect table_rect = vr; - f32 height = table_extent(table, arena, text_spec.font).y; - height = MIN(height, vr.size.h); - table_rect.pos.w += 8; - table_rect.pos.y += vr.size.h - height - 8; - table_rect.size.h = height; - table_rect.size.w = vr.size.w - 16; - - return draw_table(ui, arena, table, table_rect, text_spec, mouse, 0); -} + if (!disabled) + for (BeamformerInputEvent *event = ui_event_next(input, 0); + event; + event = ui_event_next(input, event)) + { + b32 taken = 0; + b32 press = event->kind == BeamformerInputEventKind_ButtonPress; + b32 release = event->kind == BeamformerInputEventKind_ButtonRelease; + b32 event_is_mouse = (press || release) && ( + event->button_id == BeamformerButtonID_MouseLeft || + event->button_id == BeamformerButtonID_MouseRight || + event->button_id == BeamformerButtonID_MouseMiddle || + (0)); + UIMouseButtonKind mouse_button = (event->button_id == BeamformerButtonID_MouseLeft ? UIMouseButtonKind_Left : + event->button_id == BeamformerButtonID_MouseRight ? UIMouseButtonKind_Right : + event->button_id == BeamformerButtonID_MouseMiddle ? UIMouseButtonKind_Middle : + UIMouseButtonKind_Left); + + if ((node->flags & UINodeFlag_MouseClickable) && event_is_mouse && press && collides) { + ui->hot_node_key = node->key; + ui->active_node_key[mouse_button] = node->key; + + // TODO(rnp): store timestamp + // TODO(rnp): check with timestamp for double/triple click + + result.flags |= UISignalFlag_LeftPressed << mouse_button; + + taken = 1; + } -function void -draw_3D_xplane_frame_view(BeamformerUI *ui, Arena arena, Variable *var, Rect display_rect, v2 mouse) -{ - assert(var->type == VT_BEAMFORMER_FRAME_VIEW); - BeamformerFrameView *view = var->generic; + // NOTE(rnp): release, applies whenever this node is active regardless of in bounds or not. + if ((node->flags & UINodeFlag_MouseClickable) && event_is_mouse && release && + ui_node_key_equal(ui->active_node_key[mouse_button], node->key)) + { + ui->hot_node_key = ui_node_key_zero(); + ui->active_node_key[mouse_button] = ui_node_key_zero(); + result.flags |= UISignalFlag_LeftReleased << mouse_button; - f32 aspect = (f32)view->colour_image.width / (f32)view->colour_image.height; - Rect vr = display_rect; - if (aspect > 1.0f) vr.size.w = vr.size.h; - else vr.size.h = vr.size.w; + taken = 1; + } - if (vr.size.w > display_rect.size.w) { - vr.size.w -= (vr.size.w - display_rect.size.w); - vr.size.h = vr.size.w / aspect; - } else if (vr.size.h > display_rect.size.h) { - vr.size.h -= (vr.size.h - display_rect.size.h); - vr.size.w = vr.size.h * aspect; - } - vr.pos = v2_add(vr.pos, v2_scale(v2_sub(display_rect.size, vr.size), 0.5)); + // NOTE(rnp): custom scroll handling + if (node->flags & UINodeFlag_Scroll && event->kind == BeamformerInputEventKind_MouseScroll && collides) { + v2 delta = {{event->scroll.x, event->scroll.y}}; + // TODO(rnp): glfw doesn't pass these through + if (event->modifiers & BeamformerInputModifier_Shift) + swap(delta.x, delta.y); + result.scroll = v2_add(result.scroll, delta); - i32 id = -1; - if (hover_interaction(ui, mouse, auto_interaction(vr, var))) { - ray mouse_ray = ray_for_x_plane_view(ui, view, normalized_p_in_rect(vr, mouse, 0)); - v3 x_size = v3_scale(beamformer_frame_view_plane_size(ui, view), 0.5f); + taken = 1; + } - f32 rotation = x_plane_rotation_for_view_plane(view, BeamformerViewPlaneTag_XZ); - m4 x_rotation = m4_rotation_about_y(rotation); - v3 x_position = offset_x_plane_position(ui, view, BeamformerViewPlaneTag_XZ); + // NOTE(rnp): scrollable container handling + if (node->flags & UINodeFlag_ViewScroll && collides) { + v2 delta = {{event->scroll.x, event->scroll.y}}; + // TODO(rnp): glfw doesn't pass these through + if (event->modifiers & BeamformerInputModifier_Shift) + swap(delta.x, delta.y); - f32 test[2] = {0}; - test[0] = obb_raycast(x_rotation, x_size, x_position, mouse_ray); + // NOTE(rnp): if the view only has scroll in one direction we ignore the delta's direction - x_position = offset_x_plane_position(ui, view, BeamformerViewPlaneTag_YZ); - rotation = x_plane_rotation_for_view_plane(view, BeamformerViewPlaneTag_YZ); - x_rotation = m4_rotation_about_y(rotation); - test[1] = obb_raycast(x_rotation, x_size, x_position, mouse_ray); + if ((node->flags & UINodeFlag_ViewScrollX) == 0) { + if f32_equal(delta.y, 0) + delta.y = delta.x; + delta.x = 0; + } - if (test[0] >= 0 && test[1] >= 0) id = test[1] < test[0]? 1 : 0; - else if (test[0] >= 0) id = 0; - else if (test[1] >= 0) id = 1; + if ((node->flags & UINodeFlag_ViewScrollY) == 0) { + if f32_equal(delta.x, 0) + delta.x = delta.y; + delta.y = 0; + } - if (id != -1) { - view->hit_test_point = v3_add(mouse_ray.origin, v3_scale(mouse_ray.direction, test[id])); + node->view_scroll_offset = v2_add(node->view_scroll_offset, v2_scale(delta, -10.f)); + taken = 1; } + + if (taken) ui_event_consume(input, event); } - for (i32 i = 0; i < countof(view->x_plane_shifts); i++) { - Variable *it = view->x_plane_shifts + i; - Interaction interaction = auto_interaction(vr, it); - if (id == i) ui->next_interaction = interaction; - if (interaction_is_hot(ui, interaction)) it->hover_t += HOVER_SPEED * dt_for_frame; - else it->hover_t -= HOVER_SPEED * dt_for_frame; - it->hover_t = CLAMP01(it->hover_t); + // NOTE(rnp): single click dragging + if (node->flags & UINodeFlag_MouseClickable) { + for EachEnumValue(UIMouseButtonKind, k) { + if (ui_node_key_equal(ui->active_node_key[k], node->key) || + result.flags & (UISignalFlag_LeftPressed << k)) + { + result.flags |= (UISignalFlag_LeftDragging << k); + } + } } - Rectangle tex_r = {0, 0, (f32)view->colour_image.width, (f32)view->colour_image.height}; - NPatchInfo tex_np = {tex_r, 0, 0, 0, 0, NPATCH_NINE_PATCH}; - DrawTextureNPatch(make_raylib_texture(view), tex_np, rl_rect(vr), (Vector2){0}, 0, WHITE); + // NOTE(rnp): drop handling + if (node->flags & UINodeFlag_DropSite && collides + && ui_node_key_equal(ui->drop_target_key, ui_node_key_zero())) + { + ui->drop_target_key = node->key; + } - draw_frame_view_controls(ui, arena, view, vr, mouse); -} + if (node->flags & UINodeFlag_DropSite && !collides + && ui_node_key_equal(ui->drop_target_key, node->key)) + { + ui->drop_target_key = ui_node_key_zero(); + } -function void -draw_beamformer_frame_view(BeamformerUI *ui, Arena a, Variable *var, Rect display_rect, v2 mouse) -{ - assert(var->type == VT_BEAMFORMER_FRAME_VIEW); - BeamformerFrameView *view = var->generic; - BeamformerFrame *frame = &view->frame; + // TODO(rnp): double click dragging - b32 is_1d = iv3_dimension(frame->points) == 1; + // TODO(rnp): triple click dragging - f32 txt_w = measure_text(ui->small_font, s8("-288.8 mm")).w; - f32 scale_bar_size = 1.2f * txt_w + RULER_TICK_LENGTH; + result.flags |= (!f32_equal(0, result.scroll.x) * UISignalFlag_ScrolledX); + result.flags |= (!f32_equal(0, result.scroll.y) * UISignalFlag_ScrolledY); - v3 U = frame->voxel_transform.c[0].xyz; - v3 V = frame->voxel_transform.c[1].xyz; - - v2 min_uv = plane_uv(view->min_coordinate, U, V); - v2 max_uv = plane_uv(view->max_coordinate, U, V); - - v2 output_dim; - output_dim.x = v3_magnitude(U); - output_dim.y = v3_magnitude(V); - - // NOTE(rnp): may be different from UV if recompute in progress or Copy View - v2 requested_dim; - requested_dim.x = v3_magnitude(v3_sub(v3_scale(U, max_uv.x), v3_scale(U, min_uv.x))); - requested_dim.y = v3_magnitude(v3_sub(v3_scale(V, max_uv.y), v3_scale(V, min_uv.y))); - - f32 aspect = is_1d ? 1.0f : output_dim.w / output_dim.h; - - Rect vr = display_rect; - v2 scale_bar_area = {0}; - if (view->axial_scale_bar_active->bool32) { - vr.pos.y += 0.5f * (f32)ui->small_font.baseSize; - scale_bar_area.x += scale_bar_size; - scale_bar_area.y += (f32)ui->small_font.baseSize; + if (node->flags & UINodeFlag_MouseClickable && collides && + (ui_node_key_nil(ui->hot_node_key) || ui_node_key_equal(ui->hot_node_key, node->key)) && + (ui_node_key_nil(ui->active_node_key[UIMouseButtonKind_Left]) || ui_node_key_equal(ui->active_node_key[UIMouseButtonKind_Left], node->key)) && + (ui_node_key_nil(ui->active_node_key[UIMouseButtonKind_Middle]) || ui_node_key_equal(ui->active_node_key[UIMouseButtonKind_Middle], node->key)) && + (ui_node_key_nil(ui->active_node_key[UIMouseButtonKind_Right]) || ui_node_key_equal(ui->active_node_key[UIMouseButtonKind_Right], node->key))) + { + ui->hot_node_key = node->key; } - if (view->lateral_scale_bar_active->bool32) { - vr.pos.x += 0.5f * (f32)ui->small_font.baseSize; - scale_bar_area.x += (f32)ui->small_font.baseSize; - scale_bar_area.y += scale_bar_size; + if (node->flags & UINodeFlag_ViewScroll) { + v2 offset = node->view_scroll_offset; + f32 clamp_x = Max(0, node->computed_size[Axis2_X] - node->parent->computed_size[Axis2_X]); + f32 clamp_y = Max(0, node->computed_size[Axis2_Y] - node->parent->computed_size[Axis2_Y]); + node->view_scroll_offset.x = Max(0, Sign(offset.x) * Min(Abs(offset.x), clamp_x)); + node->view_scroll_offset.y = Max(0, Sign(offset.y) * Min(Abs(offset.y), clamp_y)); } - vr.size = v2_sub(vr.size, scale_bar_area); - if (aspect > 1) vr.size.h = vr.size.w / aspect; - else vr.size.w = vr.size.h * aspect; - - v2 occupied = v2_add(vr.size, scale_bar_area); - if (occupied.w > display_rect.size.w) { - vr.size.w -= (occupied.w - display_rect.size.w); - vr.size.h = vr.size.w / aspect; - } else if (occupied.h > display_rect.size.h) { - vr.size.h -= (occupied.h - display_rect.size.h); - vr.size.w = vr.size.h * aspect; + // NOTE(rnp): activate text input + if (ui_pressed(result) && !ui_node_key_equal(ui->text_input_state.node_key, node->key)) { + ui->text_input_state.changed = 1; + ui->text_input_state.next_node_key = node->flags & UINodeFlag_TextInput ? node->key : ui_node_key_zero(); } - occupied = v2_add(vr.size, scale_bar_area); - vr.pos = v2_add(vr.pos, v2_scale(v2_sub(display_rect.size, occupied), 0.5)); - Rectangle tex_r; - if (is_1d) { - tex_r = (Rectangle){0, 0, view->colour_image.width, -view->colour_image.height}; - } else { - v2 pixels_per_meter = { - .w = (f32)view->colour_image.width / output_dim.w, - .h = (f32)view->colour_image.height / output_dim.h, - }; + // NOTE(rnp): signal ended text input + if (node->flags & UINodeFlag_TextInput && + ui_node_key_equal(ui->text_input_state.last_node_key, node->key)) + { + result.flags |= UISignalFlag_TextCommit; + result.string = (str8){.length = ui->text_input_state.last_count, + .data = ui->text_input_state.last_buffer}; + } - /* NOTE(rnp): math to resize the texture without stretching when the view changes - * but the texture hasn't been (or cannot be) rebeamformed */ - v2 texture_points = v2_mul(pixels_per_meter, requested_dim); - v2 texture_start = { - .x = pixels_per_meter.x * 0.5f * (output_dim.x - requested_dim.x), - .y = pixels_per_meter.y * (output_dim.y - requested_dim.y), - }; + if (ui_pressed(result) && !context_menu_descendent) + ui_context_menu_close(); - tex_r = (Rectangle){texture_start.x, texture_start.y, texture_points.x, texture_points.y}; + if (!disabled) { + b32 hot = ui_node_key_equal(ui->hot_node_key, node->key); + if (hot) node->hot_t += HOVER_SPEED * dt_for_frame; + else node->hot_t -= HOVER_SPEED * dt_for_frame; + node->hot_t = Clamp01(node->hot_t); } - NPatchInfo tex_np = { tex_r, 0, 0, 0, 0, NPATCH_NINE_PATCH }; - DrawTextureNPatch(make_raylib_texture(view), tex_np, rl_rect(vr), (Vector2){0}, 0, WHITE); + return result; +} - v2 start_pos = vr.pos; - start_pos.y += vr.size.y; +function UINode * +ui_build_node_from_key(UINodeFlags flags, UINodeKey key) +{ + UINode *result = ui_node_from_key(key); - if (vr.size.w > 0 && view->lateral_scale_bar_active->bool32) { - do_scale_bar(ui, a, &view->lateral_scale_bar, mouse, - (Rect){.pos = start_pos, .size = vr.size}, - *view->lateral_scale_bar.scale_bar.min_value * 1e3f, - *view->lateral_scale_bar.scale_bar.max_value * 1e3f, s8(" mm")); - } + b32 first_frame = ui_node_is_nil(result); + b32 transient = ui_node_key_equal(key, ui_node_key_zero()); - start_pos = vr.pos; - start_pos.x += vr.size.x; + assert(first_frame || result->last_frame_active_index != ui_context->current_frame_index); - if (vr.size.h > 0 && view->axial_scale_bar_active->bool32) { - if (is_1d) { - v2 end_pos = start_pos; - u32 tick_count = (u32)(vr.size.y / (1.5f * (f32)ui->small_font.baseSize)); - start_pos.y += vr.size.y; - draw_ruler(ui, a, start_pos, end_pos, 0.0f, 1.0f, 0, 0, tick_count, s8(""), RULER_COLOUR, FG_COLOUR); + if (first_frame) { + result = transient ? 0 : ui_context->node_freelist; + if (!ui_node_is_nil(result)) { + SLLStackPop(ui_context->node_freelist, next_sibling); } else { - do_scale_bar(ui, a, &view->axial_scale_bar, mouse, (Rect){.pos = start_pos, .size = vr.size}, - *view->axial_scale_bar.scale_bar.max_value * 1e3f, - *view->axial_scale_bar.scale_bar.min_value * 1e3f, s8(" mm")); + result = push_struct_no_zero(transient ? ui_build_arena() : &ui_context->arena, UINode); } + zero_struct(result); } - TextSpec text_spec = {.font = &ui->small_font, .flags = TF_LIMITED|TF_OUTLINED, - .colour = RULER_COLOUR, .outline_thick = 1, .outline_colour.a = 1, - .limits.size.x = vr.size.w}; - - f32 draw_table_width = vr.size.w; - /* NOTE: avoid hover_t modification */ - Interaction viewer = auto_interaction(vr, var); - if (point_in_rect(mouse, viewer.rect)) { - ui->next_interaction = viewer; - - v2 world = screen_point_to_world_2d(mouse, vr.pos, v2_add(vr.pos, vr.size), - XZ(view->min_coordinate), - XZ(view->max_coordinate)); - world = v2_scale(world, 1e3f); - - if (is_1d) world.y = ((vr.pos.y + vr.size.y) - mouse.y) / vr.size.y; - - Stream buf = arena_stream(a); - stream_append_s8(&buf, s8("{")); - stream_append_f64(&buf, world.x, 100); - if (is_1d) stream_append_s8(&buf, s8(" mm")); - stream_append_s8(&buf, s8(", ")); - stream_append_f64(&buf, world.y, 100); - stream_append_s8(&buf, s8("}")); - - text_spec.limits.size.w -= 4.0f; - v2 txt_s = measure_text(*text_spec.font, stream_to_s8(&buf)); - v2 txt_p = { - .x = vr.pos.x + vr.size.w - txt_s.w - 4.0f, - .y = vr.pos.y + vr.size.h - txt_s.h - 4.0f, - }; - txt_p.x = Max(vr.pos.x, txt_p.x); - draw_table_width -= draw_text(stream_to_s8(&buf), txt_p, &text_spec).w; - text_spec.limits.size.w += 4.0f; + // NOTE(rnp): reassigned per frame + { + result->parent = result->first_child = result->last_child = &ui_node_nil; + result->next_sibling = result->previous_sibling = &ui_node_nil; + result->child_count = 0; } - { - Stream buf = arena_stream(a); - s8 shader = push_acquisition_kind(&buf, frame->acquisition_kind, frame->compound_count, frame->contrast_mode); - text_spec.font = &ui->font; - text_spec.limits.size.w -= 16; - v2 txt_s = measure_text(*text_spec.font, shader); - v2 txt_p = { - .x = vr.pos.x + vr.size.w - txt_s.w - 16, - .y = vr.pos.y + 4, - }; - txt_p.x = Max(vr.pos.x, txt_p.x); - draw_text(stream_to_s8(&buf), txt_p, &text_spec); - text_spec.font = &ui->small_font; - text_spec.limits.size.w += 16; + if (first_frame && !transient) { + UINodeHashBucket *hb = ui_context->node_hash_table + (key.value % UI_HASH_TABLE_COUNT); + DLLInsert(&ui_node_nil, hb->first, hb->last, result, hash_next, hash_prev); } - if (view->ruler.state != RulerState_None) draw_view_ruler(view, a, vr, text_spec); + #define X(type, name, value_type, ...) result->name = ui_top_##name(); + UI_STACK_LIST + #undef X - vr.size.w = draw_table_width; - draw_frame_view_controls(ui, a, view, vr, mouse); -} + result->last_frame_active_index = ui_context->current_frame_index; + result->key = key; + result->flags |= flags; -function v2 -draw_compute_progress_bar(BeamformerUI *ui, ComputeProgressBar *state, Rect r) -{ - if (*state->processing) state->display_t_velocity += 65.0f * dt_for_frame; - else state->display_t_velocity -= 45.0f * dt_for_frame; - - state->display_t_velocity = CLAMP(state->display_t_velocity, -10.0f, 10.0f); - state->display_t += state->display_t_velocity * dt_for_frame; - state->display_t = CLAMP01(state->display_t); - - if (state->display_t > (1.0f / 255.0f)) { - Rect outline = {.pos = r.pos, .size = {{r.size.w, (f32)ui->font.baseSize}}}; - outline = scale_rect_centered(outline, (v2){{0.96f, 0.7f}}); - Rect filled = outline; - filled.size.w *= *state->progress; - DrawRectangleRounded(rl_rect(filled), 2.0f, 0, fade(colour_from_normalized(HOVERED_COLOUR), state->display_t)); - DrawRectangleRoundedLinesEx(rl_rect(outline), 2.0f, 0, 3, fade(BLACK, state->display_t)); + if (!ui_node_is_nil(result->parent)) { + DLLInsertLast(&ui_node_nil, result->parent->first_child, result->parent->last_child, + result, next_sibling, previous_sibling); + result->parent->child_count++; } - v2 result = {{r.size.w, (f32)ui->font.baseSize}}; return result; } -function s8 -push_compute_time(Arena *arena, s8 prefix, f32 time) +function UINode * +ui_node_from_string(UINodeFlags flags, str8 string) { - Stream sb = arena_stream(*arena); - stream_append_s8(&sb, prefix); - stream_append_f64_e(&sb, time); - return arena_stream_commit(arena, &sb); + UINode *result = ui_build_node_from_key(flags, ui_key_from_string(string, ui_node_ancestor_key())); + if (flags & UINodeFlag_DrawText) { + if (ui_node_key_equal(ui_context->text_input_state.node_key, result->key)) + result->string = ui_text_input_string(); + else if (ui_node_key_equal(ui_context->text_input_state.last_node_key, result->key)) + result->string = ui_text_input_last_string(); + else + result->string = string; + } + return result; } -function v2 -draw_compute_stats_bar_view(BeamformerUI *ui, Arena arena, ComputeShaderStats *stats, - f32 compute_time_sum, TextSpec ts, Rect r, v2 mouse) +function print_format(2, 3) UINode * +ui_node_from_stringf(UINodeFlags flags, const char *format, ...) { - read_only local_persist s8 frame_labels[] = {s8_comp("0:"), s8_comp("-1:"), s8_comp("-2:"), s8_comp("-3:")}; - f32 total_times[countof(frame_labels)] = {0}; + va_list args; + va_start(args, format); + str8 string = push_str8_fv(ui_build_arena(), format, args); + va_end(args); + UINode *result = ui_node_from_string(flags, string); + return result; +} - u32 stages = stats->table.shader_count; - Table *table = table_new(&arena, countof(frame_labels), TextAlignment_Right, TextAlignment_Left); - for (u32 i = 0; i < countof(frame_labels); i++) { - TableCell *cells = table_push_row(table, &arena, TRK_CELLS)->data; - cells[0].text = frame_labels[i]; - u32 frame_index = (stats->latest_frame_index - i - 1) % countof(stats->table.times); - for (u32 j = 0; j < stages; j++) - total_times[i] += stats->table.times[frame_index][j]; - } +typedef struct { + f32 percent; +} UIDrawSliderData; - v2 result = table_extent(table, arena, ts.font); +function UI_CUSTOM_DRAW_FUNCTION(ui_custom_draw_slider) +{ + UIDrawSliderData *data = node->custom_draw_context; - f32 remaining_width = r.size.w - result.w - table->cell_pad.w; - f32 average_width = 0.8f * remaining_width; + f32 pct = data->percent; + f32 border_thick = 3.0f; + f32 bar_height_frac = 0.8f; + v2 bar_size = {{6.0f, bar_height_frac * node_rect.size.y}}; - s8 mouse_text = s8(""); - v2 text_pos; + Rect inner = rect_shrink_centered(node_rect, (v2){{2.0f * border_thick, // NOTE(rnp): raylib jank + Max(0, 2.0f * (node_rect.size.y - bar_size.y))}}); + Rect filled = inner; + filled.size.w *= pct; - u32 row_index = 0; - TableIterator *it = table_iterator_new(table, TIK_ROWS, &arena, 0, r.pos, ts.font); - for (TableRow *row = table_iterator_next(it, &arena); - row; - row = table_iterator_next(it, &arena)) - { - Rect cr = it->cell_rect; - cr.size.w = table->widths[0]; - ts.limits.size.w = cr.size.w; - draw_table_cell(ui, arena, (TableCell *)row->data, cr, table->alignment[0], ts, mouse); + Rect bar; - u32 frame_index = (stats->latest_frame_index - row_index - 1) % countof(stats->table.times); - f32 total_width = average_width * total_times[row_index] / compute_time_sum; - Rect rect; - rect.pos = v2_add(cr.pos, (v2){{cr.size.w + table->cell_pad.w , cr.size.h * 0.15f}}); - rect.size = (v2){.y = 0.7f * cr.size.h}; - for (u32 i = 0; i < stages; i++) { - rect.size.w = total_width * stats->table.times[frame_index][i] / total_times[row_index]; - Color color = colour_from_normalized(g_colour_palette[i % countof(g_colour_palette)]); - DrawRectangleRec(rl_rect(rect), color); - if (point_in_rect(mouse, rect)) { - text_pos = v2_add(rect.pos, (v2){.x = table->cell_pad.w}); - s8 name = push_s8_from_parts(&arena, s8(""), beamformer_shader_names[stats->table.shader_ids[i]], s8(": ")); - mouse_text = push_compute_time(&arena, name, stats->table.times[frame_index][i]); - } - rect.pos.x += rect.size.w; - } - row_index++; - } + bar.pos = v2_add(node_rect.pos, (v2){{pct * (node_rect.size.w - bar_size.w), + (1 - bar_height_frac) * 0.5f * node_rect.size.y}}); + bar.size = bar_size; + v4 bar_colour = v4_lerp(FG_COLOUR, FOCUSED_COLOUR, node->hot_t); - v2 start = v2_add(r.pos, (v2){.x = table->widths[0] + average_width + table->cell_pad.w}); - v2 end = v2_add(start, (v2){.y = result.y}); - DrawLineEx(rl_v2(start), rl_v2(end), 4, colour_from_normalized(FG_COLOUR)); + DrawRectangleRec(rl_rect(filled), colour_from_normalized(node->bg_colour)); + DrawRectangleRoundedLinesEx(rl_rect(inner), 0.2f, 0, border_thick, BLACK); + DrawRectangleRounded(rl_rect(bar), 0.6f, 1, colour_from_normalized(bar_colour)); +} - if (mouse_text.len) { - ts.font = &ui->small_font; - ts.flags &= ~(u32)TF_LIMITED; - ts.flags |= (u32)TF_OUTLINED; - ts.outline_colour = (v4){.a = 1}; - ts.outline_thick = 1; - draw_text(mouse_text, text_pos, &ts); - } +function UISignal +ui_slider(f32 percent, str8 tag) +{ + UINode *slider = ui_node_from_string(UINodeFlag_Clickable| + UINodeFlag_Scroll| + UINodeFlag_CustomDraw, tag); + // TODO(rnp): don't need custom draw for this when individual borders can be specified + slider->custom_draw_function = ui_custom_draw_slider; + slider->custom_draw_context = push_struct(ui_build_arena(), UIDrawSliderData); + UIDrawSliderData *data = slider->custom_draw_context; + data->percent = percent; + UISignal result = ui_signal_from_node(slider); return result; } -function void -push_table_time_row(Table *table, Arena *arena, s8 label, f32 time) +function print_format(2, 3) UISignal +ui_sliderf(f32 percent, const char *format, ...) { - assert(table->columns == 3); - TableCell *cells = table_push_row(table, arena, TRK_CELLS)->data; - cells[0].text = push_s8_from_parts(arena, s8(""), label, s8(":")); - cells[1].text = push_compute_time(arena, s8(""), time); - cells[2].text = s8("[s]"); + va_list args; + va_start(args, format); + str8 string = push_str8_fv(ui_build_arena(), format, args); + va_end(args); + UISignal result = ui_slider(percent, string); + return result; } -function void -push_table_time_row_with_fps(Table *table, Arena *arena, s8 label, f32 time) +function UISignal +ui_button(str8 string) { - assert(table->columns == 3); - TableCell *cells = table_push_row(table, arena, TRK_CELLS)->data; - - Stream sb = arena_stream(*arena); - stream_append_f64_e(&sb, time); - stream_append_s8(&sb, s8(" (")); - stream_append_f64(&sb, time > 0 ? 1.0f / time : 0, 100); - stream_append_s8(&sb, s8(")")); - - cells[0].text = label; - cells[1].text = arena_stream_commit(arena, &sb); - cells[2].text = s8("[s] (FPS)"); + UINode *node = ui_node_from_string(UINodeFlag_Clickable| + UINodeFlag_DrawBackground| + UINodeFlag_DrawBorder| + UINodeFlag_DrawText| + UINodeFlag_DrawHotEffects| + UINodeFlag_DrawActiveEffects, + string); + UISignal result = ui_signal_from_node(node); + return result; } -function void -push_table_memory_size_row(Table *table, Arena *arena, s8 label, u64 memory_size) +function print_format(1, 2) UISignal +ui_buttonf(const char *format, ...) { - TableCell *cells = table_push_row(table, arena, TRK_CELLS)->data; - Stream sb = arena_stream(*arena); - stream_append_u64(&sb, memory_size); - cells[0].text = label; - cells[1].text = arena_stream_commit(arena, &sb); - cells[2].text = s8("[B/F]"); + va_list args; + va_start(args, format); + str8 string = push_str8_fv(ui_build_arena(), format, args); + va_end(args); + UISignal result = ui_button(string); + return result; } -function v2 -draw_compute_stats_view(BeamformerUI *ui, Arena arena, Variable *view, Rect r, v2 mouse) +function UISignal +ui_toggle_button(b32 state, str8 string) { - assert(view->type == VT_COMPUTE_STATS_VIEW); + UINode *node, *outer; - read_only local_persist BeamformerComputePlan dummy_plan = {0}; - u32 selected_plan = ui->selected_parameter_block % BeamformerMaxParameterBlocks; - BeamformerComputePlan *cp = ui->beamformer_context->compute_context.compute_plans[selected_plan]; - if (!cp) cp = &dummy_plan; + UIAxisAlign(Axis2_Y, Center) + UIAxisAlign(Axis2_X, Center) + UIParent(ui_spacer(0)) + { + UIPrefHeight(ui_pct(0.75f, 1.f)) + UIPrefWidth(ui_pct(0.75f, 1.f)) + UIBorderThickness(2.f) + UIBorderColour(FG_COLOUR) + outer = ui_node_from_string(UINodeFlag_Clickable|UINodeFlag_DrawBorder, + push_str8_from_parts(ui_build_arena(), str8(""), string, str8("_outer"))); + + UIParent(outer) + UIPrefHeight(ui_pct(0.46f, 1.f)) + UIPrefWidth(ui_pct(0.46f, 1.f)) + UIBGColour(state ? FG_COLOUR : (v4){0}) + { + node = ui_node_from_string(UINodeFlag_DrawBackground| + UINodeFlag_DrawHotEffects| + UINodeFlag_DrawActiveEffects, + string); + node->hot_t = outer->hot_t; + } + } - ComputeStatsView *csv = &view->compute_stats_view; - ComputeShaderStats *stats = csv->compute_shader_stats; - f32 compute_time_sum = 0; - u32 stages = stats->table.shader_count; - TextSpec text_spec = {.font = &ui->font, .colour = FG_COLOUR, .flags = TF_LIMITED}; + UISignal result = ui_signal_from_node(outer); + return result; +} - ui_blinker_update(&csv->blink, BLINK_SPEED); +function print_format(2, 3) UISignal +ui_toggle_buttonf(b32 state, const char *format, ...) +{ + va_list args; + va_start(args, format); + str8 string = push_str8_fv(ui_build_arena(), format, args); + va_end(args); + UISignal result = ui_toggle_button(state, string); + return result; +} - for (u32 index = 0; index < stages; index++) - compute_time_sum += stats->average_times[index]; +function UISignal +ui_label(str8 string) +{ + UINode *node = ui_node_from_string(UINodeFlag_DrawText, string); + UISignal result = ui_signal_from_node(node); + return result; +} - v2 result = {0}; +function print_format(1, 2) UISignal +ui_labelf(const char *format, ...) +{ + va_list args; + va_start(args, format); + str8 string = push_str8_fv(ui_build_arena(), format, args); + va_end(args); + UISignal result = ui_label(string); + return result; +} - Table *table = table_new(&arena, 2, TextAlignment_Left, TextAlignment_Left, TextAlignment_Left); - switch (csv->kind) { - case ComputeStatsViewKind_Average:{ - da_reserve(&arena, table, stages); - for (u32 i = 0; i < stages; i++) { - push_table_time_row(table, &arena, beamformer_shader_names[stats->table.shader_ids[i]], - stats->average_times[i]); - } - }break; - case ComputeStatsViewKind_Bar:{ - result = draw_compute_stats_bar_view(ui, arena, stats, compute_time_sum, text_spec, r, mouse); - r.pos = v2_add(r.pos, (v2){.y = result.y}); - }break; - InvalidDefaultCase; - } +function UISignal +ui_label_button(str8 string) +{ + UINode *node = ui_node_from_string(UINodeFlag_DrawText| + UINodeFlag_Clickable| + UINodeFlag_DrawHotEffects| + UINodeFlag_DrawActiveEffects, + string); + UISignal result = ui_signal_from_node(node); + return result; +} + +function print_format(1, 2) UISignal +ui_label_buttonf(const char *format, ...) +{ + va_list args; + va_start(args, format); + str8 string = push_str8_fv(ui_build_arena(), format, args); + va_end(args); + UISignal result = ui_label_button(string); + return result; +} - u32 rf_size = ui->beamformer_context->compute_context.rf_buffer.active_rf_size; - push_table_time_row_with_fps(table, &arena, s8("Compute Total:"), compute_time_sum); - push_table_time_row_with_fps(table, &arena, s8("RF Upload Delta:"), stats->rf_time_delta_average); - push_table_memory_size_row(table, &arena, s8("Input RF Size:"), rf_size); - if (rf_size != cp->rf_size) - push_table_memory_size_row(table, &arena, s8("DAS RF Size:"), cp->rf_size); +function UISignal +ui_text_box(str8 string) +{ + UINode *node = ui_node_from_string(UINodeFlag_TextInput| + UINodeFlag_DrawText| + UINodeFlag_Clickable| + UINodeFlag_DrawHotEffects| + UINodeFlag_DrawActiveEffects, + string); + UISignal result = ui_signal_from_node(node); + return result; +} - result = v2_add(result, table_extent(table, arena, text_spec.font)); +function print_format(1, 2) UISignal +ui_text_boxf(const char *format, ...) +{ + va_list args; + va_start(args, format); + str8 string = push_str8_fv(ui_build_arena(), format, args); + va_end(args); + UISignal result = ui_text_box(string); + return result; +} - u32 row_index = 0; - TableIterator *it = table_iterator_new(table, TIK_ROWS, &arena, 0, r.pos, text_spec.font); - for (TableRow *row = table_iterator_next(it, &arena); - row; - row = table_iterator_next(it, &arena), row_index++) - { - Table *t = it->frame.table; - Rect cell_rect = it->cell_rect; - for (i32 column = 0; column < t->columns; column++) { - TableCell *cell = (TableCell *)it->row->data + column; - cell_rect.size.w = t->widths[column]; - text_spec.limits.size.w = r.size.w - (cell_rect.pos.x - it->start_x); - - if (column == 0 && row_index < stages && - vk_pipeline_valid(cp->vulkan_pipelines[row_index]) == 0 && - stats->table.shader_ids[row_index] != BeamformerShaderKind_Hilbert) - { - text_spec.colour = v4_lerp(FG_COLOUR, FOCUSED_COLOUR, ease_in_out_quartic(csv->blink.t)); - } else { - text_spec.colour = FG_COLOUR; - } +function b32 +ui_tweak_f32_compute_variable(UISignal signal, f32 *value, f32 text_scale, f32 scroll_scale, v2 limits) +{ + b32 result = 0; + if (signal.flags) { + f64 new_value = *value; + if (signal.flags & UISignalFlag_TextCommit && ui_number_conversion_f64(signal.string, &new_value)) + new_value *= text_scale; - draw_table_cell(ui, arena, cell, cell_rect, t->alignment[column], text_spec, mouse); + if (signal.flags & UISignalFlag_ScrolledY) + new_value += scroll_scale * signal.scroll.y; - cell_rect.pos.x += cell_rect.size.w + t->cell_pad.w; - } - } + new_value = Clamp(new_value, limits.x, limits.y); + result = !f32_equal(*value, (f32)new_value); + *value = (f32)new_value; + } return result; } -function v2 -draw_live_controls_view(BeamformerUI *ui, Variable *var, Rect r, v2 mouse, Arena arena) -{ - BeamformerLiveImagingParameters *lip = &ui->shared_memory->live_imaging_parameters; - BeamformerLiveControlsView *lv = var->generic; +typedef struct { + v2 uv_start; + v2 uv_end; + BeamformerFrameView *view; +} BeamformerCustomDrawFrameViewData; + +function UI_CUSTOM_DRAW_FUNCTION(beamformer_custom_draw_frame_view) +{ + // TODO(rnp): we should always just draw inline, requires no raylib + BeamformerCustomDrawFrameViewData *data = node->custom_draw_context; + BeamformerFrameView *view = data->view; + Rectangle tex_r = { + data->uv_start.x * view->colour_image.width, + data->uv_start.y * view->colour_image.height, + data->uv_end.x * view->colour_image.width, + data->uv_end.y * view->colour_image.height, + }; + NPatchInfo tex_np = { tex_r, 0, 0, 0, 0, NPATCH_NINE_PATCH }; + DrawTextureNPatch(make_raylib_texture(view), tex_np, rl_rect(node_rect), (Vector2){0}, 0, WHITE); - TextSpec text_spec = {.font = &ui->font, .colour = FG_COLOUR, .flags = TF_LIMITED}; + TextSpec text_spec = {.font = &ui_context->small_font, .flags = TF_LIMITED|TF_OUTLINED, + .colour = RULER_COLOUR, .outline_thick = 1, .outline_colour.a = 1, + .limits.size.x = node_rect.size.w}; + if (view->kind != BeamformerFrameViewKind_3DXPlane && view->ruler.state != RulerState_None) + draw_view_ruler(view, *ui_build_arena(), node_rect, text_spec); +} - v2 slider_size = {{MIN(140.0f, r.size.w), (f32)ui->font.baseSize}}; - v2 button_size = {{MIN(r.size.w, slider_size.x + (f32)ui->font.baseSize), (f32)ui->font.baseSize * 1.5f}}; +function b32 +ui_rebuild_das_transform(u32 parameter_block, i32 dimension, v3 min, v3 max) +{ + BeamformerUI *ui = ui_context; - f32 text_off = r.pos.x + 0.5f * MAX(0, (r.size.w - slider_size.w - (f32)ui->font.baseSize)); - f32 slider_off = r.pos.x + 0.5f * (r.size.w - slider_size.w); - f32 button_off = r.pos.x + 0.5f * (r.size.w - button_size.w); + b32 result = 0; + m4 new_transform = m4_identity(); - text_spec.limits.size.w = r.size.w - (text_off - r.pos.x); + BeamformerParameterBlock *pb = beamformer_parameter_block(beamformer_context->shared_memory, parameter_block); - v2 at = {{text_off, r.pos.y}}; + m4 das_transform = pb->parameters.das_voxel_transform; - if (popcount_u64(lip->acquisition_kind_enabled_flags) > 1) { - u32 kind = lip->acquisition_kind; - s8 kind_string = kind < BeamformerAcquisitionKind_Count ? beamformer_acquisition_kind_strings[kind] - : s8("Invalid"); - v2 label_size = draw_text(s8("Acquisition:"), at, &text_spec); + switch (dimension) { + case 1:{new_transform = das_transform_1d(min, max);}break; + case 3:{new_transform = das_transform_3d(min, max);}break; + case 2:{ + v3 U = v3_normalize(das_transform.c[0].xyz); + v3 V = v3_normalize(das_transform.c[1].xyz); + v3 N = cross(V, U); - text_spec.colour = v4_lerp(FG_COLOUR, HOVERED_COLOUR, lv->acquisition_menu.hover_t); - text_spec.limits.size.w -= label_size.x + (f32)ui->small_font.baseSize / 2; + v2 min_2d = {{min.E[0], min.E[1]}}; + v2 max_2d = {{max.E[0], max.E[1]}}; - Rect menu = {.pos = at}; - menu.pos.x += label_size.x + (f32)ui->small_font.baseSize / 2; - menu.size.h = (f32)ui->small_font.baseSize + TITLE_BAR_PAD; - menu.size.w = draw_text(kind_string, menu.pos, &text_spec).x; + new_transform = das_transform_2d_with_normal(N, min_2d, max_2d, 0); - text_spec.colour = FG_COLOUR; - text_spec.limits.size.w = r.size.w - (text_off - r.pos.x); + v3 rotation_axis = cross(v3_normalize(new_transform.c[0].xyz), N); - Interaction interaction = {.kind = InteractionKind_AcquisitionMenu, - .var = &lv->acquisition_menu, - .rect = menu}; - hover_interaction(ui, mouse, interaction); + m4 R = m4_rotation_about_axis(rotation_axis, ui->beamform_plane); + m4 T = m4_translation(v3_scale(m4_mul_v3(R, N), ui->off_axis_position)); - at.y += label_size.y; + new_transform = m4_mul(T, m4_mul(R, new_transform)); + }break; } - v4 hsv_power_slider = {{0.35f * ease_in_out_cubic(1.0f - lip->transmit_power), 0.65f, 0.65f, 1}}; - at.y += draw_text(s8("Power:"), at, &text_spec).y; - at.x = slider_off; - at.y += draw_variable_slider(ui, &lv->transmit_power, (Rect){.pos = at, .size = slider_size}, - lip->transmit_power, hsv_to_rgb(hsv_power_slider), mouse); - - at.x = text_off; - at.y += draw_text(s8("TGC:"), at, &text_spec).y; - at.x = slider_off; - for (u32 i = 0; i < countof(lip->tgc_control_points); i++) { - Variable *v = lv->tgc_control_points + i; - at.y += draw_variable_slider(ui, v, (Rect){.pos = at, .size = slider_size}, - lip->tgc_control_points[i], g_colour_palette[1], mouse); - - if (interaction_is_hot(ui, auto_interaction(r, v))) - lv->hot_field_flag = BeamformerLiveImagingDirtyFlags_TGCControlPoints; - } + new_transform = m4_mul(new_transform, m4_inverse(das_transform)); - at.x = button_off; - at.y += (f32)ui->font.baseSize * 0.5f; - at.y += draw_fancy_button(ui, &lv->stop_button, lv->stop_button.name, - (Rect){.pos = at, .size = button_size}, - BORDER_COLOUR, mouse, text_spec).y; - - if (lip->save_enabled) { - b32 active = lip->save_active; - s8 label = lv->save_button.cycler.labels[active % lv->save_button.cycler.cycle_length]; - - f32 save_t = ui_blinker_update(&lv->save_button_blink, BLINK_SPEED); - v4 border_colour = BORDER_COLOUR; - if (active) border_colour = v4_lerp(BORDER_COLOUR, FOCUSED_COLOUR, ease_in_out_cubic(save_t)); - - at.x = text_off; - at.y += draw_text(s8("File Tag:"), at, &text_spec).y; - at.x += (f32)text_spec.font->baseSize / 2; - text_spec.limits.size.w -= (f32)text_spec.font->baseSize; - - v4 save_text_colour = FG_COLOUR; - if (lip->save_name_tag_length <= 0) - save_text_colour.a = 0.6f; - at.y += draw_variable(ui, arena, &lv->save_text, at, mouse, save_text_colour, text_spec).y; - text_spec.limits.size.w += (f32)text_spec.font->baseSize; - - at.x = button_off; - at.y += (f32)ui->font.baseSize * 0.25f; - at.y += draw_fancy_button(ui, &lv->save_button, label, (Rect){.pos = at, .size = button_size}, - border_colour, mouse, text_spec).y; - - if (interaction_is_hot(ui, auto_interaction(r, &lv->save_text))) - lv->hot_field_flag = BeamformerLiveImagingDirtyFlags_SaveNameTag; - if (interaction_is_hot(ui, auto_interaction(r, &lv->save_button))) - lv->hot_field_flag = BeamformerLiveImagingDirtyFlags_SaveData; + BeamformerComputePlan *cp = beamformer_context->compute_context.compute_plans[parameter_block]; + if (cp) { + result |= !m4_equal(new_transform, cp->ui_voxel_transform); + memory_copy(cp->ui_voxel_transform.E, new_transform.E, sizeof(new_transform)); } - if (interaction_is_hot(ui, auto_interaction(r, &lv->transmit_power))) - lv->hot_field_flag = BeamformerLiveImagingDirtyFlags_TransmitPower; - if (interaction_is_hot(ui, auto_interaction(r, &lv->stop_button))) - lv->hot_field_flag = BeamformerLiveImagingDirtyFlags_StopImaging; + if (result) { + mark_parameter_block_region_dirty(beamformer_context->shared_memory, parameter_block, + BeamformerParameterBlockRegion_Parameters); + } - v2 result = {{r.size.w, at.y - r.pos.y}}; return result; } -struct variable_iterator { Variable *current; }; -function i32 -variable_iterator_next(struct variable_iterator *it) +function void +ui_scroll_begin(Axis2 scroll_axis) { - i32 result = 0; + UINode *outer, *inner, *clip, *child; - if (it->current->type == VT_GROUP && it->current->group.expanded) { - it->current = it->current->group.first; - result++; - } else { - while (it->current) { - if (it->current->next) { - it->current = it->current->next; - break; - } - it->current = it->current->parent; - result--; - } + UIChildLayoutAxis(Axis2_Y) + UIParent(ui_spacer(0)) + { + UIChildLayoutAxis(Axis2_X) + UIFontSize(30.f) + outer = ui_node_from_string(UINodeFlag_Scroll, str8("###scroll_box")); + + ui_padh(UI_NODE_PAD); } - return result; -} + UIParent(outer) + { + ui_padw(UI_NODE_PAD); + UIChildLayoutAxis(Axis2_Y) + inner = ui_node_from_string(0, str8("###scroll_inner")); + } -function v2 -draw_ui_view_menu(BeamformerUI *ui, Variable *group, Arena arena, Rect r, v2 mouse, TextSpec text_spec) -{ - assert(group->type == VT_GROUP); - Table *table = table_new(&arena, 0, TextAlignment_Left, TextAlignment_Right); - table->row_border_thick = 2.0f; - table->cell_pad = (v2){{16.0f, 8.0f}}; - - i32 nesting = 0; - for (struct variable_iterator it = {group->group.first}; - it.current; - nesting = variable_iterator_next(&it)) + UINodeFlags axis_flags; + switch (scroll_axis) { + InvalidDefaultCase; + case Axis2_Count:{axis_flags = UINodeFlag_ViewScroll; }break; + case Axis2_X:{ axis_flags = UINodeFlag_ViewScrollX;}break; + case Axis2_Y:{ axis_flags = UINodeFlag_ViewScrollY;}break; + } + UIParent(inner) + clip = ui_node_from_string(axis_flags| + UINodeFlag_Clip| + UINodeFlag_AllowOverflow| + 0, str8("###scroll_clip")); + + UIParent(clip) { - (void)nesting; - assert(nesting == 0); - Variable *var = it.current; - TableCell *cells = table_push_row(table, &arena, TRK_CELLS)->data; - switch (var->type) { - case VT_B32: - case VT_CYCLER: - { - cells[0] = (TableCell){.text = var->name}; - cells[1] = table_variable_cell(&arena, var); - }break; - case VT_UI_BUTTON:{ - cells[0] = (TableCell){.text = var->name, .kind = TableCellKind_Variable, .var = var}; - }break; - InvalidDefaultCase; - } + UIPrefWidth(ui_children_sum(1.f)) + UIPrefHeight(ui_children_sum(1.f)) + child = ui_node_from_string(0, str8("###scroll_child")); } - r.size = table_extent(table, arena, text_spec.font); - return draw_table(ui, arena, table, r, text_spec, mouse, 0); + ui_push_parent(child); } -function v2 -draw_ui_view_listing(BeamformerUI *ui, Variable *group, Arena arena, Rect r, v2 mouse, TextSpec text_spec) +function void +ui_scroll_end(void) { - assert(group->type == VT_GROUP); - Table *table = table_new(&arena, 0, TextAlignment_Left, TextAlignment_Left, TextAlignment_Right); + BeamformerUI *ui = ui_context; + UINode *child = ui_pop_parent(); + UINode *clip = child->parent; + UINode *inner = clip->parent; + UINode *outer = inner->parent; + + v2 scroll_offset = clip->view_scroll_offset; + + str8 labels[2][2] = { + [Axis2_X] = {str8_comp("<"), str8_comp(">")}, + [Axis2_Y] = {str8_comp("^"), str8_comp("v")}, + }; - i32 nesting = 0; - for (struct variable_iterator it = {group->group.first}; - it.current; - nesting = variable_iterator_next(&it)) + f32 btn_size = (f32)ui_font_for_node(outer).baseSize; + + UINode *axis_parents[] = {[Axis2_X] = inner, [Axis2_Y] = outer}; + for EachElement(axis_parents, axis) + if (clip->flags & (UINodeFlag_ViewScrollX << axis)) + UIParent(axis_parents[axis]) { - while (nesting > 0) { - table = table_begin_subtable(table, &arena, TextAlignment_Left, - TextAlignment_Center, TextAlignment_Right); - nesting--; + b32 build_scrollbar = 2.f * btn_size < clip->computed_size[axis] && + child->computed_size[axis] > clip->computed_size[axis]; + + // NOTE(rnp): vertical scroll bar shares padding on bottom with horizontal + // scroll bar so padding was already pushed, if we aren't drawing the horizontal + // scroll bar we need to avoid a double pad + if (axis == Axis2_Y || build_scrollbar) { + UIChildLayoutAxis(axis2_flip(axis)) + ui_pads(UI_NODE_PAD); } - while (nesting < 0) { table = table_end_subtable(table); nesting++; } - Variable *var = it.current; - switch (var->type) { - case VT_CYCLER: - case VT_BEAMFORMER_VARIABLE: + if (build_scrollbar) + UIAxisSize(axis2_flip(axis), ui_px(12.f, 1.f)) + UIChildLayoutAxis(axis) + UIParent(axis_parents[axis]) { - s8 suffix = s8(""); - if (var->type == VT_BEAMFORMER_VARIABLE) - suffix = var->beamformer_variable.suffix; - table_push_parameter_row(table, &arena, var->name, var, suffix); - }break; - case VT_GROUP:{ - VariableGroup *g = &var->group; - - TableCell *cells = table_push_row(table, &arena, TRK_CELLS)->data; - cells[0] = (TableCell){.text = var->name, .kind = TableCellKind_Variable, .var = var}; - - if (!g->expanded) { - Stream sb = arena_stream(arena); - stream_append_variable_group(&sb, var); - cells[1].kind = TableCellKind_VariableGroup; - cells[1].text = arena_stream_commit(&arena, &sb); - cells[1].var = var; - - Variable *v = g->first; - assert(!v || v->type == VT_BEAMFORMER_VARIABLE); - /* NOTE(rnp): assume the suffix is the same for all elements */ - if (v) cells[2].text = v->beamformer_variable.suffix; - } - }break; - InvalidDefaultCase; - } - } - - v2 result = table_extent(table, arena, text_spec.font); - draw_table(ui, arena, table, r, text_spec, mouse, 0); - return result; -} + UINode *parent = axis_parents[axis]; + f32 d_size = child->computed_size[axis] - clip->computed_size[axis]; + f32 used_pct = clip->computed_size[axis] / child->computed_size[axis]; + f32 rem_pct = 1.f - used_pct; + f32 before_pct = rem_pct - (d_size - scroll_offset.E[axis]) / child->computed_size[axis]; + f32 after_pct = rem_pct - before_pct; + + UINode *scroll_container; + UIAxisAlign(axis2_flip(axis), Center) + UIAxisSize(axis, ui_px(parent->computed_size[axis], 1.f)) + scroll_container = ui_spacer(0); + + UIAxisSize(axis2_flip(axis), ui_pct(1.f, 0.5f)) + UIFontSize(outer->font_size) + UIParent(scroll_container) + { + UISignal signal; + // TODO(rnp): icons + UIFlags(UINodeFlag_IconText) + UIAxisSize(axis2_flip(axis), ui_text_dim(1.f, 1.f)) + UIAxisSize(axis, ui_text_dim(1.f, 1.f)) + signal = ui_label_button(labels[axis][0]); + if (signal.flags & UISignalFlag_LeftPressed) { + // TODO(rnp): handle repeat + scroll_offset.E[axis] -= btn_size * 0.5f; + } -function Rect -draw_ui_view_container(BeamformerUI *ui, Variable *var, v2 mouse, Rect bounds) -{ - UIView *fw = &var->view; - Rect result = fw->rect; - if (fw->rect.size.x > 0 && fw->rect.size.y > 0) { - f32 line_height = (f32)ui->small_font.baseSize; - - f32 pad = MAX(line_height + 5.0f, UI_REGION_PAD); - if (fw->rect.pos.y < pad) - fw->rect.pos.y += pad - fw->rect.pos.y; - result = fw->rect; - - f32 delta_x = (result.pos.x + result.size.x) - (bounds.size.x + bounds.pos.x); - if (delta_x > 0) { - result.pos.x -= delta_x; - result.pos.x = MAX(0, result.pos.x); - } + ui_pads(3.f); - Rect container = result; - if (fw->close) { - container.pos.y -= 5 + line_height; - container.size.y += 2 + line_height; - Rect handle = {container.pos, (v2){.x = container.size.w, .y = 2 + line_height}}; - Rect close; - hover_interaction(ui, mouse, auto_interaction(container, var)); - cut_rect_horizontal(handle, handle.size.w - handle.size.h - 6, 0, &close); - close.size.w = close.size.h; - DrawRectangleRounded(rl_rect(handle), 0.1f, 0, colour_from_normalized(BG_COLOUR)); - DrawRectangleRoundedLinesEx(rl_rect(handle), 0.2f, 0, 2, BLACK); - draw_close_button(ui, fw->close, mouse, close, (v2){{0.45f, 0.45f}}); - } else { - hover_interaction(ui, mouse, auto_interaction(container, var)); - } - f32 roundness = 12.0f / fw->rect.size.y; - DrawRectangleRounded(rl_rect(result), roundness / 2.0f, 0, colour_from_normalized(BG_COLOUR)); - DrawRectangleRoundedLinesEx(rl_rect(result), roundness, 0, 2, BLACK); - } - return result; -} + UISignalFlags bar_flags = 0; -function void -draw_ui_view(BeamformerUI *ui, Variable *ui_view, Rect r, v2 mouse, TextSpec text_spec) -{ - assert(ui_view->type == VT_UI_VIEW || ui_view->type == VT_UI_MENU || ui_view->type == VT_UI_TEXT_BOX); + UIBorderColour((v4){0}) + UIFlags(UINodeFlag_Clickable|UINodeFlag_DrawBorder|UINodeFlag_DrawHotEffects) + UIAxisSize(axis, ui_pct(before_pct, 0.5f)) + bar_flags |= ui_signal_from_node(ui_node_from_string(0, str8("###before"))).flags; - UIView *view = &ui_view->view; + UIBGColour(FG_COLOUR) + UIAxisSize(axis, ui_pct(used_pct, 0.5f)) + UIFlags(UINodeFlag_Clickable|UINodeFlag_DrawBackground|UINodeFlag_DrawHotEffects) + signal = ui_signal_from_node(ui_node_from_string(0, str8("###used"))); + bar_flags |= signal.flags; - if (view->flags & UIViewFlag_Floating) { - r = draw_ui_view_container(ui, ui_view, mouse, r); - } else { - if (view->rect.size.h - r.size.h < view->rect.pos.h) - view->rect.pos.h = view->rect.size.h - r.size.h; + UIBorderColour((v4){0}) + UIFlags(UINodeFlag_Clickable|UINodeFlag_DrawBorder|UINodeFlag_DrawHotEffects) + UIAxisSize(axis, ui_pct(after_pct , 0.5f)) + bar_flags |= ui_signal_from_node(ui_node_from_string(0, str8("###after"))).flags; - if (view->rect.size.h - r.size.h < 0) - view->rect.pos.h = 0; + if (bar_flags & (UISignalFlag_Dragging|UISignalFlag_Pressed)) { + f32 off_pct = rect_uv(ui->last_mouse, ui_node_rect(clip)).E[axis] - 0.5f * used_pct; + scroll_offset.E[axis] = Clamp01(off_pct) * child->computed_size[axis]; + } - r.pos.y -= view->rect.pos.h; - } + ui_pads(3.f); - v2 size = {0}; + UIFlags(UINodeFlag_IconText) + UIAxisSize(axis2_flip(axis), ui_text_dim(1.f, 1.f)) + UIAxisSize(axis, ui_text_dim(1.f, 1.f)) + signal = ui_label_button(labels[axis][1]); + if (signal.flags & UISignalFlag_LeftPressed) { + // TODO(rnp): handle repeat + scroll_offset.E[axis] += btn_size * 0.5f; + } + } - Variable *var = view->child; - switch (var->type) { - case VT_GROUP:{ - if (ui_view->type == VT_UI_MENU) - size = draw_ui_view_menu(ui, var, ui->arena, r, mouse, text_spec); - else { - size = draw_ui_view_listing(ui, var, ui->arena, r, mouse, text_spec); - } - }break; - case VT_BEAMFORMER_FRAME_VIEW: { - BeamformerFrameView *bv = var->generic; - if (frame_view_ready_to_present(ui, bv)) { - if (bv->kind == BeamformerFrameViewKind_3DXPlane) - draw_3D_xplane_frame_view(ui, ui->arena, var, r, mouse); - else - draw_beamformer_frame_view(ui, ui->arena, var, r, mouse); + // NOTE(rnp): vertical scroll bar needs padding next to it but must share + // padding on the bottom with the horizontal scrollbar + if (axis == Axis2_Y) ui_padw(UI_NODE_PAD); } - } break; - case VT_COMPUTE_PROGRESS_BAR: { - size = draw_compute_progress_bar(ui, &var->compute_progress_bar, r); - } break; - case VT_COMPUTE_STATS_VIEW:{ size = draw_compute_stats_view(ui, ui->arena, var, r, mouse); }break; - case VT_LIVE_CONTROLS_VIEW:{ - if (view->rect.size.h - r.size.h < 0) - r.pos.y += 0.5f * (r.size.h - view->rect.size.h); - if (ui->shared_memory->live_imaging_parameters.active) - size = draw_live_controls_view(ui, var, r, mouse, ui->arena); - }break; - InvalidDefaultCase; } - view->rect.size = size; + // TODO(rnp): view scroll is being added to ViewScroll node in ui_signal_from_node maybe we are ignoring it? + UISignal signal = ui_signal_from_node(outer); + scroll_offset = v2_sub(scroll_offset, v2_scale(signal.scroll, btn_size * 0.5f)); + + scroll_offset.x = Max(0, Min(scroll_offset.x, child->computed_size[Axis2_X] - clip->computed_size[Axis2_X])); + scroll_offset.y = Max(0, Min(scroll_offset.y, child->computed_size[Axis2_Y] - clip->computed_size[Axis2_Y])); + clip->view_scroll_offset = scroll_offset; } -function void -draw_layout_variable(BeamformerUI *ui, Variable *var, Rect draw_rect, v2 mouse) +typedef struct { + Axis2 axis; + f32 start_value; + f32 end_value; + u32 segments; +} UIDrawScaleBarData; + +function UI_CUSTOM_DRAW_FUNCTION(ui_custom_draw_scale_bar) { - if (var->type != VT_UI_REGION_SPLIT) { - v2 shrink = {.x = UI_REGION_PAD, .y = UI_REGION_PAD}; - draw_rect = shrink_rect_centered(draw_rect, shrink); - draw_rect.size = v2_floor(draw_rect.size); - BeginScissorMode((i32)draw_rect.pos.x, (i32)draw_rect.pos.y, (i32)draw_rect.size.w, (i32)draw_rect.size.h); - draw_rect = draw_title_bar(ui, ui->arena, var, draw_rect, mouse); - EndScissorMode(); - } + UIDrawScaleBarData *info = node->custom_draw_context; - /* TODO(rnp): post order traversal of the ui tree will remove the need for this */ - if (!CheckCollisionPointRec(rl_v2(mouse), rl_rect(draw_rect))) - mouse = (v2){.x = F32_INFINITY, .y = F32_INFINITY}; - - draw_rect.size = v2_floor(draw_rect.size); - BeginScissorMode((i32)draw_rect.pos.x, (i32)draw_rect.pos.y, (i32)draw_rect.size.w, (i32)draw_rect.size.h); - switch (var->type) { - case VT_UI_VIEW: { - hover_interaction(ui, mouse, auto_interaction(draw_rect, var)); - TextSpec text_spec = {.font = &ui->font, .colour = FG_COLOUR, .flags = TF_LIMITED}; - draw_ui_view(ui, var, draw_rect, mouse, text_spec); - } break; - case VT_UI_REGION_SPLIT: { - RegionSplit *rs = &var->region_split; - - Rect split = {0}, hover = {0}; - switch (rs->direction) { - case RSD_VERTICAL: { - split_rect_vertical(draw_rect, rs->fraction, 0, &split); - split.pos.x += UI_REGION_PAD; - split.pos.y -= UI_SPLIT_HANDLE_THICK / 2; - split.size.h = UI_SPLIT_HANDLE_THICK; - split.size.w -= 2 * UI_REGION_PAD; - hover = extend_rect_centered(split, (v2){.y = 0.75f * UI_REGION_PAD}); - } break; - case RSD_HORIZONTAL: { - split_rect_horizontal(draw_rect, rs->fraction, 0, &split); - split.pos.x -= UI_SPLIT_HANDLE_THICK / 2; - split.pos.y += UI_REGION_PAD; - split.size.w = UI_SPLIT_HANDLE_THICK; - split.size.h -= 2 * UI_REGION_PAD; - hover = extend_rect_centered(split, (v2){.x = 0.75f * UI_REGION_PAD}); - } break; - } + b32 draw_plus = Sign(info->end_value) != Sign(info->start_value); - Interaction drag = {.kind = InteractionKind_Drag, .rect = hover, .var = var}; - hover_interaction(ui, mouse, drag); + Font font = ui_font_for_node(node); + v2 start_point = node_rect.pos; + v2 end_point = node_rect.pos; - v4 colour = HOVERED_COLOUR; - colour.a = var->hover_t; - DrawRectangleRounded(rl_rect(split), 0.6f, 0, colour_from_normalized(colour)); - } break; - InvalidDefaultCase; - } - EndScissorMode(); -} + if (info->axis == Axis2_Y) start_point.y += node_rect.size.y; + else end_point.x += node_rect.size.x; -function void -draw_ui_regions(BeamformerUI *ui, Rect window, v2 mouse) -{ - struct region_frame { - Variable *var; - Rect rect; - } init[16]; + end_point = v2_sub(end_point, start_point); - struct { - struct region_frame *data; - da_count count; - da_count capacity; - } stack = {init, 0, countof(init)}; + rlPushMatrix(); + rlTranslatef(start_point.x, start_point.y, 0); + rlRotatef(atan2_f32(end_point.y, end_point.x) * 180 / PI, 0, 0, 1); - TempArena arena_savepoint = begin_temp_arena(&ui->arena); + Stream buf = arena_stream(*ui_build_arena()); + f32 inc = v2_magnitude(end_point) / (f32)info->segments; + f32 value_inc = (info->end_value - info->start_value) / (f32)info->segments; + f32 value = info->start_value; - *da_push(&ui->arena, &stack) = (struct region_frame){ui->regions, window}; - while (stack.count) { - struct region_frame *top = stack.data + --stack.count; - Rect rect = top->rect; - draw_layout_variable(ui, top->var, rect, mouse); - - if (top->var->type == VT_UI_REGION_SPLIT) { - Rect first, second; - RegionSplit *rs = &top->var->region_split; - switch (rs->direction) { - case RSD_VERTICAL: { - split_rect_vertical(rect, rs->fraction, &first, &second); - } break; - case RSD_HORIZONTAL: { - split_rect_horizontal(rect, rs->fraction, &first, &second); - } break; - } + v2 sp = {0}, ep = {.y = RULER_TICK_LENGTH}; + v2 tp = {{(f32)font.baseSize / 2.0f, ep.y + RULER_TEXT_PAD}}; - *da_push(&ui->arena, &stack) = (struct region_frame){rs->right, second}; - *da_push(&ui->arena, &stack) = (struct region_frame){rs->left, first}; - } + TextSpec text_spec = {.font = &font, .rotation = 90.0f, .colour = node->text_colour, .flags = TF_ROTATED}; + if (node->flags & UINodeFlag_DrawHotEffects) + text_spec.colour = v4_lerp(text_spec.colour, HOVERED_COLOUR, node->hot_t); + + Color rl_txt_colour = colour_from_normalized(node->text_colour); + for (u32 j = 0; j <= info->segments; j++) { + DrawLineEx(rl_v2(sp), rl_v2(ep), 4.f, rl_txt_colour); + + stream_reset(&buf, 0); + if (draw_plus && value > 0) stream_append_byte(&buf, '+'); + stream_append_f64(&buf, value, Abs(value_inc) < 1 ? 100 : 10); + stream_append_s8(&buf, s8("mm")); + draw_text(stream_to_str8(&buf), tp, &text_spec); + + value += value_inc; + sp.x += inc; + ep.x += inc; + tp.x += inc; } - end_temp_arena(arena_savepoint); + rlPopMatrix(); } -function void -draw_floating_widgets(BeamformerUI *ui, Rect window_rect, v2 mouse) +function UISignal +ui_build_scale_bar(Axis2 axis, v2 min, v2 max) { - TextSpec text_spec = {.font = &ui->small_font, .colour = FG_COLOUR}; - window_rect = shrink_rect_centered(window_rect, (v2){{UI_REGION_PAD, UI_REGION_PAD}}); - for (Variable *var = ui->floating_widget_sentinal.parent; - var != &ui->floating_widget_sentinal; - var = var->parent) + Font font = ui_font_for_node(ui_top_parent()); + f32 label_size = measure_text(font, str8("-288.88mm")).w; + + UISignal result; + UIAxisSize(axis2_flip(axis), ui_px(RULER_TICK_LENGTH + RULER_TEXT_PAD + label_size, 1.f)) + UIFlags(UINodeFlag_Clickable|UINodeFlag_Scroll|UINodeFlag_DrawHotEffects|UINodeFlag_CustomDraw) { - if (var->type == VT_UI_TEXT_BOX) { - UIView *fw = &var->view; - InputState *is = &ui->text_input_state; + UINode *node = ui_node_from_string(0, str8("###scale_bar")); + result = ui_signal_from_node(node); + + UIDrawScaleBarData *info = push_struct(ui_build_arena(), UIDrawScaleBarData); + node->custom_draw_function = ui_custom_draw_scale_bar; + node->custom_draw_context = info; + + Rect tick_rect = ui_node_rect(node); + if (tick_rect.size.E[axis] > 0) { + info->axis = axis; + info->segments = (u32)(tick_rect.size.E[axis] / (1.5f * font.baseSize)); + info->start_value = min.E[axis] * 1e3; + info->end_value = max.E[axis] * 1e3; + if (axis == Axis2_Y) swap(info->start_value, info->end_value); + } + } + return result; +} - draw_ui_view_container(ui, var, mouse, fw->rect); +function void +ui_build_frame_view_overlay(UINode *frame_view, BeamformerFrameView *view, v2 min_2d, v2 max_2d) +{ + BeamformerUI *ui = ui_context; + UIParent(frame_view) + UIChildLayoutAxis(Axis2_X) + UIPrefHeight(ui_children_sum(1.f)) + UIPrefWidth(ui_pct(1.f, 0.5f)) + UITextOutlineColour((v4){.a = 1.f}) + UITextOutlineThickness(1.f) + UITextColour(RULER_COLOUR) + { + ui_padh(UI_NODE_PAD); - f32 cursor_width = (is->cursor == is->count) ? 0.55f * (f32)is->font->baseSize : 4.0f; - s8 text = {.len = is->count, .data = is->buf}; - v2 text_size = measure_text(*is->font, text); + if (view->kind != BeamformerFrameViewKind_3DXPlane) + UIFontSize(30.f) + UIParent(ui_spacer(0)) + { + ui_spacer(0); - f32 text_pad = 4.0f; - f32 desired_width = text_pad + text_size.w + cursor_width; - fw->rect.size = (v2){{MAX(desired_width, fw->rect.size.w), text_size.h + text_pad}}; + UIPrefHeight(ui_text_dim(1.f, 1.f)) + UIPrefWidth(ui_text_dim(1.f, 1.f)) + ui_label(push_acquisition_kind(ui_build_arena(), view->frame.acquisition_kind, + view->frame.compound_count, view->frame.contrast_mode)); - v2 text_position = {{fw->rect.pos.x + text_pad / 2, fw->rect.pos.y + text_pad / 2}}; - f32 cursor_offset = measure_text(*is->font, (s8){is->cursor, text.data}).w; - cursor_offset += text_position.x; + ui_padw(2.f * UI_NODE_PAD); + } - Rect cursor; - cursor.pos = (v2){{cursor_offset, text_position.y}}; - cursor.size = (v2){{cursor_width, text_size.h}}; + UIPrefHeight(ui_pct(1.f, 0.5f)) ui_spacer(0); - v4 cursor_colour = FOCUSED_COLOUR; - cursor_colour.a = ease_in_out_cubic(is->cursor_blink.t); - v4 text_colour = v4_lerp(FG_COLOUR, HOVERED_COLOUR, fw->child->hover_t); + UIFontSize(24.f) + UIAxisAlign(Axis2_Y, Right) + UIParent(ui_spacer(0)) + { + ui_padw(2.f * UI_NODE_PAD); + + UINode *label_column, *value_column, *unit_column; + UIAxisAlign(Axis2_X, Left) + UIAxisAlign(Axis2_Y, Left) + UIPrefWidth(ui_children_sum(1.f)) + UIParent(ui_spacer(0)) + UIChildLayoutAxis(Axis2_Y) + { + label_column = ui_node_from_string(0, str8("###labels")); + ui_padw(UI_NODE_PAD); + value_column = ui_node_from_string(0, str8("###values")); + ui_padw(UI_NODE_PAD); + unit_column = ui_node_from_string(0, str8("###units")); + } + + UIPrefWidth(ui_text_dim(1.f, 1.f)) + UIPrefHeight(ui_text_dim(1.f, 1.f)) + { + if (view->log_scale) { + UIParent(label_column) ui_label(str8("Dynamic Range:")); + UIParent(unit_column) ui_label(str8("[dB]")); + UIParent(value_column) + UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric) + { + UISignal signal = ui_text_boxf("%0.2f###dynamic_range", view->dynamic_range); + view->dirty |= ui_tweak_f32_compute_variable(signal, &view->dynamic_range, 1.f, 0.5f, V2_INFINITY); + } + } + + // TODO(rnp): ui_em after text height matches correctly + f32 spacer_height; + UIParent(label_column) spacer_height = ui_label(str8("Gamma:")).node->computed_size[Axis2_Y]; + UIParent(unit_column) ui_padh(spacer_height); + UIParent(value_column) + UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric) + { + UISignal signal = ui_text_boxf("%0.2f###gamma", view->gamma); + view->dirty |= ui_tweak_f32_compute_variable(signal, &view->gamma, 1.f, 0.025f, V2_INFINITY); + } + + UIParent(label_column) spacer_height = ui_label(str8("Threshold:")).node->computed_size[Axis2_Y]; + UIParent(unit_column) ui_padh(spacer_height); + UIParent(value_column) + UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric) + { + UISignal signal = ui_text_boxf("%0.2f###threshold", view->threshold); + view->dirty |= ui_tweak_f32_compute_variable(signal, &view->threshold, 1.f, 1.f, V2_INFINITY); + } + } + + UIPrefWidth(ui_pct(1.f, 0.5f)) ui_spacer(0); + + Rect nr = ui_node_rect(frame_view); + if (view->kind != BeamformerFrameViewKind_3DXPlane && point_in_rect(ui->last_mouse, nr) && ui->drag_panel == 0) { + b32 is_1d = iv3_dimension(view->frame.points) == 1; + v2 world = screen_point_to_world_2d(ui->last_mouse, nr.pos, v2_add(nr.pos, nr.size), + min_2d, max_2d); + world = v2_scale(world, 1e3f); + if (is_1d) world.y = ((nr.pos.y + nr.size.y) - ui->last_mouse.y) / nr.size.y; + + UIPrefWidth(ui_text_dim(1.f, 1.f)) + UIPrefHeight(ui_text_dim(1.f, 1.f)) + ui_labelf("{%0.2f%s, %0.2f}", world.x, is_1d ? " mm" : "", world.y); + } + + ui_padw(2.f * UI_NODE_PAD); + } + + ui_padh(UI_NODE_PAD); + } +} + +function void +ui_build_3d_xplane_frame_view(UINode *container, BeamformerFrameView *view) +{ + assert(view->kind == BeamformerFrameViewKind_3DXPlane); + Rect display_rect = ui_node_rect(container); + Rect vr = rect_shrink_centered(display_rect, (v2){{UI_NODE_PAD, UI_NODE_PAD}}); + + f32 aspect = (f32)view->colour_image.width / (f32)view->colour_image.height; + if (aspect > 1.0f) vr.size.w = vr.size.h; + else vr.size.h = vr.size.w; + + if (vr.size.w > display_rect.size.w) { + vr.size.w -= (vr.size.w - display_rect.size.w); + vr.size.h = vr.size.w / aspect; + } else if (vr.size.h > display_rect.size.h) { + vr.size.h -= (vr.size.h - display_rect.size.h); + vr.size.w = vr.size.h * aspect; + } + + // TODO(rnp): probably we don't need frame_top in this path + UINode *frame_top, *frame_view; + UIParent(container) + { + ui_padh(UI_NODE_PAD); + + UIChildLayoutAxis(Axis2_X) + UIPrefHeight(ui_children_sum(1.f)) + UIPrefWidth(ui_children_sum(1.f)) + frame_top = ui_node_from_string(0, str8("###frame_view_top")); + + UIParent(frame_top) + UIPrefHeight(ui_px(vr.size.h, 1.f)) + { + UIChildLayoutAxis(Axis2_Y) + UIPrefWidth(ui_px(vr.size.w, 1.f)) + frame_view = ui_node_from_string(UINodeFlag_Clickable| + UINodeFlag_CustomDraw| + UINodeFlag_Clip| + UINodeFlag_Scroll| + 0, str8("###frame_view")); + frame_view->custom_draw_function = beamformer_custom_draw_frame_view; + frame_view->custom_draw_context = push_struct(ui_build_arena(), BeamformerCustomDrawFrameViewData); + { + BeamformerCustomDrawFrameViewData *data = frame_view->custom_draw_context; + data->uv_start = (v2){0}; + data->uv_end = (v2){{1.f, 1.f}}; + data->view = view; + } + + ui_build_frame_view_overlay(frame_view, view, (v2){0}, (v2){0}); + } + } + + UISignal signal = ui_signal_from_node(frame_view); + if (ui_tweak_f32_compute_variable(signal, &view->threshold, 1.f, 1.f, V2_INFINITY)) + view->dirty = 1; + + f32 test[countof(view->plane_active)] = {0}; + ray mouse_rays[countof(view->plane_active)] = {0}; + v2 mouse_uv = rect_uv_ndc(ui_context->last_mouse, vr); + + i32 hovered_plane = -1; + if ui_node_hot(frame_view) { + for EachElement(test, it) if (view->plane_active[it]) { + BeamformerFrame *frame = ui_context->latest_plane + it; + v2 min_2d, max_2d; + plane_corners_from_transform(frame->voxel_transform, &min_2d, &max_2d); + v3 x_size = v3_scale(x_plane_display_size(frame), 0.5f); + m4 x_rotation = m4_rotation_about_y(x_plane_rotation_for_view_plane(view, it)); + v3 x_position = x_plane_offset_position(view, frame, it); + mouse_rays[it] = x_plane_raycast(view, frame, mouse_uv); + test[it] = obb_raycast(x_rotation, x_size, x_position, mouse_rays[it]); + } + + f32 min_valid_t = inf32(); + for EachElement(test, it) { + if (view->plane_active[it] && Between(test[it], 0, min_valid_t)) { + hovered_plane = (i32)it; + min_valid_t = test[it]; + } + } + } + + if ui_pressed(signal) { + view->plane_drag_index = hovered_plane; + if (hovered_plane != -1) { + v3 origin = mouse_rays[hovered_plane].origin; + v3 p = v3_scale(mouse_rays[hovered_plane].direction, test[hovered_plane]); + view->hit_start_point = view->hit_test_point = v3_add(origin, p); + } + } - TextSpec input_text_spec = {.font = is->font, .colour = text_colour}; - draw_text(text, text_position, &input_text_spec); - DrawRectanglePro(rl_rect(cursor), (Vector2){0}, 0, colour_from_normalized(cursor_colour)); + b32 active = ui_node_key_equal(ui_context->active_node_key[UIMouseButtonKind_Left], frame_view->key); + for EachElement(view->hot_t, it) { + b32 hot = active ? (view->plane_drag_index == (i32)it) : (hovered_plane == (i32)it); + if (hot) view->hot_t[it] += HOVER_SPEED * dt_for_frame; + else view->hot_t[it] -= HOVER_SPEED * dt_for_frame; + view->hot_t[it] = Clamp01(view->hot_t[it]); + } + + if ui_dragging(signal) { + ui_disable_cursor(); + // TODO(rnp): hide mouse + if (view->plane_drag_index != -1) { + /* NOTE(rnp): project start point onto ray */ + BeamformerFrame *frame = ui_context->latest_plane + view->plane_drag_index; + ray mouse_ray = x_plane_raycast(view, frame, rect_uv_ndc(clamp_v2_rect(ui_context->last_mouse, vr), vr)); + v3 s = v3_sub(view->hit_start_point, mouse_ray.origin); + v3 r = v3_sub(mouse_ray.direction, mouse_ray.origin); + f32 scale = v3_dot(s, r) / v3_magnitude_squared(r); + view->hit_test_point = v3_add(mouse_ray.origin, v3_scale(r, scale)); } else { - draw_ui_view(ui, var, window_rect, mouse, text_spec); + f32 dMouseX = ui_context->current_mouse.x - ui_context->last_mouse.x; + view->rotation -= dMouseX / (f32)beamformer_context->window_size.w; + if (view->rotation > 1.0f) view->rotation -= 1.0f; + if (view->rotation < 0.0f) view->rotation += 1.0f; + } + } + + if ui_released(signal) { + ui_enable_cursor(); + + if (view->plane_drag_index != -1) { + m4 x_rotation = m4_rotation_about_y(x_plane_rotation_for_view_plane(view, view->plane_drag_index)); + v3 Z = x_rotation.c[2].xyz; + f32 delta = v3_dot(Z, v3_sub(view->hit_test_point, view->hit_start_point)); + + BeamformerSharedMemory *sm = beamformer_context->shared_memory; + BeamformerLiveImagingParameters *li = &sm->live_imaging_parameters; + li->image_plane_offsets[view->plane_drag_index] += delta; + atomic_or_u32(&sm->live_imaging_dirty_flags, BeamformerLiveImagingDirtyFlags_ImagePlaneOffsets); } + + view->plane_drag_index = -1; + view->hit_start_point = view->hit_test_point = (v3){0}; } } function void -scroll_interaction(Variable *var, f32 delta) +ui_build_frame_view(UINode *container, BeamformerFrameView *view) { - switch (var->type) { - case VT_B32:{ var->bool32 = !var->bool32; }break; - case VT_F32:{ var->real32 += delta; }break; - case VT_I32:{ var->signed32 += (i32)delta; }break; - case VT_SCALED_F32:{ var->scaled_real32.val += delta * var->scaled_real32.scale; }break; - case VT_BEAMFORMER_FRAME_VIEW:{ - BeamformerFrameView *bv = var->generic; - bv->threshold.real32 += delta; - bv->dirty = 1; - } break; - case VT_BEAMFORMER_VARIABLE:{ - BeamformerVariable *bv = &var->beamformer_variable; - f32 value = *bv->store + delta * bv->scroll_scale; - *bv->store = Clamp(value, bv->limits.x, bv->limits.y); - }break; - case VT_CYCLER:{ - u32 new_state = *var->cycler.state += delta; - *var->cycler.state = new_state % var->cycler.cycle_length; - }break; - case VT_UI_VIEW:{ - var->view.rect.pos.h += UI_SCROLL_SPEED * delta; - var->view.rect.pos.h = MAX(0, var->view.rect.pos.h); - }break; - InvalidDefaultCase; + assert(view->kind != BeamformerFrameViewKind_3DXPlane); + + BeamformerUI *ui = ui_context; + BeamformerFrame *frame = &view->frame; + b32 is_1d = iv3_dimension(frame->points) == 1; + f32 txt_w = measure_text(ui->small_font, str8(" -288.8 mm")).w; + f32 scale_bar_size = 1.2f * txt_w + RULER_TICK_LENGTH; + + v3 U = frame->voxel_transform.c[0].xyz; + v3 V = frame->voxel_transform.c[1].xyz; + + v2 output_dim; + output_dim.x = v3_magnitude(U); + output_dim.y = v3_magnitude(V); + + U = v3_scale(U, 1.f / output_dim.x); + V = v3_scale(V, 1.f / output_dim.y); + + v3 min_coordinate = m4_mul_v3(frame->voxel_transform, (v3){{0.f, 0.f, 0.f}}); + v3 max_coordinate = m4_mul_v3(frame->voxel_transform, (v3){{1.f, 1.f, 1.f}}); + + v2 min_2d = {{v3_dot(U, min_coordinate), v3_dot(V, min_coordinate)}}; + v2 max_2d = {{v3_dot(U, max_coordinate), v3_dot(V, max_coordinate)}}; + + f32 aspect = is_1d ? 1.0f : output_dim.w / output_dim.h; + + Rect display_rect = ui_node_rect(container); + Rect vr = rect_shrink_centered(display_rect, (v2){{UI_NODE_PAD, UI_NODE_PAD}}); + + v2 scale_bar_area = {0}; + if (view->scale_bar_active[Axis2_Y]) { + vr.pos.y += 0.5f * (f32)ui->small_font.baseSize; + scale_bar_area.x += scale_bar_size; + scale_bar_area.y += (f32)ui->small_font.baseSize; + } + if (view->scale_bar_active[Axis2_X]) { + vr.pos.x += 0.5f * (f32)ui->small_font.baseSize; + scale_bar_area.x += (f32)ui->small_font.baseSize; + scale_bar_area.y += scale_bar_size; + } + + vr.size = v2_sub(vr.size, scale_bar_area); + if (aspect > 1) vr.size.h = vr.size.w / aspect; + else vr.size.w = vr.size.h * aspect; + + v2 occupied = v2_add(vr.size, scale_bar_area); + if (occupied.w > display_rect.size.w) { + vr.size.w -= (occupied.w - display_rect.size.w); + vr.size.h = vr.size.w / aspect; + } else if (occupied.h > display_rect.size.h) { + vr.size.h -= (occupied.h - display_rect.size.h); + vr.size.w = vr.size.h * aspect; + } + + b32 rebuild_transform = 0; + + UIParent(container) + { + ui_padh(UI_NODE_PAD); + + UINode *frame_top, *frame_view; + UIChildLayoutAxis(Axis2_X) + UIPrefHeight(ui_children_sum(1.f)) + UIPrefWidth(ui_children_sum(1.f)) + frame_top = ui_node_from_string(0, str8("###frame_view_top")); + + UIParent(frame_top) + UIPrefHeight(ui_px(vr.size.h, 1.f)) + { + UIChildLayoutAxis(Axis2_Y) + UIPrefWidth(ui_px(vr.size.w, 1.f)) + frame_view = ui_node_from_string(UINodeFlag_Clickable| + UINodeFlag_CustomDraw| + UINodeFlag_Clip| + UINodeFlag_Scroll| + 0, str8("###frame_view")); + frame_view->custom_draw_function = beamformer_custom_draw_frame_view; + frame_view->custom_draw_context = push_struct(ui_build_arena(), BeamformerCustomDrawFrameViewData); + { + BeamformerCustomDrawFrameViewData *data = frame_view->custom_draw_context; + data->uv_start = (v2){0}; + data->uv_end = (v2){{1.f, 1.f}}; + data->view = view; + } + + ui_build_frame_view_overlay(frame_view, view, min_2d, max_2d); + + UISignal signal = ui_signal_from_node(frame_view); + // TODO(rnp): is this correct for x-plane? + if (ui_tweak_f32_compute_variable(signal, &view->threshold, 1.f, 1.f, V2_INFINITY)) + view->dirty = 1; + + if ui_pressed(signal) { + view->ruler.state = circular_add(view->ruler.state, 1, RulerState_Count); + // TODO(rnp): cleanup: this + v3 p = world_point_from_plane_uv(frame->voxel_transform, rect_uv(ui->last_mouse, ui_node_rect(frame_view))); + switch (view->ruler.state) { + InvalidDefaultCase; + case RulerState_None:{}break; + case RulerState_Start:{view->ruler.start = p;}break; + case RulerState_Hold:{ view->ruler.end = p;}break; + } + } + + if (view->scale_bar_active[Axis2_Y]) { + signal = ui_build_scale_bar(Axis2_Y, min_2d, max_2d); + if ui_scrolled(signal) { + max_2d.y += signal.scroll.y * 1e-3f; + rebuild_transform = 1; + } + } + } + + if (view->scale_bar_active[Axis2_X]) + UIChildLayoutAxis(Axis2_X) + UIPrefHeight(ui_children_sum(1.0f)) + UIPrefWidth(ui_children_sum(1.0f)) + UIParent(ui_node_from_string(0, str8("###frame_view_bot"))) + UIPrefWidth(ui_px(vr.size.w, 1.f)) + { + f32 top_position_offset = frame_view->computed_position[Axis2_X] - display_rect.pos.x; + ui_padw(top_position_offset); + + UISignal signal = ui_build_scale_bar(Axis2_X, min_2d, max_2d); + if ui_scrolled(signal) { + min_2d.x += signal.scroll.y * 0.5e-3f; + max_2d.x -= signal.scroll.y * 0.5e-3f; + rebuild_transform = 1; + } + + ui_padw(display_rect.size.x - top_position_offset); + } + } + + if (rebuild_transform) { + min_coordinate.E[0] = min_2d.E[0]; min_coordinate.E[1] = min_2d.E[1]; + max_coordinate.E[0] = max_2d.E[0]; max_coordinate.E[1] = max_2d.E[1]; + if (ui_rebuild_das_transform(frame->parameter_block, iv3_dimension(frame->points), min_coordinate, max_coordinate)) + ui->flush_parameters = 1; + } +} + +function UI_CUSTOM_DRAW_FUNCTION(beamformer_ui_custom_draw_compute_bar_graph) +{ + ComputeShaderStats *stats = beamformer_context->compute_shader_stats; + + UINode *labels = node->previous_sibling->previous_sibling; + + u32 label_count = labels->child_count; + f32 *total_times = push_array(ui_build_arena(), f32, label_count); + f32 compute_time_sum = 0; + + u32 stages = stats->table.shader_count; + for (u32 index = 0; index < stages; index++) + compute_time_sum += stats->average_times[index]; + for EachIndex(label_count, frame) { + u32 frame_index = (stats->latest_frame_index - frame - 1) % countof(stats->table.times); + for EachIndex(stages, stage) + total_times[frame] += stats->table.times[frame_index][stage]; + } + + f32 remaining_width = node_rect.size.w; + f32 average_width = 0.8f * remaining_width; + + s8 mouse_text = s8(""); + v2 text_pos; + + u32 row_index = 0; + for (UINode *ln = labels->first_child; !ui_node_is_nil(ln); ln = ln->next_sibling, row_index++) { + u32 frame_index = (stats->latest_frame_index - row_index - 1) % countof(stats->table.times); + f32 total_width = average_width * total_times[row_index] / compute_time_sum; + Rect rect; + rect.pos = (v2){{node_rect.pos.x, ln->computed_position[Axis2_Y]}}; + rect.size = (v2){.y = ln->computed_size[Axis2_Y]}; + rect = rect_squish_centered(rect, (v2){.y = 0.4f}); + + for (u32 i = 0; i < stages; i++) { + rect.size.w = total_width * stats->table.times[frame_index][i] / total_times[row_index]; + Color color = colour_from_normalized(g_colour_palette[i % countof(g_colour_palette)]); + DrawRectangleRec(rl_rect(rect), color); + if (point_in_rect(ui_context->last_mouse, rect)) { + // TODO(rnp): tooltips + text_pos = v2_add(rect.pos, (v2){{UI_NODE_PAD, 3.f}}); + Stream sb = arena_stream(*ui_build_arena()); + stream_append_s8s(&sb, beamformer_shader_names[stats->table.shader_ids[i]], s8(": ")); + stream_append_f64_e(&sb, stats->table.times[frame_index][i]); + mouse_text = arena_stream_commit(ui_build_arena(), &sb); + } + rect.pos.x += rect.size.w; + } + } + + v2 start = v2_add(node_rect.pos, (v2){.x = average_width, .y = 0.01f * node_rect.size.y}); + v2 end = v2_add(start, (v2){.y = node_rect.size.y - 0.02f * node_rect.size.y}); + DrawLineEx(rl_v2(start), rl_v2(end), 4, colour_from_normalized(FG_COLOUR)); + + if (mouse_text.len) { + TextSpec ts = {.font = &ui_context->small_font, .flags = TF_OUTLINED, .colour = FG_COLOUR, + .outline_colour = {.a = 1.f}, .outline_thick = 1.f}; + draw_text(str8_from_s8(mouse_text), text_pos, &ts); + } +} + +function void +ui_build_compute_stats(BeamformerComputePlan *cp, f32 broken_shader_t) +{ + ComputeShaderStats *stats = beamformer_context->compute_shader_stats; + f32 compute_time_sum = 0; + u32 stages = stats->table.shader_count; + + for (u32 index = 0; index < stages; index++) + compute_time_sum += stats->average_times[index]; + + UIFontSize(30.f) + UIScroll(Axis2_Count) + { + ui_top_parent()->child_layout_axis = Axis2_X; + + UINode *label_column, *value_column, *unit_column; + UIAxisAlign(Axis2_X, Left) + UIChildLayoutAxis(Axis2_Y) + UIPrefWidth(ui_children_sum(1.0f)) + UIPrefHeight(ui_children_sum(1.0f)) + { + label_column = ui_node_from_string(0, str8("###labels")); + ui_padw(UI_NODE_PAD); + value_column = ui_node_from_string(0, str8("###values")); + ui_padw(UI_NODE_PAD); + unit_column = ui_node_from_string(0, str8("###units")); + } + + UIPrefWidth(ui_text_dim(1.0f, 1.0f)) + UIPrefHeight(ui_text_dim(1.05f, 1.0f)) + { + for EachIndex(stages, it) { + v4 label_colour = FG_COLOUR; + if (vk_pipeline_valid(cp->vulkan_pipelines[it]) == 0 && + stats->table.shader_ids[it] != BeamformerShaderKind_Hilbert) + { + v4_lerp(FG_COLOUR, FOCUSED_COLOUR, ease_in_out_quartic(broken_shader_t)); + } + + str8 shader = str8_from_s8(beamformer_shader_names[stats->table.shader_ids[it]]); + + UITextColour(label_colour) + UIParent(label_column) ui_labelf("%.*s:###csl%u", (i32)shader.length, shader.data, (u32)it); + UIParent(value_column) ui_labelf("%0.2e###csv%u", stats->average_times[it], (u32)it); + UIParent(unit_column) ui_labelf("[s]###csu%u", (u32)it); + } + + UIParent(label_column) ui_label(str8("Compute Total:")); + UIParent(value_column) ui_labelf("%0.2e (%0.2f)###csv_total", compute_time_sum, + compute_time_sum > 0.f ? 1.0f / compute_time_sum : 0.f); + UIParent(unit_column) ui_label(str8("[s] (FPS)###csv_total")); + + UIParent(label_column) ui_label(str8("RF Upload Delta:")); + UIParent(value_column) ui_labelf("%0.2e (%0.2f)###csv_upload", stats->rf_time_delta_average, + stats->rf_time_delta_average > 0.f ? 1.0f / stats->rf_time_delta_average + : 0.f); + UIParent(unit_column) ui_label(str8("[s] (FPS)###csv_upload")); + + u32 rf_size = beamformer_context->compute_context.rf_buffer.active_rf_size; + UIParent(label_column) ui_label(str8("Input RF Size:")); + UIParent(value_column) ui_labelf("%u###csv_rf_size", rf_size); + UIParent(unit_column) ui_label(str8("[B/F]###csv_rf_size")); + + UIParent(label_column) ui_label(str8("DAS RF Size:")); + UIParent(value_column) ui_labelf("%u###csv_das_size", cp->rf_size); + UIParent(unit_column) ui_label(str8("[B/F]###csv_das_size")); + } + } +} + +function void +ui_build_parameters_listing(BeamformerUIPanel *panel) +{ + BeamformerUI *ui = ui_context; + + UIFontSize(30.f) + UIScroll(Axis2_Count) + { + ui_top_parent()->child_layout_axis = Axis2_X; + + UINode *label_column, *value_column, *unit_column; + UIChildLayoutAxis(Axis2_Y) + UIPrefWidth(ui_children_sum(1.0f)) + UIPrefHeight(ui_children_sum(1.0f)) + { + UIAxisAlign(Axis2_X, Left) label_column = ui_node_from_string(0, str8("###labels")); + ui_padw(UI_NODE_PAD); + UIAxisAlign(Axis2_X, Center) value_column = ui_node_from_string(0, str8("###values")); + ui_padw(UI_NODE_PAD); + UIAxisAlign(Axis2_X, Right) unit_column = ui_node_from_string(0, str8("###units")); + } + + f32 line_pad_pct = 1.05f; + UIPrefWidth(ui_text_dim(1.f, 1.f)) + UIPrefHeight(ui_text_dim(line_pad_pct, 1.f)) + { + BeamformerUIParameters *bp = &ui_context->parameters; + UIParent(label_column) ui_label(str8("Sampling Frequency")); + UIParent(value_column) ui_labelf("%0.2f##sampling", bp->sampling_frequency * 1e-6); + UIParent(unit_column) ui_label(str8("[MHz]##sampling")); + + UIParent(label_column) ui_label(str8("Demodulation Frequency")); + UIParent(value_column) ui_labelf("%0.2f###demod", bp->demodulation_frequency * 1e-6); + UIParent(unit_column) ui_label(str8("[MHz]##demod")); + + UIParent(label_column) ui_label(str8("Speed of Sound")); + UIParent(unit_column) ui_label(str8("[m/s]")); + UIParent(value_column) + UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric) + { + UISignal signal = ui_text_boxf("%0.2f###sound", bp->speed_of_sound); + if (ui_tweak_f32_compute_variable(signal, &bp->speed_of_sound, 1.f, 10.f, (v2){{0, inf32()}})) + ui->flush_parameters = 1; + } + + u32 parameter_block = panel->u.parameter_listing.parameter_block; + b32 rebuild_transform = 0; + + BeamformerParameterBlock *pb = beamformer_parameter_block(beamformer_context->shared_memory, parameter_block); + BeamformerComputePlan *cp = beamformer_context->compute_context.compute_plans[parameter_block]; + m4 das_transform = pb->parameters.das_voxel_transform; + if (cp) das_transform = m4_mul(cp->ui_voxel_transform, das_transform); + v3 coordinates[2] = { + m4_mul_v3(das_transform, (v3){{0.0f, 0.0f, 0.0f}}), + m4_mul_v3(das_transform, (v3){{1.0f, 1.0f, 1.0f}}), + }; + + i32 dimension = iv3_dimension(bp->output_points.xyz); + if (dimension > 0) { + read_only local_persist str8 dimension_strings[3][2] = { + {str8_comp("Start Point"), str8_comp("End Point") }, + {str8_comp("Lateral Extent"), str8_comp("Axial Extent")}, + {str8_comp("Min Corner"), str8_comp("Max Corner") }, + }; + + for (u32 index = 0; index < 2; index++) { + UIParent(label_column) + { + UISignal signal = ui_button(dimension_strings[dimension - 1][index]); + signal.node->flags &= ~(UINodeFlag_DrawBackground|UINodeFlag_DrawBorder); + if ui_pressed(signal) + panel->u.parameter_listing.expand_coordinate[index] ^= 1u; + } + + f32 values[3] = {coordinates[index].x, coordinates[index].y, coordinates[index].z}; + u32 value_count = dimension == 2 ? 2 : 3; + v3 normalized_axis = v3_normalize(das_transform.c[index].xyz); + if (dimension == 2) { + values[0] = v3_dot(normalized_axis, coordinates[0]); + values[1] = v3_dot(normalized_axis, coordinates[1]); + } + + if (panel->u.parameter_listing.expand_coordinate[index]) { + UIPrefHeight(ui_px((f32)ui_font_for_node(value_column).baseSize * line_pad_pct, 1.f)) + { + UIParent(value_column) ui_spacer(0); + UIParent(unit_column) ui_spacer(0); + } + + read_only local_persist str8 axis_strings[2][3] = { + {str8_comp(" X:"), str8_comp(" Y:"), str8_comp(" Z:")}, + {str8_comp(" Min:"), str8_comp(" Max:"), }, + }; + str8 *strs = dimension == 2 ? axis_strings[1] : axis_strings[0]; + for EachIndex(value_count, it) { + UIParent(label_column) ui_labelf(" %.*s##label%u_%u", + (i32)strs[it].length, strs[it].data, + index, (u32)it); + UIParent(unit_column) ui_labelf("[mm]##%u_%u", index, (u32)it); + UIParent(value_column) + UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric) + { + UISignal signal = ui_text_boxf("%0.2f###%u_%u", values[it] * 1e3f, index, (u32)it); + rebuild_transform |= ui_tweak_f32_compute_variable(signal, values + it, + 1e-3f, 0.5e-3f, V2_INFINITY); + } + } + } else { + UIParent(unit_column) ui_labelf("[mm]##dim%u", index); + + UINode *group; + UIParent(value_column) + UIChildLayoutAxis(Axis2_X) + UIPrefWidth(ui_children_sum(1.f)) + UIPrefHeight(ui_children_sum(1.f)) + group = ui_spacer(0); + + UIParent(group) + { + ui_labelf("{##%u", index); + for EachIndex(value_count, it) { + if (it != 0) ui_labelf(", ##%u_%u", index, (u32)it); + UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric) + { + UISignal signal = ui_text_boxf("%0.2f###%u_%u", values[it] * 1e3f, index, (u32)it); + rebuild_transform |= ui_tweak_f32_compute_variable(signal, values + it, + 1e-3f, 0.5e-3f, V2_INFINITY); + } + } + ui_labelf("}##%u", index); + } + } + + if (dimension == 2) { + coordinates[0].E[index] = values[0]; + coordinates[1].E[index] = values[1]; + } + } + } + + if (dimension == 2) { + UIParent(label_column) ui_label(str8("Off Axis Position")); + UIParent(unit_column) ui_label(str8("[mm]##off_axis")); + UIParent(value_column) + UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric) + { + UISignal signal = ui_text_boxf("%0.2f###off_axis", plane_offset_from_transform(das_transform) * 1e3f); + rebuild_transform |= ui_tweak_f32_compute_variable(signal, &ui->off_axis_position, + 1e-3f, 0.1e-3f, V2_INFINITY); + } + + UIParent(label_column) ui_label(str8("Beamform Plane")); + UIParent(unit_column) UIPrefHeight(ui_em(1.f, 1.f)) ui_spacer(0); + UIParent(value_column) + UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric) + { + UISignal signal = ui_text_boxf("%0.2f###beamform_plane", ui->beamform_plane); + rebuild_transform |= ui_tweak_f32_compute_variable(signal, &ui->beamform_plane, + 1.f, 0.025f, (v2){{-1.f, 1.f}}); + } + } + + UIParent(label_column) ui_label(str8("F#")); + UIParent(unit_column) UIPrefHeight(ui_em(1.f, 1.f)) ui_spacer(0); + UIParent(value_column) + UIFlags(UINodeFlag_Scroll|UINodeFlag_TextInputNumeric) + { + UISignal signal = ui_text_boxf("%0.2f###f_number", bp->f_number); + if (ui_tweak_f32_compute_variable(signal, &bp->f_number, 1.f, 0.05f, (v2){{0, inf32()}})) + ui->flush_parameters = 1; + } + + UIParent(label_column) ui_label(str8("Interpolation")); + UIParent(unit_column) ui_build_node_from_key(0, ui_node_key_zero()); + UIParent(value_column) + UIFlags(UINodeFlag_Scroll) + { + str8 label = str8_from_s8(beamformer_interpolation_mode_strings[bp->interpolation_mode]); + UISignal signal = ui_label_button(label); + if (ui_pressed(signal) || ui_scrolled(signal)) { + i32 delta = signal.scroll.y + ui_pressed(signal); + bp->interpolation_mode = circular_add(bp->interpolation_mode, delta, + BeamformerInterpolationMode_Count); + ui->flush_parameters = 1; + } + } + + UIParent(label_column) ui_label(str8("Coherency Weighting")); + UIParent(unit_column) UIPrefHeight(ui_em(1.0f, 1.0f)) ui_spacer(0); + UIParent(value_column) + UIFlags(UINodeFlag_Scroll) + { + UISignal signal = ui_label_button(bp->coherency_weighting ? + str8("True###coherency_weighting") : + str8("False###coherency_weighting")); + if (signal.flags & (UISignalFlag_Pressed|UISignalFlag_Scrolled)) { + bp->coherency_weighting = !bp->coherency_weighting; + ui->flush_parameters = 1; + } + } + + if (rebuild_transform) { + if (ui_rebuild_das_transform(parameter_block, dimension, coordinates[0], coordinates[1])) + ui->flush_parameters = 1; + } + } + } +} + +function f32 +ui_slider_update_from_signal(f32 percent, UISignal signal) +{ + f32 result = percent; + result += 0.05f * signal.scroll.y; + if ui_dragging(signal) + result = rect_uv(ui_context->last_mouse, ui_node_rect(signal.node)).E[signal.node->parent->child_layout_axis]; + result = Clamp01(result); + return result; +} + +function void +ui_build_live_imaging_controls(BeamformerUIPanel *panel) +{ + BeamformerLiveImagingParameters *lip = &beamformer_context->shared_memory->live_imaging_parameters; + + UIFontSize(30.f) + UIScroll(Axis2_Count) + { + ui_top_parent()->child_layout_axis = Axis2_Y; + + if (popcount_u64(lip->acquisition_kind_enabled_flags) > 1) + UIPrefWidth(ui_children_sum(1.f)) + UIPrefHeight(ui_children_sum(1.f)) + UIChildLayoutAxis(Axis2_X) + UIParent(ui_spacer(0)) + UIPrefHeight(ui_text_dim(1.1f, 1.f)) + UIPrefWidth(ui_text_dim(1.f, 1.f)) + { + u32 kind = lip->acquisition_kind; + ui_label(str8("Acquisition: ")); + str8 kind_string = kind < BeamformerAcquisitionKind_Count ? beamformer_acquisition_kind_strings[kind] + : str8("Invalid"); + + UISignal signal = ui_label_button(kind_string); + if ui_pressed(signal) + ui_context_menu_open(signal.node->key, panel); + } + + UIPrefHeight(ui_text_dim(1.1f, 1.f)) + UIPrefWidth(ui_text_dim(1.f, 1.f)) + { + UINode *spacer; + UISignal signal; + + f32 row_height = ui_label(str8("Power:")).node->computed_size[Axis2_Y]; + + UIPrefWidth(ui_pct(1.f, 1.f)) + UIPrefHeight(ui_px(row_height, 1.f)) + UIChildLayoutAxis(Axis2_X) + spacer = ui_spacer(0); + UIParent(spacer) + { + ui_padw(2 * UI_NODE_PAD); + v4 hsv_power_slider = {{0.35f * ease_in_out_cubic(1.0f - lip->transmit_power), 0.65f, 0.65f, 1}}; + UIBGColour(hsv_to_rgb(hsv_power_slider)) + UIPrefHeight(ui_px(row_height, 1.f)) + UIPrefWidth(ui_pct(1.f, 0.5f)) + signal = ui_slider(lip->transmit_power, str8("###transmit_power")); + if (signal.flags) { + lip->transmit_power = ui_slider_update_from_signal(lip->transmit_power, signal); + atomic_or_u32(&beamformer_context->shared_memory->live_imaging_dirty_flags, + BeamformerLiveImagingDirtyFlags_TransmitPower); + } + ui_padw(2 * UI_NODE_PAD); + } + + row_height = ui_label(str8("TGC:")).node->computed_size[Axis2_Y]; + for EachElement(lip->tgc_control_points, it) { + UIPrefWidth(ui_pct(1.f, 1.f)) + UIPrefHeight(ui_px(row_height, 1.f)) + UIChildLayoutAxis(Axis2_X) + spacer = ui_spacer(0); + UIParent(spacer) + { + ui_padw(2 * UI_NODE_PAD); + UIBGColour(g_colour_palette[1]) + UIPrefHeight(ui_px(row_height, 1.f)) + UIPrefWidth(ui_pct(1.f, 0.5f)) + signal = ui_sliderf(lip->tgc_control_points[it], "###tgc_%u", (u32)it); + if (signal.flags) { + lip->tgc_control_points[it] = ui_slider_update_from_signal(lip->tgc_control_points[it], signal); + atomic_or_u32(&beamformer_context->shared_memory->live_imaging_dirty_flags, + BeamformerLiveImagingDirtyFlags_TGCControlPoints); + } + ui_padw(2 * UI_NODE_PAD); + } + } + + if (lip->save_enabled) { + ui_label(str8("File Name Tag:")); + str8 save_name = (str8){.data = (u8 *)lip->save_name_tag, + .length = Clamp(lip->save_name_tag_length, 0, countof(lip->save_name_tag))}; + + + v4 save_text_colour = FG_COLOUR; + u64 text_input_flags = 0; + if (lip->save_name_tag_length <= 0) { + save_text_colour.a = 0.6f; + save_name = str8("Insert Text..."); + text_input_flags = UINodeFlag_TextInputClearOnStart; + } + + UIPrefWidth(ui_children_sum(1.f)) + UIPrefHeight(ui_children_sum(1.f)) + UIChildLayoutAxis(Axis2_X) + spacer = ui_spacer(0); + UIParent(spacer) + { + ui_padw(2 * UI_NODE_PAD); + UITextColour(save_text_colour) + UIFlags(text_input_flags) + signal = ui_text_box(push_str8_from_parts(ui_build_arena(), str8(""), save_name, + str8("###save_name_field"))); + if (ui_node_key_equal(signal.node->key, ui_context->text_input_state.node_key)) + signal.node->text_colour = FG_COLOUR; + + if (signal.flags & UISignalFlag_TextCommit) { + str8 string = signal.string; + lip->save_name_tag_length = Min(string.length, countof(lip->save_name_tag)); + memory_copy(lip->save_name_tag, string.data, lip->save_name_tag_length); + atomic_or_u32(&beamformer_context->shared_memory->live_imaging_dirty_flags, + BeamformerLiveImagingDirtyFlags_SaveNameTag); + } + } + + ui_padh(UI_NODE_PAD); + + UIAxisAlign(Axis2_X, Center) + UITextAlign(Center) + UIBGColour((v4){0}) + UIPrefWidth(ui_text_dim(1.3f, 1.f)) + UIPrefHeight(ui_text_dim(1.3f, 1.f)) + { + UIPrefWidth(ui_pct(1.f, 1.f)) UIPrefHeight(ui_children_sum(1.f)) + spacer = ui_spacer(0); + + UIParent(spacer) + { + b32 active = lip->save_active; + str8 label = active ? str8("Saving...###save_button") : str8("Save Data###save_button"); + f32 save_t = beamformer_ui_blinker_update(&panel->u.live_imaging_save_button_blinker, BLINK_SPEED); + v4 border_colour = (v4){.a = 0.6f}; + if (active) border_colour = v4_lerp(BORDER_COLOUR, FOCUSED_COLOUR, ease_in_out_cubic(save_t)); + UIBorderColour(border_colour) + signal = ui_button(label); + if ui_pressed(signal) { + lip->save_active = !active; + atomic_or_u32(&beamformer_context->shared_memory->live_imaging_dirty_flags, + BeamformerLiveImagingDirtyFlags_SaveData); + } + } + + ui_padh(UI_NODE_PAD); + + UIPrefWidth(ui_pct(1.f, 1.f)) UIPrefHeight(ui_children_sum(1.f)) + spacer = ui_spacer(0); + + UIParent(spacer) + { + UIBorderColour((v4){.a = 0.6f}) + signal = ui_button(str8("Stop Imaging")); + if ui_pressed(signal) + atomic_or_u32(&beamformer_context->shared_memory->live_imaging_dirty_flags, + BeamformerLiveImagingDirtyFlags_StopImaging); + } + } + } + } + } +} + +function UISignal +ui_panel_label(BeamformerUIPanel *panel) +{ + Stream sb = arena_stream(*ui_build_arena()); + switch (panel->kind) { + InvalidDefaultCase; + case BeamformerPanelKind_ComputeBarGraph:{stream_append_s8(&sb, s8("Compute Bar Graph"));}break; + case BeamformerPanelKind_ComputeStats:{stream_append_s8(&sb, s8("Compute Stats"));}break; + case BeamformerPanelKind_FrameViewLive:{stream_append_s8(&sb, s8("Frame View"));}break; + case BeamformerPanelKind_FrameViewXPlane:{stream_append_s8(&sb, s8("X-Plane View"));}break; + case BeamformerPanelKind_LiveImagingControls:{stream_append_s8(&sb, s8("Live Controls"));}break; + case BeamformerPanelKind_FrameViewCopy:{ + stream_append_s8(&sb, s8("Frame Copy [")); + stream_append_hex_u64(&sb, panel->u.frame_view->frame.id); + stream_append_s8(&sb, s8("]#")); + }break; + case BeamformerPanelKind_ParameterListing:{ + stream_append_s8(&sb, s8("Parameter Listing [")); + stream_append_u64(&sb, panel->u.parameter_listing.parameter_block); + stream_append_s8(&sb, s8("]#")); + }break; + } + stream_append_s8(&sb, s8("##")); + stream_append_hex_u64(&sb, (u64)panel); + s8 title_s8 = arena_stream_commit(ui_build_arena(), &sb); + + UISignal result; + UIPrefWidth(ui_text_dim(1.f, 1.f)) + UIPrefHeight(ui_text_dim(1.4f, 1.f)) + result = ui_label(str8_from_s8(title_s8)); + + return result; +} + +function void +ui_insert_drop_site_spacer_before(UINode *before, f32 pad_node_width) +{ + UIParent(0) + { + UINode *spacer, *spacer_gap; + UIPrefHeight(ui_pct(1.f, 0.5f)) + UIPrefWidth(ui_px(24.f, 1.f)) + UIBorderColour((v4){.a = 0.9f}) + UIBGColour((v4){.a = 0.6f}) + spacer = ui_spacer(UINodeFlag_DrawBackground|UINodeFlag_DrawBorder); + + UIPrefWidth(ui_px(pad_node_width, 1.f)) + spacer_gap = ui_spacer(0); + + spacer->parent = before->parent; + spacer_gap->parent = before->parent; + spacer->parent->child_count += 2; + + spacer->previous_sibling = before->previous_sibling; + spacer->next_sibling = spacer_gap; + before->previous_sibling->next_sibling = spacer; + + spacer_gap->previous_sibling = spacer; + spacer_gap->next_sibling = before; + + before->previous_sibling = spacer_gap; + } +} + +function UINode * +ui_box_pad(UINode *container, UISize pad, str8 tag) +{ + UINode *result; + UIParent(container) + UIAxisSize(Axis2_X, ui_pct(1.f, 0.5f)) + { + UIAxisSize(Axis2_Y, pad) ui_spacer(0); + + UIAxisSize(Axis2_Y, ui_pct(1.f, 0.5f)) + UIChildLayoutAxis(Axis2_X) + UIParent(ui_spacer(0)) + { + UIAxisSize(Axis2_X, pad) ui_spacer(0); + UIChildLayoutAxis(container->child_layout_axis) + result = ui_node_from_string(0, tag); + UIAxisSize(Axis2_X, pad) ui_spacer(0); + } + + UIAxisSize(Axis2_Y, pad) ui_spacer(0); + } + return result; +} + +function print_format(3, 4) UINode * +ui_box_padf(UINode *container, UISize pad, const char *format, ...) +{ + va_list args; + va_start(args, format); + UINode *result = ui_box_pad(container, pad, push_str8_fv(ui_build_arena(), format, args)); + va_end(args); + return result; +} + +function BeamformerUIPanel * +ui_panel_group_equip(UINode *node, BeamformerUIPanel *group) +{ + BeamformerUIPanel *result = group; + + if (group->kind != BeamformerPanelKind_Split) + UIPrefWidth(ui_children_sum(1.f)) + UIPrefHeight(ui_children_sum(1.f)) + { + assert(group->kind == BeamformerPanelKind_TabGroup); + BeamformerUIPanel *focus = result = group->u.tab_focus; + + node->flags |= UINodeFlag_DropSite; + + UINode *tab_bar_node, *tab_clip_node; + UIParent(node) + UIChildLayoutAxis(Axis2_X) + UIPrefWidth(ui_pct(1.f, 1.f)) + tab_bar_node = ui_node_from_string(UINodeFlag_Clip, str8("###tab_scroll")); + + UIParent(tab_bar_node) + UIAxisAlign(Axis2_Y, Center) + UIChildLayoutAxis(Axis2_X) + tab_clip_node = ui_node_from_string(UINodeFlag_ViewScrollX, str8("###tab_clip")); + + b32 drop_site = ui_node_key_equal(node->key, ui_context->drop_target_key) && + ui_context->drag_panel && + !beamformer_registers()->split_left_tree && + !beamformer_registers()->split_right_tree; + b32 drop_site_handled = 0; + u32 drop_site_index = 0; + f32 tab_pad = 6.f; + UIParent(tab_clip_node) + UIFontSize(24.f) + { + for (BeamformerUIPanel *tab = group->first_child; tab; tab = tab->next_sibling) { + ui_padw(tab_pad); + + // NOTE(rnp): push tab + UINode *tab_node; + UIAxisAlign(Axis2_Y, Center) + UIChildLayoutAxis(Axis2_X) + // TODO(rnp): per edge border colour + UIBorderColour((v4){.a = 0.9f}) + UIBGColour(tab == focus ? BG_COLOUR : (v4){.a = 0.6f}) + UIFlags(UINodeFlag_Clickable| + UINodeFlag_DrawBackground| + UINodeFlag_DrawBorder| + UINodeFlag_DrawHotEffects| + UINodeFlag_DrawActiveEffects) + tab_node = ui_node_from_stringf(tab == focus ? UINodeFlag_FocusActive : 0, "###tab%p", tab); + + if (drop_site && !drop_site_handled && + ui_context->last_mouse.x < (tab_node->computed_position[Axis2_X] + 0.5f * tab_node->computed_size[Axis2_X])) + { + drop_site_handled = 1; + if (ui_context->drag_panel != tab && ui_context->drag_panel != tab->previous_sibling) + ui_insert_drop_site_spacer_before(tab_node, tab_pad); + } + + UISignal signal = {0}; + UIParent(tab_node) + { + ui_padw(UI_BORDER_THICK + tab_pad); + + v4 fg_colour = FG_COLOUR; + if (tab != focus) fg_colour.a = 0.8f; + UITextColour(fg_colour) + ui_panel_label(tab); + + b32 has_settings = (beamformer_panel_infos[tab->kind].flags & BeamformerPanelFlags_HasSettings) != 0; + if (tab == focus && has_settings) + UIPrefWidth(ui_text_dim(2.f, 1.f)) + UIPrefHeight(ui_pct(1.f, 1.f)) + UITextAlign(Right) + UIFlags(UINodeFlag_IconText) + { + ui_padw(0.5f * UI_NODE_PAD); + + signal = ui_label_button(str8("+")); + if ui_pressed(signal) + ui_context_menu_open(signal.node->key, tab); + } + + ui_padw(0.5f * UI_NODE_PAD); + + UIPrefWidth(ui_text_dim(2.f, 1.f)) + UIPrefHeight(ui_pct(1.f, 1.f)) + UITextAlign(Center) + UIFlags(UINodeFlag_IconText) + signal = ui_label_button(str8("x")); + if (ui_pressed(signal) || signal.flags & UISignalFlag_MiddlePressed) + beamformer_command(beamformer_command_infos[BeamformerCommandKind_CloseTab].string, .tree_node = (u64)tab); + + ui_padw(0.5f * UI_NODE_PAD); + } + + if (!drop_site_handled) drop_site_index++; + + signal = ui_signal_from_node(tab_node); + if ui_pressed(signal) + beamformer_command(beamformer_command_infos[BeamformerCommandKind_FocusTab].string, .tree_node = (u64)tab); + if (signal.flags & UISignalFlag_MiddlePressed) + beamformer_command(beamformer_command_infos[BeamformerCommandKind_CloseTab].string, .tree_node = (u64)tab); + if (ui_dragging(signal) && !point_in_rect(ui_context->last_mouse, ui_node_rect(signal.node))) + ui_drag_begin(tab); + if ui_released(signal) + ui_context->drag_end = 1; + } + + ui_padw(tab_pad); + + // NOTE(rnp): context menu opener + UISignal signal; + UIPrefWidth(ui_text_dim(3.f, 1.f)) + UIPrefHeight(ui_text_dim(3.f, 1.f)) + UITextAlign(Center) + UIFlags(UINodeFlag_IconText) + signal = ui_label_button(str8("+")); + if ui_pressed(signal) + ui_context_menu_open(signal.node->key, group); + + if (drop_site && !drop_site_handled && ui_context->drag_panel != group->last_child) { + drop_site_handled = 1; + ui_insert_drop_site_spacer_before(signal.node, tab_pad); + } + + ui_padw(tab_pad); + } + + ui_signal_from_node(tab_clip_node); + + if (drop_site || drop_site_handled) { + beamformer_registers()->drop_target_tree = (u64)group; + beamformer_registers()->drop_child_index = drop_site_handled ? drop_site_index : group->child_count; + } + } + + // NOTE(rnp): close tabgroup button + if (!result && group != ui_context->tree) + UIParent(ui_box_pad(node, ui_pct(0.5f, 0.5f), str8(""))) + { + ui_top_parent()->semantic_size[Axis2_X] = ui_children_sum(1.f); + + UISignal signal; + UIPrefWidth(ui_text_dim(1.5f, 1.f)) + UIPrefHeight(ui_text_dim(2.f, 1.f)) + UIBGColour((v4){.a = 0.3f}) + UIBorderColour((v4){.a = 0.6f}) + UITextAlign(Center) + signal = ui_button(str8("Close Panel")); + if ui_pressed(signal) + beamformer_command(beamformer_command_infos[BeamformerCommandKind_CloseTab].string, .tree_node = (u64)group); + } + + ui_signal_from_node(node); + + return result; +} + +function void +ui_build_regions(UINode *root_node, BeamformerUIPanel *tree_root) +{ + BeamformerUI *ui = ui_context; + + struct tree_frame { + BeamformerUIPanel *tree; + UINode *node; + } init[64]; + + struct { + struct tree_frame *data; + da_count count; + da_count capacity; + } stack = {init, 0, countof(init)}; + + *da_push(ui_build_arena(), &stack) = (struct tree_frame){ + .node = ui_box_padf(root_node, ui_px(UI_NODE_PAD, 1.f), "%p_padded", root_node), + .tree = tree_root, + }; + while (stack.count) { + struct tree_frame *top = stack.data + --stack.count; + + BeamformerUIPanel *panel = top->tree; + UINode *top_node = top->node; + + UIParent(top_node) + switch (panel->kind) { + + case BeamformerPanelKind_TabGroup:{ + UINode *node; + UIChildLayoutAxis(Axis2_Y) + node = ui_node_from_stringf(UINodeFlag_Clip, "###%p_group", panel); + BeamformerUIPanel *next = ui_panel_group_equip(node, panel); + if (next) *da_push(ui_build_arena(), &stack) = (struct tree_frame){ + .tree = next, + .node = node, + }; + }break; + + case BeamformerPanelKind_Split:{ + assert(panel->child_count == 2); + + Axis2 axis = panel->u.split.axis; + top_node->child_layout_axis = axis; + + UIAxisSize(axis2_flip(axis), ui_pct(1.f, 0.5f)) + { + f32 split_pct = panel->u.split.fraction; + + UINode *left; + UIAxisSize(axis, ui_pct(split_pct, 0.5f)) + UIChildLayoutAxis(Axis2_Y) + left = ui_node_from_stringf(UINodeFlag_Clip, "###%p_left", panel); + + BeamformerUIPanel *next = ui_panel_group_equip(left, panel->first_child); + if (next) *da_push(ui_build_arena(), &stack) = (struct tree_frame){ + .tree = next, + .node = left, + }; + + UIAxisSize(axis, ui_children_sum(1.f)) + UIChildLayoutAxis(axis) + UIParent(ui_node_from_stringf(UINodeFlag_Clickable, "###%p_split", panel)) + { + UIAxisSize(axis, ui_px(UI_NODE_PAD, 1.f)) ui_spacer(0); + UIAxisSize(axis, ui_px(UI_SPLIT_HANDLE_THICK, 1.f)) + UIBGColour((v4){.a = 0.6f}) + { + UINode *rn = ui_spacer(UINodeFlag_DrawBackground|UINodeFlag_DrawHotEffects|UINodeFlag_DrawActiveEffects); + rn->hot_t = ui_top_parent()->hot_t; + } + UIAxisSize(axis, ui_px(UI_NODE_PAD, 1.f)) ui_spacer(0); + + UISignal signal = ui_signal_from_node(ui_top_parent()); + if ui_dragging(signal) { + Rect nr = ui_node_rect(top_node); + v2 uv = rect_uv(clamp_v2_rect(ui->last_mouse, nr), nr); + panel->u.split.fraction = Clamp(uv.E[panel->u.split.axis], 0.03f, 0.97f); + } + } + + UINode *right; + UIAxisSize(axis, ui_pct(1.f - split_pct, 0.5f)) + UIChildLayoutAxis(Axis2_Y) + right = ui_node_from_stringf(UINodeFlag_Clip, "###%p_right", panel); + + next = ui_panel_group_equip(right, panel->last_child); + if (next) *da_push(ui_build_arena(), &stack) = (struct tree_frame){ + .tree = next, + .node = right, + }; + } + }break; + + case BeamformerPanelKind_ComputeBarGraph:{ + UIFontSize(30.f) + UIScroll(Axis2_Y) + { + ui_top_parent()->child_layout_axis = Axis2_X; + + UINode *label_column, *bar_column; + UIAxisAlign(Axis2_X, Left) + UIChildLayoutAxis(Axis2_Y) + UIPrefWidth(ui_children_sum(1.f)) + UIPrefHeight(ui_children_sum(1.f)) + { + UIAxisAlign(Axis2_X, Right) + label_column = ui_node_from_string(0, str8("###labels")); + ui_padw(UI_NODE_PAD); + + f32 bar_width = ui_top_parent()->parent->parent->parent->computed_size[Axis2_X] + - label_column->computed_size[Axis2_X] - UI_NODE_PAD; + bar_column = ui_node_from_string(UINodeFlag_CustomDraw, str8("###bars")); + bar_column->semantic_size[Axis2_X] = ui_px(bar_width, 0.1f); + bar_column->semantic_size[Axis2_Y] = ui_px(label_column->computed_size[Axis2_Y], 1.f); + bar_column->custom_draw_function = beamformer_ui_custom_draw_compute_bar_graph; + } + UIParent(label_column) + UIPrefHeight(ui_text_dim(1.3f, 1.f)) + UIPrefWidth(ui_text_dim(1.f, 1.f)) + for (i32 i = 0; i < 4; i++) + ui_labelf("%d:", -i); + } + + }break; + + case BeamformerPanelKind_ComputeStats:{ + u32 selected_plan = ui->selected_parameter_block % BeamformerMaxParameterBlocks; + BeamformerComputePlan *cp = beamformer_context->compute_context.compute_plans[selected_plan]; + if (!cp) cp = &beamformer_nil_compute_plan; + f32 t = beamformer_ui_blinker_update(&panel->u.compute_stats_broken_shader_blinker, BLINK_SPEED); + ui_build_compute_stats(cp, t); + }break; + + case BeamformerPanelKind_FrameViewXPlane: + { + BeamformerFrameView *view = panel->u.frame_view; + b32 any_valid = 0; + for EachElement(view->plane_active, plane) + any_valid |= (view->plane_active[plane] && ui_context->latest_plane[plane].timeline_valid_value); + if (any_valid) { + UINode *container; + UIChildLayoutAxis(Axis2_Y) + UIPrefWidth(ui_pct(1.f, 0.5f)) + UIPrefHeight(ui_pct(1.f, 0.5f)) + UIAxisAlign(Axis2_X, Center) + container = ui_node_from_string(0, str8("###frame_view_container")); + ui_build_3d_xplane_frame_view(container, view); + } + }break; + + case BeamformerPanelKind_FrameViewCopy: + case BeamformerPanelKind_FrameViewLive: + { + BeamformerFrameView *view = panel->u.frame_view; + if (iv3_dimension(view->frame.points) != 0) { + // TODO(rnp): cleanup, why do we need this extra container + UINode *container; + UIChildLayoutAxis(Axis2_Y) + UIPrefWidth(ui_pct(1.f, 0.5f)) + UIPrefHeight(ui_pct(1.f, 0.5f)) + UIAxisAlign(Axis2_X, Center) + container = ui_node_from_string(0, str8("###frame_view_container")); + ui_build_frame_view(container, view); + } + }break; + + case BeamformerPanelKind_ParameterListing:{ ui_build_parameters_listing(panel); }break; + + case BeamformerPanelKind_LiveImagingControls:{ ui_build_live_imaging_controls(panel); }break; + + InvalidDefaultCase; + } + + ui_signal_from_node(top_node); + } +} + +function UINode * +ui_build_drag_hover_node(void) +{ + BeamformerUI *ui = ui_context; + + BeamformerUIPanel *tree = (BeamformerUIPanel *)beamformer_registers()->drop_target_tree; + UINode *target = (UINode *)ui->drop_target_node; + Axis2 axis = beamformer_registers()->split_axis; + Axis2 flip = axis2_flip(axis); + Rect nr = ui_node_rect(target); + f32 pct = 4.0f; + f32 off_pct = 0.95f; + if (target == ui->root_node) + pct = 0.1f; + if (tree->kind == BeamformerPanelKind_TabGroup) { + off_pct = 0.8f; + pct = 0.3f; + } + if (beamformer_registers()->split_left_tree == beamformer_registers()->split_right_tree) + off_pct = pct = 0.8f; + + UINode *result = push_struct(ui_build_arena(), UINode); + result->flags = UINodeFlag_DrawBackground; + result->bg_colour = NODE_SPLIT_COLOUR; + result->computed_size[flip] = off_pct * nr.size.E[flip]; + result->computed_size[axis] = pct * nr.size.E[axis]; + result->computed_position[axis] = nr.pos.E[axis]; + result->computed_position[flip] = nr.pos.E[flip]; + + if (target == ui->root_node) { + result->computed_position[flip] += 0.5f * (nr.size.E[flip] - result->computed_size[flip]); + if (beamformer_registers()->split_left_tree == (u64)ui->tree) + result->computed_position[axis] = nr.pos.E[axis] + nr.size.E[axis] - result->computed_size[axis]; + } else { + result->computed_position[axis] += 0.5f * (nr.size.E[axis] - result->computed_size[axis]); + result->computed_position[flip] += 0.5f * (nr.size.E[flip] - result->computed_size[flip]); + + if (tree->kind == BeamformerPanelKind_TabGroup) { + if (beamformer_registers()->split_left_tree == (u64)tree) + result->computed_position[axis] += 0.45f * (nr.size.E[axis] - result->computed_size[axis]); + if (beamformer_registers()->split_right_tree == (u64)tree) + result->computed_position[axis] -= 0.45f * (nr.size.E[axis] - result->computed_size[axis]); + } + } + + return result; +} + +function b32 +ui_build_drag_split_box(Axis2 axis, b32 two_way, i32 highlight_index, str8 tag) +{ + UINode *split_box; + UIAxisAlign(axis2_flip(axis), Center) + UIAxisSize(axis2_flip(axis), ui_px(60.f, 1.f)) + UIAxisSize(axis, ui_children_sum(1.f)) + UIChildLayoutAxis(axis) + UIBorderColour((v4){.a = 0.8f}) + UIBGColour(BG_COLOUR) + split_box = ui_node_from_string(UINodeFlag_DrawBorder|UINodeFlag_DrawBackground, + push_str8_from_parts(ui_build_arena(), str8(""), + str8("drag_split_box_"), tag)); + b32 result = point_in_rect(ui_context->last_mouse, ui_node_rect(split_box)); + + UIParent(split_box) + UIAxisSize(axis2_flip(axis), ui_pct(0.7f, 0.5f)) + UIAxisSize(axis, ui_px(1.5f * UI_NODE_PAD, 1.f)) + { + UIAxisSize(axis, ui_px(UI_NODE_PAD, 1.f)) ui_spacer(0); + + UIBorderColour((v4){.a = highlight_index <= 0 ? 0.0f : 0.4f}) + UIBGColour(highlight_index <= 0 ? NODE_SPLIT_COLOUR : (v4){0}) + ui_spacer(UINodeFlag_DrawBorder|UINodeFlag_DrawBackground); + + UIAxisSize(axis, ui_px(UI_NODE_PAD, 1.f)) ui_spacer(0); + + if (two_way) { + UIBorderColour((v4){.a = highlight_index != 0 ? 0.0f : 0.4f}) + UIBGColour(highlight_index != 0 ? NODE_SPLIT_COLOUR : (v4){0}) + ui_spacer(UINodeFlag_DrawBorder|UINodeFlag_DrawBackground); + + UIAxisSize(axis, ui_px(UI_NODE_PAD, 1.f)) ui_spacer(0); + } + } + return result; +} + +function b32 +ui_build_drag_overlay_splitter(Axis2 axis, b32 two_way, i32 highlight_index, str8 tag) +{ + b32 result = 0; + + UINode *container; + UIChildLayoutAxis(axis2_flip(axis)) + UIAxisAlign(axis2_flip(axis), Center) + UIAxisSize(axis, ui_children_sum(1.f)) + UIAxisSize(axis2_flip(axis), ui_pct(1.f, 0.5f)) + { + container = ui_spacer(0); + } + + UIParent(container) + result = ui_build_drag_split_box(axis, two_way, highlight_index, tag); + return result; +} + +function void +ui_build_drag_overlay(Rect window_rect) +{ + BeamformerUI *ui = ui_context; + + UIChildLayoutAxis(Axis2_Y) + UIPrefWidth(ui_px(window_rect.size.x, 1.f)) + UIPrefHeight(ui_px(window_rect.size.y, 1.f)) + ui->drag_overlay_edges_root = ui_node_from_string(0, str8("drag_overlay_edges_root")); + + UIParent(ui->drag_overlay_edges_root) + { + if (ui_build_drag_overlay_splitter(Axis2_Y, 0, 0, str8("top"))) + { + beamformer_registers()->split_axis = Axis2_Y; + beamformer_registers()->split_left_tree = 0; + beamformer_registers()->split_right_tree = (u64)ui->tree; + beamformer_registers()->drop_target_tree = (u64)ui->tree; + ui->drop_target_node = ui->root_node; + } + + UIPrefHeight(ui_pct(1.f, 0.5f)) + UIParent(ui_spacer(0)) + { + if (ui_build_drag_overlay_splitter(Axis2_X, 0, 0, str8("left"))) + { + beamformer_registers()->split_axis = Axis2_X; + beamformer_registers()->split_left_tree = 0; + beamformer_registers()->split_right_tree = (u64)ui->tree; + beamformer_registers()->drop_target_tree = (u64)ui->tree; + ui->drop_target_node = ui->root_node; + } + + UIPrefWidth(ui_pct(1.f, 0.5f)) ui_spacer(0); + + if (ui_build_drag_overlay_splitter(Axis2_X, 0, 0, str8("right"))) + { + beamformer_registers()->split_axis = Axis2_X; + beamformer_registers()->split_left_tree = (u64)ui->tree; + beamformer_registers()->split_right_tree = 0; + beamformer_registers()->drop_target_tree = (u64)ui->tree; + ui->drop_target_node = ui->root_node; + } + } + + if (ui_build_drag_overlay_splitter(Axis2_Y, 0, 0, str8("bottom"))) + { + beamformer_registers()->split_axis = Axis2_Y; + beamformer_registers()->split_left_tree = (u64)ui->tree; + beamformer_registers()->split_right_tree = 0; + beamformer_registers()->drop_target_tree = (u64)ui->tree; + ui->drop_target_node = ui->root_node; + } + } + + struct tree_frame { + BeamformerUIPanel *tree; + UINode *node; + } init[64]; + + struct { + struct tree_frame *data; + da_count count; + da_count capacity; + } stack = {init, 0, countof(init)}; + + UIChildLayoutAxis(Axis2_Y) + UIPrefWidth(ui_px(window_rect.size.x, 1.f)) + UIPrefHeight(ui_px(window_rect.size.y, 1.f)) + ui->drag_overlay_root = ui_node_from_string(0, str8("drag_overlay_root")); + + *da_push(ui_build_arena(), &stack) = (struct tree_frame){ + .node = ui->drag_overlay_root, + .tree = ui->tree, + }; + while (stack.count) { + struct tree_frame *top = stack.data + --stack.count; + + BeamformerUIPanel *panel = top->tree; + UINode *top_node = top->node; + + UIParent(top_node) + switch (panel->kind) { + default:{}break; + case BeamformerPanelKind_Split:{ + Axis2 axis = panel->u.split.axis; + top_node->child_layout_axis = axis; + + UIAxisSize(axis2_flip(axis), ui_pct(1.f, 0.5f)) + { + f32 split_pct = panel->u.split.fraction; + + UINode *spacer; + UIAxisSize(axis, ui_pct(split_pct, 0.5f)) spacer = ui_spacer(0); + + if (panel->first_child->kind == BeamformerPanelKind_Split) { + *da_push(ui_build_arena(), &stack) = (struct tree_frame){ + .tree = panel->first_child, + .node = spacer, + }; + } + + UIChildLayoutAxis(axis2_flip(axis)) + UIAxisAlign(axis2_flip(axis), Center) + UIAxisSize(axis, ui_children_sum(1.f)) + UIParent(ui_spacer(0)) + { + // TODO(rnp): cleanup + Stream sb = arena_stream(*ui_build_arena()); + stream_appendf(&sb, "###%p_split", panel); + s8 tag = arena_stream_commit(ui_build_arena(), &sb); + + if (ui_build_drag_split_box(axis, 1, -1, str8_from_s8(tag))) { + beamformer_registers()->split_axis = axis; + beamformer_registers()->split_left_tree = (u64)panel; + beamformer_registers()->split_right_tree = (u64)ui->drag_panel; + beamformer_registers()->drop_target_tree = (u64)panel; + ui->drop_target_node = ui_top_parent(); + } + } + + UIAxisSize(axis, ui_pct(1.f - split_pct, 0.5f)) spacer = ui_spacer(0); + + if (panel->last_child->kind == BeamformerPanelKind_Split) { + *da_push(ui_build_arena(), &stack) = (struct tree_frame){ + .tree = panel->last_child, + .node = spacer, + }; + } + } + }break; + } + } + + ui->drag_overlay_tab_root = 0; + if (beamformer_registers()->drop_target_tree && + !beamformer_registers()->split_left_tree && + !beamformer_registers()->split_right_tree) + { + BeamformerUIPanel *group = (BeamformerUIPanel *)beamformer_registers()->drop_target_tree; + UINode *target = ui_node_from_key(ui->drop_target_key); + + assert(!group->parent || (group->parent && group->parent->kind == BeamformerPanelKind_Split)); + Axis2 parent_axis = group->parent ? group->parent->u.split.axis : Axis2_Count; + + Rect tr = ui_node_rect(target); + if (point_in_rect(ui->last_mouse, tr)) { + UIPrefWidth(ui_px(tr.size.x, 1.f)) + UIPrefHeight(ui_px(tr.size.h, 1.f)) + UIAxisAlign(Axis2_X, Center) + UIAxisAlign(Axis2_Y, Center) + UIChildLayoutAxis(Axis2_Y) + { + ui->drag_overlay_tab_root = ui_node_from_string(0, str8("drag_overlay_tab_root")); + } + + ui->drag_overlay_tab_root->computed_position[Axis2_X] = tr.pos.x; + ui->drag_overlay_tab_root->computed_position[Axis2_Y] = tr.pos.y; + + UINode *inner; + UIAxisAlign(Axis2_X, Center) + UIChildLayoutAxis(Axis2_Y) + UIPrefHeight(ui_children_sum(1.f)) + UIPrefWidth(ui_children_sum(1.f)) + UIParent(ui->drag_overlay_tab_root) + inner = ui_spacer(0); + + UIParent(inner) + { + if (parent_axis != Axis2_Y && ui_build_drag_split_box(Axis2_Y, 1, 0, str8("top"))) + { + beamformer_registers()->split_axis = Axis2_Y; + beamformer_registers()->split_left_tree = (u64)ui->drag_panel; + beamformer_registers()->split_right_tree = (u64)group; + ui->drop_target_node = target; + } + + ui_padh(UI_NODE_PAD); + + UIPrefHeight(ui_children_sum(1.f)) + UIPrefWidth(ui_children_sum(1.f)) + UIParent(ui_spacer(0)) + { + if (parent_axis != Axis2_X && ui_build_drag_split_box(Axis2_X, 1, 0, str8("left"))) + { + beamformer_registers()->split_axis = Axis2_X; + beamformer_registers()->split_left_tree = (u64)ui->drag_panel; + beamformer_registers()->split_right_tree = (u64)group; + ui->drop_target_node = target; + } + + ui_padw(UI_NODE_PAD); + + if (parent_axis != Axis2_Count && + ui_build_drag_split_box(axis2_flip(parent_axis), 0, 0, str8("center"))) + { + beamformer_registers()->split_left_tree = (u64)group; + beamformer_registers()->split_right_tree = (u64)group; + beamformer_registers()->drop_target_tree = (u64)group; + beamformer_registers()->drop_child_index = group->child_count; + ui->drop_target_node = target; + } + + ui_padw(UI_NODE_PAD); + + if (parent_axis != Axis2_X && ui_build_drag_split_box(Axis2_X, 1, 1, str8("right"))) + { + beamformer_registers()->split_axis = Axis2_X; + beamformer_registers()->split_left_tree = (u64)group; + beamformer_registers()->split_right_tree = (u64)ui->drag_panel; + ui->drop_target_node = target; + } + } + + ui_padh(UI_NODE_PAD); + + if (parent_axis != Axis2_Y && ui_build_drag_split_box(Axis2_Y, 1, 1, str8("bottom"))) + { + beamformer_registers()->split_axis = Axis2_Y; + beamformer_registers()->split_left_tree = (u64)group; + beamformer_registers()->split_right_tree = (u64)ui->drag_panel; + ui->drop_target_node = target; + } + } + } + } +} + +function void +ui_build_context_menu_for_panel(BeamformerUIPanel *panel) +{ + if (!panel) + { + // TODO(rnp): command pallete + } + + if (panel) { + UIAxisSize(Axis2_X, ui_px(0.f, 0.5f)) ui_padh(0.8f * UI_NODE_PAD); + switch(panel->kind) { + InvalidDefaultCase; + + case BeamformerPanelKind_TabGroup:{ + for EachElement(beamformer_panel_infos, it) { + BeamformerPanelInfo *info = beamformer_panel_infos + it; + b32 list = (info->flags & BeamformerPanelFlags_List) != 0; + b32 needs_frame = (info->flags & BeamformerPanelFlags_NeedsFrame) != 0; + if (list && (!needs_frame || beamformer_frame_valid(beamformer_registers()->frame))) { + UIChildLayoutAxis(Axis2_X) + UIPrefHeight(ui_children_sum(1.f)) + UIPrefWidth(ui_children_sum(1.f)) + UIParent(ui_spacer(0)) + { + UISignal signal; + ui_padw(UI_NODE_PAD); + UIPrefHeight(ui_text_dim(1.1f, 1.f)) + UIPrefWidth(ui_text_dim(1.f, 1.f)) + signal = ui_label_button(info->display); + ui_padw(UI_NODE_PAD); + + if ui_pressed(signal) { + ui_context_menu_close(); + beamformer_command(beamformer_command_infos[BeamformerCommandKind_OpenTab].string, + .tree_node = (u64)panel, + .string = info->string); + } + } + } + } + }break; + + case BeamformerPanelKind_FrameViewCopy: + case BeamformerPanelKind_FrameViewLive: + { + BeamformerFrameView *view = panel->u.frame_view; + + UINode *label_column, *button_column; + UIChildLayoutAxis(Axis2_X) + UIPrefHeight(ui_children_sum(1.f)) + UIPrefWidth(ui_children_sum(1.f)) + UIParent(ui_spacer(0)) + UIChildLayoutAxis(Axis2_Y) + { + ui_padw(UI_NODE_PAD); + UIAxisAlign(Axis2_X, Left) label_column = ui_node_from_string(0, str8("###labels")); + ui_padw(UI_NODE_PAD * 2.f); + UIAxisAlign(Axis2_X, Center) + button_column = ui_node_from_string(0, str8("###buttons")); + ui_padw(UI_NODE_PAD); + } + + UIPrefHeight(ui_text_dim(1.1f, 1.f)) + UIPrefWidth(ui_text_dim(1.f, 1.f)) + { + read_only local_persist str8 dimension_strings[2][2] = { + {str8_comp("Extent Scale Bar"), str8_comp("Magnitude Scale Bar")}, + {str8_comp("Lateral Scale Bar"), str8_comp("Axial Scale Bar") }, + }; + + UIParent(label_column) ui_label(str8("Plane Tag")); + UIParent(button_column) + UIFlags(UINodeFlag_Scroll) + { + str8 tag = str8("Any"); + if (view->view_plane != BeamformerViewPlaneTag_Count) + tag = beamformer_view_plane_tag_strings[view->view_plane]; + UISignal signal = ui_label_button(push_str8_from_parts(ui_build_arena(), str8(""), + tag, str8("###PlaneTagButton"))); + i32 delta = signal.scroll.y + ui_pressed(signal); + view->view_plane = circular_add(view->view_plane, delta, BeamformerViewPlaneTag_Count + 1); + if (ui_pressed(signal) || ui_scrolled(signal)) + view->dirty = 1; + } + + i32 dimension = iv3_dimension(view->frame.points); + dimension = Min(dimension, 2); + if (dimension > 0) { + for EachEnumValue(Axis2, axis) { + f32 row_height; + UIParent(label_column) + row_height = ui_label(dimension_strings[dimension - 1][axis]).node->computed_size[Axis2_Y]; + + UIParent(button_column) + // TODO(rnp): ui_em(1.f, 1.f) once font size matches directly + UIPrefHeight(ui_px(row_height, 1.f)) + UIPrefWidth(ui_px(row_height, 1.f)) + { + UISignal signal = ui_toggle_buttonf(view->scale_bar_active[axis], "###axis_%u", axis); + if ui_pressed(signal) + view->scale_bar_active[axis] = !view->scale_bar_active[axis]; + } + } + } + + { + f32 row_height; + UIParent(label_column) + row_height = ui_label(str8("Log Scale")).node->computed_size[Axis2_Y]; + + UIParent(button_column) + // TODO(rnp): ui_em(1.f, 1.f) once font size matches directly + UIPrefHeight(ui_px(row_height, 1.f)) + UIPrefWidth(ui_px(row_height, 1.f)) + { + UISignal signal = ui_toggle_button(view->log_scale, str8("###log_scale")); + if ui_pressed(signal) { + view->log_scale = !view->log_scale; + view->dirty = 1; + } + } + } + + if (dimension > 0 && panel->kind != BeamformerPanelKind_FrameViewCopy) { + f32 row_height; + UIParent(label_column) + { + UISignal signal = ui_label_button(str8("Copy Frame")); + row_height = signal.node->computed_size[Axis2_Y]; + if ui_pressed(signal) { + ui_context_menu_close(); + beamformer_command(beamformer_command_infos[BeamformerCommandKind_OpenTab].string, + .tree_node = (u64)panel->parent, + .frame_view = (u64)view, + .string = beamformer_panel_infos[BeamformerPanelKind_FrameViewCopy].string); + } + } + + // TODO(rnp): ui_em(1.f, 1.f) once font size matches directly + UIParent(button_column) ui_padh(row_height); + } + + // TODO(rnp): extra frame view copy settings + if (panel->kind == BeamformerPanelKind_FrameViewCopy) { + } + } + }break; + + case BeamformerPanelKind_FrameViewXPlane:{ + BeamformerFrameView *view = panel->u.frame_view; + + UINode *label_column, *button_column; + UIChildLayoutAxis(Axis2_X) + UIPrefHeight(ui_children_sum(1.f)) + UIPrefWidth(ui_children_sum(1.f)) + UIParent(ui_spacer(0)) + UIChildLayoutAxis(Axis2_Y) + { + ui_padw(UI_NODE_PAD); + UIAxisAlign(Axis2_X, Left) label_column = ui_node_from_string(0, str8("###labels")); + ui_padw(UI_NODE_PAD * 2.f); + UIAxisAlign(Axis2_X, Center) + button_column = ui_node_from_string(0, str8("###buttons")); + ui_padw(UI_NODE_PAD); + } + + UIPrefHeight(ui_text_dim(1.1f, 1.f)) + UIPrefWidth(ui_text_dim(1.f, 1.f)) + { + { + f32 row_height; + UIParent(label_column) + row_height = ui_label(str8("Log Scale")).node->computed_size[Axis2_Y]; + + UIParent(button_column) + // TODO(rnp): ui_em(1.f, 1.f) once font size matches directly + UIPrefHeight(ui_px(row_height, 1.f)) + UIPrefWidth(ui_px(row_height, 1.f)) + { + UISignal signal = ui_toggle_button(view->log_scale, str8("###log_scale")); + if ui_pressed(signal) { + view->log_scale = !view->log_scale; + view->dirty = 1; + } + } + } + + { + f32 row_height; + UIParent(label_column) + row_height = ui_label(str8("Demo Mode")).node->computed_size[Axis2_Y]; + + UIParent(button_column) + // TODO(rnp): ui_em(1.f, 1.f) once font size matches directly + UIPrefHeight(ui_px(row_height, 1.f)) + UIPrefWidth(ui_px(row_height, 1.f)) + { + UISignal signal = ui_toggle_button(view->demo, str8("###demo_mode")); + if ui_pressed(signal) + view->demo = !view->demo; + } + } + + UIParent(label_column) + { + f32 row_height = ui_label(str8("Planes:")).node->computed_size[Axis2_Y]; + // TODO(rnp): ui_em(1.f, 1.f) once font size matches directly + UIParent(button_column) ui_padh(row_height); + } + for EachElement(view->plane_active, plane) { + f32 row_height; + UIParent(label_column) + { + str8 label = push_str8_from_parts(ui_build_arena(), str8(""), str8(" "), + beamformer_view_plane_tag_strings[plane]); + row_height = ui_label(label).node->computed_size[Axis2_Y]; + } + + UIParent(button_column) + UIPrefHeight(ui_px(row_height, 1.f)) + UIPrefWidth(ui_px(row_height, 1.f)) + { + UISignal signal = ui_toggle_button(view->plane_active[plane], + beamformer_view_plane_tag_strings[plane]); + if ui_pressed(signal) + view->plane_active[plane] = !view->plane_active[plane]; + } + } + } + }break; + + case BeamformerPanelKind_ParameterListing:{ + UINode *label_column, *button_column; + UIChildLayoutAxis(Axis2_X) + UIPrefHeight(ui_children_sum(1.f)) + UIPrefWidth(ui_children_sum(1.f)) + UIParent(ui_spacer(0)) + UIChildLayoutAxis(Axis2_Y) + { + ui_padw(UI_NODE_PAD); + UIAxisAlign(Axis2_X, Left) label_column = ui_node_from_string(0, str8("###labels")); + ui_padw(UI_NODE_PAD * 2.f); + UIAxisAlign(Axis2_X, Center) + button_column = ui_node_from_string(0, str8("###buttons")); + ui_padw(UI_NODE_PAD); + } + + UIPrefHeight(ui_text_dim(1.1f, 1.f)) + UIPrefWidth(ui_text_dim(1.f, 1.f)) + { + UIParent(label_column) ui_label(str8("Block")); + UIParent(button_column) + { + UISignal signal; + u32 cycle = beamformer_context->shared_memory->reserved_parameter_blocks; + u32 block = panel->u.parameter_listing.parameter_block; + UIFlags(cycle <= 1 ? UINodeFlag_Disabled : 0) + signal = ui_label_buttonf("%u", block); + if (ui_pressed(signal) || ui_scrolled(signal)) { + i32 delta = signal.scroll.y + ui_pressed(signal); + panel->u.parameter_listing.parameter_block = circular_add(block, delta, cycle); + } + } + } + }break; + + case BeamformerPanelKind_LiveImagingControls:{ + BeamformerLiveImagingParameters *lip = &beamformer_context->shared_memory->live_imaging_parameters; + u64 enabled_kinds = atomic_load_u64(&lip->acquisition_kind_enabled_flags); + + UIChildLayoutAxis(Axis2_X) + UIPrefHeight(ui_children_sum(1.f)) + UIPrefWidth(ui_children_sum(1.f)) + for EachBit(enabled_kinds, kind) + UIParent(ui_spacer(0)) + { + UISignal signal; + ui_padw(UI_NODE_PAD); + UIPrefHeight(ui_text_dim(1.1f, 1.f)) + UIPrefWidth(ui_text_dim(1.f, 1.f)) + signal = ui_label_button(beamformer_acquisition_kind_strings[kind]); + ui_padw(UI_NODE_PAD); + + if ui_pressed(signal) { + ui_context_menu_close(); + lip->acquisition_kind = kind; + atomic_or_u32(&beamformer_context->shared_memory->live_imaging_dirty_flags, + BeamformerLiveImagingDirtyFlags_AcquisitionKind); + } + } + }break; + + } + UIAxisSize(Axis2_X, ui_px(0.f, 0.5f)) ui_padh(0.8f * UI_NODE_PAD); } } function void -begin_text_input(InputState *is, Rect r, Variable *container, v2 mouse) +ui_layout_constrain(UINode *root) { - assert(container->type == VT_UI_TEXT_BOX); - Font *font = is->font = is->hot_font; - Stream s = {.cap = countof(is->buf), .data = is->buf}; - stream_append_variable(&s, container->view.child); - is->count = s.widx; - is->container = container; - - is->numeric = container->view.child->type != VT_LIVE_CONTROLS_STRING; - if (container->view.child->type == VT_LIVE_CONTROLS_STRING) { - BeamformerLiveImagingParameters *lip = container->view.child->generic; - if (lip->save_name_tag_length <= 0) - is->count = 0; + assert(!ui_node_is_nil(root->first_child)); + + // NOTE(rnp): for violations in non-layout axis all we can do is clamp + { + Axis2 axis = axis2_flip(root->child_layout_axis); + if ((root->flags & (UINodeFlag_AllowOverflowX << axis)) == 0) { + for (UINode *child = root->first_child; !ui_node_is_nil(child); child = child->next_sibling) + child->computed_size[axis] = Min(child->computed_size[axis], root->computed_size[axis]); + } } - /* NOTE: extra offset to help with putting a cursor at idx 0 */ - f32 text_half_char_width = 10.0f; - f32 hover_p = CLAMP01((mouse.x - r.pos.x) / r.size.w); - i32 i; - f32 x_off = text_half_char_width, x_bounds = r.size.w * hover_p; - for (i = 0; i < is->count && x_off < x_bounds; i++) { - /* NOTE: assumes font glyphs are ordered ASCII */ - i32 idx = is->buf[i] - 0x20; - x_off += (f32)font->glyphs[idx].advanceX; - if (font->glyphs[idx].advanceX == 0) - x_off += font->recs[idx].width; + Axis2 axis = root->child_layout_axis; + if ((root->flags & (UINodeFlag_AllowOverflowX << axis)) == 0) { + f32 allowed_size = root->computed_size[axis]; + f32 total_size = 0; + f32 total_weighted_size = 0; + + for (UINode *child = root->first_child; !ui_node_is_nil(child); child = child->next_sibling) { + total_size += child->computed_size[axis]; + total_weighted_size += child->computed_size[axis] * (1.0f - child->semantic_size[axis].strictness); + } + + f32 remaining_size = root->computed_size[axis]; + f32 violation = total_size - allowed_size; + if (violation > 0 && total_weighted_size > 0) { + f32 fixup_fraction = Clamp01(violation / total_weighted_size); + for (UINode *child = root->first_child; !ui_node_is_nil(child); child = child->next_sibling) { + f32 fixup = Max(0, child->computed_size[axis] * (1.0f - child->semantic_size[axis].strictness)); + child->computed_size[axis] -= fixup * fixup_fraction; + + if (child->semantic_size[axis].kind != UISizeKind_PercentOfParent) + remaining_size -= child->computed_size[axis]; + } + } + + // NOTE(rnp): fixup sizes dependant on parent + for (UINode *child = root->first_child; !ui_node_is_nil(child); child = child->next_sibling) + if (child->semantic_size[axis].kind == UISizeKind_PercentOfParent) + child->computed_size[axis] = remaining_size * child->semantic_size[axis].value; } - is->cursor = i; } function void -end_text_input(InputState *is, Variable *var) +ui_layout_nodes(UINode *root) { - f32 value = 0; - if (is->numeric) { - NumberConversion number = number_from_str8((str8){.length = is->count, .data = is->buf}); - value = number.F64; - } + struct node_frame { + UINode *node; + // NOTE(rnp): for post order traversal + b32 visited; + } init[64] = {0}; - switch (var->type) { - case VT_SCALED_F32:{ var->scaled_real32.val = value; }break; - case VT_F32:{ var->real32 = value; }break; - case VT_BEAMFORMER_VARIABLE:{ - BeamformerVariable *bv = &var->beamformer_variable; - *bv->store = CLAMP(value / bv->display_scale, bv->limits.x, bv->limits.y); - var->hover_t = 0; - }break; - case VT_LIVE_CONTROLS_STRING:{ - BeamformerLiveImagingParameters *lip = var->generic; - mem_copy(lip->save_name_tag, is->buf, (uz)is->count % countof(lip->save_name_tag)); - lip->save_name_tag_length = is->count % countof(lip->save_name_tag); - }break; - InvalidDefaultCase; - } -} + struct { + struct node_frame *data; + da_count count; + da_count capacity; + } stack = {init, 0, countof(init)}; -function b32 -update_text_input(InputState *is, Variable *var) -{ - assert(is->cursor != -1); + /////////////////////// + // NOTE(rnp): First Pass: non dependant sizes + da_push(ui_build_arena(), &stack)->node = root; + while (stack.count) { + struct node_frame *top = stack.data + --stack.count; + UINode *node = top->node; + + if (node->flags & UINodeFlag_DrawText) { + Font font = ui_font_for_node(node); + str8 string = ui_draw_part_from_key_string(node->string); + if (node->flags & UINodeFlag_IconText) + node->text_size = measure_text_tight(font, string); + else + node->text_size = measure_text(font, string); + } - ui_blinker_update(&is->cursor_blink, BLINK_SPEED); + for EachElement(node->semantic_size, it) { + switch (node->semantic_size[it].kind) { + case UISizeKind_Pixels:{node->computed_size[it] = node->semantic_size[it].value;}break; - var->hover_t -= 2 * HOVER_SPEED * dt_for_frame; - var->hover_t = CLAMP01(var->hover_t); + case UISizeKind_TextContent:{ + node->computed_size[it] = node->semantic_size[it].value * node->text_size.E[it]; + }break; - /* NOTE: handle multiple input keys on a single frame */ - for (i32 key = GetCharPressed(); - is->count < countof(is->buf) && key > 0; - key = GetCharPressed()) - { - b32 allow_key = !is->numeric || (BETWEEN(key, '0', '9') || (key == '.') || - (key == '-' && is->cursor == 0)); - if (allow_key) { - memory_move(is->buf + is->cursor + 1, - is->buf + is->cursor, - (uz)(is->count - is->cursor)); - is->buf[is->cursor++] = (u8)key; - is->count++; + default:{}break; + } } + + // NOTE(rnp): push children + for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling) + da_push(ui_build_arena(), &stack)->node = child; } - is->cursor -= (IsKeyPressed(KEY_LEFT) || IsKeyPressedRepeat(KEY_LEFT)) && is->cursor > 0; - is->cursor += (IsKeyPressed(KEY_RIGHT) || IsKeyPressedRepeat(KEY_RIGHT)) && is->cursor < is->count; + /////////////////////// + // NOTE(rnp): Second Pass (Pre Order): parent dependant sizes + da_push(ui_build_arena(), &stack)->node = root; + while (stack.count) { + struct node_frame *top = stack.data + --stack.count; + UINode *node = top->node; - if ((IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE)) && is->cursor > 0) { - is->cursor--; - if (is->cursor < countof(is->buf) - 1) { - memory_move(is->buf + is->cursor, - is->buf + is->cursor + 1, - (uz)(is->count - is->cursor - 1)); + for EachElement(node->semantic_size, it) { + if (node->semantic_size[it].kind == UISizeKind_PercentOfParent) { + f32 parent_size = node->parent->computed_size[it]; + node->computed_size[it] = node->semantic_size[it].value * parent_size; + } } - is->count--; - } - if ((IsKeyPressed(KEY_DELETE) || IsKeyPressedRepeat(KEY_DELETE)) && is->cursor < is->count) { - memory_move(is->buf + is->cursor, - is->buf + is->cursor + 1, - (uz)(is->count - is->cursor - 1)); - is->count--; + // NOTE(rnp): push children + for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling) + da_push(ui_build_arena(), &stack)->node = child; } - b32 result = IsKeyPressed(KEY_ENTER); - return result; -} + /////////////////////// + // NOTE(rnp): Third Pass (Post Order): child dependant sizes + da_push(ui_build_arena(), &stack)->node = root; + while (stack.count) { + struct node_frame *top = stack.data + stack.count - 1; -function void -scale_bar_interaction(BeamformerUI *ui, ScaleBar *sb, v2 mouse) -{ - Interaction *it = &ui->interaction; - b32 mouse_left_pressed = IsMouseButtonPressed(MOUSE_BUTTON_LEFT); - b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT); - f32 mouse_wheel = GetMouseWheelMoveV().y; - - if (mouse_left_pressed) { - v2 world_mouse = screen_point_to_world_2d(mouse, it->rect.pos, - v2_add(it->rect.pos, it->rect.size), - (v2){{*sb->min_value, *sb->min_value}}, - (v2){{*sb->max_value, *sb->max_value}}); - f32 new_coord = F32_INFINITY; - switch (sb->direction) { - case SB_LATERAL: new_coord = world_mouse.x; break; - case SB_AXIAL: new_coord = world_mouse.y; break; - } - if (sb->zoom_starting_coord == F32_INFINITY) { - sb->zoom_starting_coord = new_coord; + UINode *node = top->node; + if (!top->visited && node->child_count) { + top->visited = 1; + + // NOTE(rnp): push children + for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling) + da_push(ui_build_arena(), &stack)->node = child; } else { - f32 min = sb->zoom_starting_coord; - f32 max = new_coord; - if (min > max) swap(min, max); + // NOTE(rnp): pop + stack.count--; + + for EachElement(node->semantic_size, it) { + if (node->semantic_size[it].kind == UISizeKind_ChildrenSum) { + f32 size_sum = 0; + for (UINode *child = node->first_child; + !ui_node_is_nil(child); + child = child->next_sibling) + { + if (it == node->child_layout_axis) { + size_sum += child->computed_size[it]; + } else { + size_sum = Max(size_sum, child->computed_size[it]); + } + } + node->computed_size[it] = size_sum; + } + } + } + } - v2_sll *savepoint = SLLPopFreelist(ui->scale_bar_savepoint_freelist); - if (!savepoint) savepoint = push_struct(&ui->arena, v2_sll); + /////////////////////// + // NOTE(rnp): Fourth Pass (Pre Order): solve violations + da_push(ui_build_arena(), &stack)->node = root; + while (stack.count) { + struct node_frame *top = stack.data + --stack.count; - savepoint->v.x = *sb->min_value; - savepoint->v.y = *sb->max_value; - SLLPush(savepoint, sb->savepoint_stack); + UINode *node = top->node; + if (node->child_count) + ui_layout_constrain(node); - *sb->min_value = min; - *sb->max_value = max; + // NOTE(rnp): push children + for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling) + da_push(ui_build_arena(), &stack)->node = child; + } - sb->zoom_starting_coord = F32_INFINITY; + /////////////////////// + // NOTE(rnp): Final Pass (Pre Order): fill positions + da_push(ui_build_arena(), &stack)->node = root; + while (stack.count) { + struct node_frame *top = stack.data + --stack.count; + + UINode *node = top->node; + Axis2 layout_axis = node->child_layout_axis; + Axis2 flipped_axis = axis2_flip(layout_axis); + f32 offset = 0; + for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling) { + child->computed_position[flipped_axis] = node->computed_position[flipped_axis]; + child->computed_position[layout_axis] = offset + node->computed_position[layout_axis]; + offset += child->computed_size[layout_axis]; } - } - if (mouse_right_pressed) { - v2_sll *savepoint = sb->savepoint_stack; - if (savepoint) { - *sb->min_value = savepoint->v.x; - *sb->max_value = savepoint->v.y; - sb->savepoint_stack = savepoint->next; - SLLPushFreelist(savepoint, ui->scale_bar_savepoint_freelist); + for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling) { + for EachElement(node->alignment, axis) { + f32 size_delta = node->computed_size[axis] - child->computed_size[axis]; + child->computed_position[axis] += ui_alignment_correction(node->alignment[axis], size_delta); + } } - sb->zoom_starting_coord = F32_INFINITY; - } - if (mouse_wheel != 0) { - *sb->min_value += mouse_wheel * sb->scroll_scale.x; - *sb->max_value += mouse_wheel * sb->scroll_scale.y; + // NOTE(rnp): push children + for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling) + if (child->child_count > 0) + da_push(ui_build_arena(), &stack)->node = child; } } function void -ui_widget_bring_to_front(Variable *sentinal, Variable *widget) +ui_draw_nodes(UINode *root, Rect window_rect) { - /* TODO(rnp): clean up the linkage so this can be a macro */ - widget->parent->next = widget->next; - widget->next->parent = widget->parent; - - widget->parent = sentinal; - widget->next = sentinal->next; - widget->next->parent = widget; - sentinal->next = widget; -} + BeamformerUI *ui = ui_context; -function void -ui_view_close(BeamformerUI *ui, Variable *view) -{ - switch (view->type) { - case VT_UI_MENU: - case VT_UI_TEXT_BOX: - { - UIView *fw = &view->view; - if (view->type == VT_UI_MENU) { - assert(fw->child->type == VT_GROUP); - fw->child->group.expanded = 0; - fw->child->group.container = 0; - } else { - end_text_input(&ui->text_input_state, fw->child); - } - view->parent->next = view->next; - view->next->parent = view->parent; - if (fw->close) SLLPushFreelist(fw->close, ui->variable_freelist); - SLLPushFreelist(view, ui->variable_freelist); - }break; - case VT_UI_VIEW:{ - assert(view->parent->type == VT_UI_REGION_SPLIT); - Variable *region = view->parent; + struct node_frame { + b32 visited; + UINode *node; + } init[64]; + + struct { + struct node_frame *data; + da_count count; + da_count capacity; + } stack = {init, 0, countof(init)}; - Variable *parent = region->parent; - Variable *remaining = region->region_split.left; - if (remaining == view) remaining = region->region_split.right; + u32 colour_index = 0; + (void)colour_index; - ui_view_free(ui, view); + da_push(ui_build_arena(), &stack)->node = root; + while (stack.count) { + struct node_frame *top = stack.data + stack.count - 1; - assert(parent->type == VT_UI_REGION_SPLIT); - if (parent->region_split.left == region) { - parent->region_split.left = remaining; - } else { - parent->region_split.right = remaining; - } - remaining->parent = parent; + UINode *node = top->node; + if (!top->visited) { + top->visited = 1; - SLLPushFreelist(region, ui->variable_freelist); - }break; - InvalidDefaultCase; - } -} + Rect r = ui_node_rect(node); + if (node->flags & UINodeFlag_Clip) + BeginScissorMode(r.pos.x, r.pos.y, r.size.w, r.size.h); -function void -ui_button_interaction(BeamformerUI *ui, Variable *button) -{ - assert(button->type == VT_UI_BUTTON); - if (Between(button->button, UI_BID_ACQUISITION_KIND_FIRST, UI_BID_ACQUISITION_KIND_LAST)) { - BeamformerSharedMemory *sm = ui->shared_memory; - BeamformerLiveImagingParameters *lip = &sm->live_imaging_parameters; - - lip->acquisition_kind = button->button - UI_BID_ACQUISITION_KIND_FIRST; - atomic_or_u32(&sm->live_imaging_dirty_flags, BeamformerLiveImagingDirtyFlags_AcquisitionKind); - ui_view_close(ui, button->parent->group.container); - } else { - switch (button->button) { - case UI_BID_VIEW_CLOSE:{ ui_view_close(ui, button->parent); }break; - case UI_BID_FV_COPY_HORIZONTAL:{ - ui_copy_frame(ui, button->parent->parent, RSD_HORIZONTAL); - }break; - case UI_BID_FV_COPY_VERTICAL:{ - ui_copy_frame(ui, button->parent->parent, RSD_VERTICAL); - }break; - case UI_BID_GM_OPEN_VIEW_RIGHT:{ - ui_add_live_frame_view(ui, button->parent->parent, RSD_HORIZONTAL, BeamformerFrameViewKind_Latest); - }break; - case UI_BID_GM_OPEN_VIEW_BELOW:{ - ui_add_live_frame_view(ui, button->parent->parent, RSD_VERTICAL, BeamformerFrameViewKind_Latest); - }break; - InvalidDefaultCase; - } - } -} + if (node->flags & UINodeFlag_ViewScroll) { + v2 view_off = node->view_scroll_offset; + rlPushMatrix(); + rlTranslatef(-view_off.x, -view_off.y, 0); + } -function void -ui_begin_interact(BeamformerUI *ui, v2 mouse, b32 scroll) -{ - Interaction hot = ui->hot_interaction; - if (hot.kind != InteractionKind_None) { - if (hot.kind == InteractionKind_Auto) { - switch (hot.var->type) { - case VT_NULL:{ hot.kind = InteractionKind_Nop; }break; - case VT_B32:{ hot.kind = InteractionKind_Set; }break; - case VT_SCALE_BAR:{ hot.kind = InteractionKind_Set; }break; - case VT_UI_BUTTON:{ hot.kind = InteractionKind_Button; }break; - case VT_GROUP:{ hot.kind = InteractionKind_Set; }break; - case VT_UI_TEXT_BOX: - case VT_UI_MENU: - { - if (hot.var->type == VT_UI_MENU) { - hot.kind = InteractionKind_Drag; - } else { - hot.kind = InteractionKind_Text; - begin_text_input(&ui->text_input_state, hot.rect, hot.var, mouse); - } - ui_widget_bring_to_front(&ui->floating_widget_sentinal, hot.var); + //v4 colour = g_colour_palette[(colour_index++) % countof(g_colour_palette)]; + //DrawRectangleLinesEx(rl_rect(r), 4.0f, colour_from_normalized(colour)); - // TODO(rnp): hack. this won't be needed with a proper immediate mode UI - if (ui->interaction.kind == InteractionKind_Text) - hot.var = hot.var->view.child; - }break; - case VT_UI_VIEW:{ - if (scroll) hot.kind = InteractionKind_Scroll; - else hot.kind = InteractionKind_Nop; - }break; - case VT_X_PLANE_SHIFT:{ - assert(hot.var->parent && hot.var->parent->type == VT_BEAMFORMER_FRAME_VIEW); - BeamformerFrameView *bv = hot.var->parent->generic; - if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { - XPlaneShift *xp = &hot.var->x_plane_shift; - xp->start_point = xp->end_point = bv->hit_test_point; - hot.kind = InteractionKind_Drag; - } else { - if (scroll) { - hot.kind = InteractionKind_Scroll; - hot.var = &bv->threshold; - } else { - hot.kind = InteractionKind_Nop; - } - } - }break; - case VT_BEAMFORMER_FRAME_VIEW:{ - if (scroll) { - hot.kind = InteractionKind_Scroll; - } else { - BeamformerFrameView *bv = hot.var->generic; - switch (bv->kind) { - case BeamformerFrameViewKind_3DXPlane:{ hot.kind = InteractionKind_Drag; }break; - default:{ - hot.kind = InteractionKind_Nop; - switch (++bv->ruler.state) { - case RulerState_Start:{ - hot.kind = InteractionKind_Ruler; - bv->ruler.start = world_point_from_plane_uv(bv->frame.voxel_transform, - rect_uv(mouse, hot.rect)); - }break; - case RulerState_Hold:{}break; - default:{ bv->ruler.state = RulerState_None; }break; + v4 bg_colour = node->bg_colour; + if (node->flags & UINodeFlag_DrawHotEffects) + bg_colour = v4_lerp(bg_colour, HOVERED_COLOUR, node->hot_t); + + if (node->flags & UINodeFlag_DrawBackground) + DrawRectangleRec(rl_rect(r), colour_from_normalized(bg_colour)); + + if (node->flags & UINodeFlag_DrawBorder) { + v4 colour = node->border_colour; + u64 masked = node->flags & (UINodeFlag_DrawBackground|UINodeFlag_DrawHotEffects); + if (masked == UINodeFlag_DrawHotEffects) + colour = v4_lerp(colour, HOVERED_COLOUR, node->hot_t); + + DrawRectangleLinesEx(rl_rect(r), node->border_thickness, colour_from_normalized(colour)); + } + + if (node->flags & UINodeFlag_CustomDraw) { + node->custom_draw_function(node, r); + } else { + if (node->flags & UINodeFlag_DrawText) { + Font font = ui_font_for_node(node); + + TextSpec text_spec = { + .font = &font, + .flags = TF_LIMITED, + .colour = node->text_colour, + .outline_colour = node->text_outline_colour, + .outline_thick = node->text_outline_thickness, + .limits.size = r.size, + }; + if (node->text_outline_thickness > 0) + text_spec.flags |= TF_OUTLINED; + + v2 pos = ui_node_text_position(node); + + UITextInputState *tis = &ui_context->text_input_state; + b32 input = ui_node_key_equal(node->key, tis->node_key); + // TODO(rnp): cleanup: visible part + str8 string = ui_draw_part_from_key_string(node->string); + if (!input && node->flags & UINodeFlag_DrawHotEffects && (node->flags & UINodeFlag_DrawBackground) == 0) + text_spec.colour = v4_lerp(text_spec.colour, HOVERED_COLOUR, node->hot_t); + + if (node->flags & UINodeFlag_IconText) + draw_text_tight(*text_spec.font, string, pos, colour_from_normalized(text_spec.colour)); + else + draw_text(string, pos, &text_spec); + + if (input) { + iv2 range = ui_text_input_cursor_range(); + str8 parts[2]; + parts[0] = (str8){.data = string.data, .length = range.x}; + parts[1] = (str8){.data = string.data + range.x, .length = range.y - range.x}; + + Rect cursor = {.pos = pos}; + cursor.pos.x += measure_text(font, parts[0]).x; + + v4 cursor_colour = FOCUSED_COLOUR; + if (parts[1].length > 0) { + cursor_colour = SELECTION_COLOUR; + cursor.size = measure_text(font, parts[1]); + + if (range.x == 0) { + cursor.pos.x -= 2.f; + cursor.size.x += 2.f; + } + + if (range.y == tis->count) + cursor.size.x += 2.f; + } else { + cursor_colour.a = ease_in_out_cubic(ui->text_input_state.blinker.t); + cursor.size.x = string.length - range.y > 0 ? 4.0f : 0.55f * (f32)font.baseSize; + cursor.size.y = font.baseSize; } - }break; + + if (cursor.size.x > 0) + DrawRectanglePro(rl_rect(cursor), (Vector2){0}, 0, colour_from_normalized(cursor_colour)); } } - }break; - case VT_CYCLER:{ - if (scroll) hot.kind = InteractionKind_Scroll; - else hot.kind = InteractionKind_Set; - }break; - case VT_BEAMFORMER_VARIABLE: - case VT_LIVE_CONTROLS_STRING: - case VT_F32: - case VT_SCALED_F32: - { - if (scroll) { - hot.kind = InteractionKind_Scroll; - } else if (hot.var->flags & V_TEXT) { - hot.kind = InteractionKind_Text; - Variable *w = add_floating_view(ui, &ui->arena, VT_UI_TEXT_BOX, - hot.rect.pos, hot.var, 0); - w->view.rect = hot.rect; - begin_text_input(&ui->text_input_state, hot.rect, w, mouse); - } else { - hot.kind = InteractionKind_Drag; - } - }break; - InvalidDefaultCase; } - } - ui->interaction = hot; + // NOTE(rnp): push children + for (UINode *child = node->first_child; !ui_node_is_nil(child); child = child->next_sibling) { + Rect cr = ui_node_rect(child); + if ((cr.size.x > 0 && cr.size.y > 0) || ui_node_key_equal(child->key, ui->text_input_state.node_key)) + da_push(ui_build_arena(), &stack)->node = child; + } + } else { + // NOTE(rnp): pop + stack.count--; - if (ui->interaction.var->flags & V_LIVE_CONTROL) { - assert(ui->interaction.var->parent->type == VT_LIVE_CONTROLS_VIEW); - BeamformerLiveControlsView *lv = ui->interaction.var->parent->generic; - lv->active_field_flag = lv->hot_field_flag; - } + if (node->flags & UINodeFlag_ViewScroll) { + rlPopMatrix(); + } - if (ui->interaction.var->flags & V_HIDES_CURSOR) { - HideCursor(); - DisableCursor(); - /* wtf raylib */ - SetMousePosition((i32)mouse.x, (i32)mouse.y); + if (node->flags & UINodeFlag_Clip) + EndScissorMode(); } - } else { - ui->interaction.kind = InteractionKind_Nop; } -} - -function u32 -ui_cycler_delta_for_frame(void) -{ - u32 result = (u32)GetMouseWheelMoveV().y; - if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) result += 1; - if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) result -= 1; - return result; -} -function void -ui_extra_actions(BeamformerUI *ui, Variable *var) -{ - switch (var->type) { - case VT_CYCLER:{ - assert(var->parent && var->parent->parent && var->parent->parent->type == VT_UI_VIEW); - Variable *view_var = var->parent->parent; - UIView *view = &view_var->view; - switch (view->child->type) { - case VT_BEAMFORMER_FRAME_VIEW:{ - u32 delta = ui_cycler_delta_for_frame(); - BeamformerFrameView *old = view->child->generic; - view->child->generic = ui_beamformer_frame_view_new(ui, &ui->arena); - BeamformerFrameViewKind last_kind = ((u32)old->kind - delta) % BeamformerFrameViewKind_Count; - - /* NOTE(rnp): log_scale gets released below before its needed */ - b32 log_scale = old->log_scale->bool32; - ui_variable_free_group_items(ui, view->menu); - - ui_beamformer_frame_view_release_subresources(ui, old, last_kind); - ui_beamformer_frame_view_convert(ui, &ui->arena, view->child, view->menu, old->kind, old, log_scale); - - DLLRemove(0, old->next, old->prev, old, next, prev); - SLLPushFreelist(old, ui->view_freelist); - }break; - InvalidDefaultCase; - } - }break; - InvalidDefaultCase; - } + // TODO(rnp): can we make the mouse latency not shit? + //if (ui->current_mouse.x > 0) DrawCircle(ui->current_mouse.x, ui->current_mouse.y, 6, GREEN); } function void -ui_live_control_update(BeamformerUI *ui, Variable *controls) +beamformer_ui_panel_unlink(BeamformerUIPanel *node) { - assert(controls->type == VT_LIVE_CONTROLS_VIEW); - BeamformerLiveControlsView *lv = controls->generic; - atomic_or_u32(&ui->shared_memory->live_imaging_dirty_flags, lv->active_field_flag); + BeamformerUIPanel *parent = node->parent; + if (parent->kind == BeamformerPanelKind_TabGroup && parent->u.tab_focus == node) + parent->u.tab_focus = node->previous_sibling ? node->previous_sibling : node->next_sibling; + DLLRemove(0, parent->first_child, parent->last_child, node, next_sibling, previous_sibling); + parent->child_count--; } function void -ui_end_interact(BeamformerUI *ui, v2 mouse) +ui_kill_panel(BeamformerUIPanel *node) { - Interaction *it = &ui->interaction; - Variable *parent = it->var->parent; - u32 flags = it->var->flags; - - switch (it->kind) { - case InteractionKind_Nop:{}break; - case InteractionKind_Drag:{ - switch (it->var->type) { - case VT_X_PLANE_SHIFT:{ - assert(parent && parent->type == VT_BEAMFORMER_FRAME_VIEW); - XPlaneShift *xp = &it->var->x_plane_shift; - BeamformerFrameView *view = parent->generic; - BeamformerViewPlaneTag plane = view_plane_tag_from_x_plane_shift(view, it->var); - f32 rotation = x_plane_rotation_for_view_plane(view, plane); - m4 x_rotation = m4_rotation_about_y(rotation); - v3 Z = x_rotation.c[2].xyz; - f32 delta = v3_dot(Z, v3_sub(xp->end_point, xp->start_point)); - xp->start_point = xp->end_point; + BeamformerUI *ui = ui_context; + BeamformerUIPanel *parent = node->parent; - BeamformerSharedMemory * sm = ui->shared_memory; - BeamformerLiveImagingParameters * li = &sm->live_imaging_parameters; - li->image_plane_offsets[plane] += delta; - atomic_or_u32(&sm->live_imaging_dirty_flags, BeamformerLiveImagingDirtyFlags_ImagePlaneOffsets); - }break; - default:{}break; - } - }break; - case InteractionKind_Set:{ - switch (it->var->type) { - case VT_B32:{ it->var->bool32 = !it->var->bool32; }break; - case VT_GROUP:{ it->var->group.expanded = !it->var->group.expanded; }break; - case VT_SCALE_BAR:{ scale_bar_interaction(ui, &it->var->scale_bar, mouse); }break; - case VT_CYCLER:{ - *it->var->cycler.state += ui_cycler_delta_for_frame(); - *it->var->cycler.state %= it->var->cycler.cycle_length; - }break; - InvalidDefaultCase; - } - }break; - case InteractionKind_AcquisitionMenu: - case InteractionKind_Menu: + if (node->kind == BeamformerPanelKind_FrameViewLive || + node->kind == BeamformerPanelKind_FrameViewCopy || + node->kind == BeamformerPanelKind_FrameViewXPlane) { - assert(it->var->type == VT_GROUP); - VariableGroup *g = &it->var->group; - if (g->container) { - ui_widget_bring_to_front(&ui->floating_widget_sentinal, g->container); - } else { - if (it->kind == InteractionKind_AcquisitionMenu) { - for (Variable *var = g->first; var;) { - Variable *next = var->next; - var->next = var->parent = 0; - ui_variable_free(ui, var); - var = next; - } - g->first = g->last = 0; - - BeamformerLiveImagingParameters *lip = &ui->shared_memory->live_imaging_parameters; - u64 enabled_kinds = atomic_load_u64(&lip->acquisition_kind_enabled_flags); - for EachBit(enabled_kinds, kind) { - Variable *button = add_variable(ui, it->var, &ui->arena, beamformer_acquisition_kind_strings[kind], - V_INPUT, VT_UI_BUTTON, ui->small_font); - button->button = UI_BID_ACQUISITION_KIND_FIRST + kind; - } - } - g->container = add_floating_view(ui, &ui->arena, VT_UI_MENU, mouse, it->var, 1); - } - }break; - case InteractionKind_Ruler:{ - assert(it->var->type == VT_BEAMFORMER_FRAME_VIEW); - ((BeamformerFrameView *)it->var->generic)->ruler.state = RulerState_None; - }break; - case InteractionKind_Button:{ ui_button_interaction(ui, it->var); }break; - case InteractionKind_Scroll:{ scroll_interaction(it->var, GetMouseWheelMoveV().y); }break; - case InteractionKind_Text:{ ui_view_close(ui, ui->text_input_state.container); }break; - InvalidDefaultCase; + BeamformerFrameView *bv = node->u.frame_view; + beamformer_ui_frame_view_release_subresources(bv, bv->kind); + DLLRemove(0, ui->view_first, ui->view_last, bv, next, prev); + SLLStackPush(ui->view_freelist, bv, next); } - if (flags & V_CAUSES_COMPUTE) - ui->flush_params = 1; + beamformer_ui_panel_unlink(node); - if (flags & V_UPDATE_VIEW) { - BeamformerFrameView *frame = parent->generic; - /* TODO(rnp): more straight forward way of achieving this */ - if (parent->type != VT_BEAMFORMER_FRAME_VIEW) { - assert(parent->parent->type == VT_UI_VIEW); - assert(parent->parent->view.child->type == VT_BEAMFORMER_FRAME_VIEW); - frame = parent->parent->view.child->generic; - } - frame->dirty = 1; - } + if (node->kind == BeamformerPanelKind_TabGroup) { + assert(parent->kind == BeamformerPanelKind_Split); - if (flags & V_LIVE_CONTROL) - ui_live_control_update(ui, it->var->parent); + BeamformerUIPanel *old_child = parent->first_child; + parent->kind = old_child->kind; + parent->first_child = old_child->first_child; + parent->last_child = old_child->last_child; + parent->child_count = old_child->child_count; + memory_copy(&parent->u, &old_child->u, sizeof(parent->u)); - if (flags & V_HIDES_CURSOR) - EnableCursor(); + for (BeamformerUIPanel *child = parent->first_child; child; child = child->next_sibling) + child->parent = parent; - if (flags & V_EXTRA_ACTION) - ui_extra_actions(ui, it->var); + SLLStackPush(ui->tree_node_freelist, old_child, next_sibling); + } - ui->interaction = (Interaction){.kind = InteractionKind_None}; + SLLStackPush(ui->tree_node_freelist, node, next_sibling); } -function void -ui_sticky_interaction_check_end(BeamformerUI *ui, v2 mouse) +function BeamformerUIPanel * +beamformer_ui_push_panel_node(BeamformerUIPanel *parent) { - Interaction *it = &ui->interaction; - switch (it->kind) { - case InteractionKind_Ruler:{ - if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) || !point_in_rect(mouse, it->rect)) - ui_end_interact(ui, mouse); - }break; - case InteractionKind_Text:{ - Interaction text_box = auto_interaction({0}, ui->text_input_state.container); - if (!interactions_equal(text_box, ui->hot_interaction)) - ui_end_interact(ui, mouse); - }break; - InvalidDefaultCase; + BeamformerUI *ui = ui_context; + BeamformerUIPanel *result = ui->tree_node_freelist; + if (result) SLLStackPop(ui->tree_node_freelist, next_sibling); + else result = push_struct_no_zero(&ui->arena, BeamformerUIPanel); + zero_struct(result); + + result->parent = parent; + if (parent) { + DLLInsertLast(0, parent->first_child, parent->last_child, result, next_sibling, previous_sibling); + parent->child_count++; } + + return result; } -function void -ui_interact(BeamformerUI *ui, BeamformerInput *input, Rect window_rect) +function BeamformerUIPanel * +beamformer_ui_push_panel(BeamformerUIPanel *parent, BeamformerPanelKind kind) { - v2 input_mouse = {{input->mouse_x, input->mouse_y}}; - Interaction *it = &ui->interaction; - if (it->kind == InteractionKind_None || interaction_is_sticky(*it)) { - ui->hot_interaction = ui->next_interaction; - - b32 mouse_left_pressed = IsMouseButtonPressed(MOUSE_BUTTON_LEFT); - b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT); - b32 wheel_moved = GetMouseWheelMoveV().y != 0; - if (mouse_right_pressed || mouse_left_pressed || wheel_moved) { - if (it->kind != InteractionKind_None) - ui_sticky_interaction_check_end(ui, input_mouse); - ui_begin_interact(ui, input_mouse, wheel_moved); - } - } + BeamformerUIPanel *result = beamformer_ui_push_panel_node(parent); + result->kind = kind; + if (parent && parent->kind == BeamformerPanelKind_TabGroup) + parent->u.tab_focus = result; - switch (it->kind) { - case InteractionKind_Nop:{ it->kind = InteractionKind_None; }break; - case InteractionKind_None:{}break; - case InteractionKind_Text:{ - if (update_text_input(&ui->text_input_state, it->var)) - ui_end_interact(ui, input_mouse); - }break; - case InteractionKind_Ruler:{ - assert(it->var->type == VT_BEAMFORMER_FRAME_VIEW); - BeamformerFrameView *bv = it->var->generic; - v2 mouse = clamp_v2_rect(input_mouse, it->rect); - bv->ruler.end = world_point_from_plane_uv(bv->frame.voxel_transform, rect_uv(mouse, it->rect)); - }break; - case InteractionKind_Drag:{ - if (!IsMouseButtonDown(MOUSE_BUTTON_LEFT) && !IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) { - ui_end_interact(ui, input_mouse); - } else { - v2 ws = window_rect.size; - v2 dMouse = v2_sub(input_mouse, ui->last_mouse); - - switch (it->var->type) { - case VT_BEAMFORMER_VARIABLE:{ - BeamformerVariable *bv = &it->var->beamformer_variable; - /* TODO(rnp): vertical sliders? */ - f32 mouse_frac = CLAMP01((input_mouse.x - it->rect.pos.x) / it->rect.size.w); - *bv->store = bv->limits.x + mouse_frac * (bv->limits.y - bv->limits.x); - }break; - case VT_X_PLANE_SHIFT:{ - assert(it->var->parent && it->var->parent->type == VT_BEAMFORMER_FRAME_VIEW); - v2 mouse = clamp_v2_rect(input_mouse, it->rect); - XPlaneShift *xp = &it->var->x_plane_shift; - ray mouse_ray = ray_for_x_plane_view(ui, it->var->parent->generic, - normalized_p_in_rect(it->rect, mouse, 0)); - /* NOTE(rnp): project start point onto ray */ - v3 s = v3_sub(xp->start_point, mouse_ray.origin); - v3 r = v3_sub(mouse_ray.direction, mouse_ray.origin); - f32 scale = v3_dot(s, r) / v3_magnitude_squared(r); - xp->end_point = v3_add(mouse_ray.origin, v3_scale(r, scale)); - }break; - case VT_BEAMFORMER_FRAME_VIEW:{ - BeamformerFrameView *bv = it->var->generic; - switch (bv->kind) { - case BeamformerFrameViewKind_3DXPlane:{ - bv->rotation -= dMouse.x / ws.w; - if (bv->rotation > 1.0f) bv->rotation -= 1.0f; - if (bv->rotation < 0.0f) bv->rotation += 1.0f; - }break; - InvalidDefaultCase; - } - }break; - case VT_UI_MENU:{ - v2 *pos = &ui->interaction.var->view.rect.pos; - *pos = clamp_v2_rect(v2_add(*pos, dMouse), window_rect); - }break; - case VT_UI_REGION_SPLIT:{ - f32 min_fraction = 0; - dMouse = v2_mul(dMouse, (v2){{1.0f / ws.w, 1.0f / ws.h}}); - RegionSplit *rs = &ui->interaction.var->region_split; - switch (rs->direction) { - case RSD_VERTICAL: { - min_fraction = (UI_SPLIT_HANDLE_THICK + 0.5f * UI_REGION_PAD) / ws.h; - rs->fraction += dMouse.y; - } break; - case RSD_HORIZONTAL: { - min_fraction = (UI_SPLIT_HANDLE_THICK + 0.5f * UI_REGION_PAD) / ws.w; - rs->fraction += dMouse.x; - } break; - } - rs->fraction = CLAMP(rs->fraction, min_fraction, 1 - min_fraction); - }break; - default:{}break; - } - if (it->var->flags & V_LIVE_CONTROL) - ui_live_control_update(ui, it->var->parent); - } - } break; - default:{ ui_end_interact(ui, input_mouse); }break; + if (kind == BeamformerPanelKind_FrameViewLive || + kind == BeamformerPanelKind_FrameViewCopy || + kind == BeamformerPanelKind_FrameViewXPlane) + { + BeamformerFrameViewKind view_kind = BeamformerFrameViewKind_Latest; + if (kind == BeamformerPanelKind_FrameViewCopy) + view_kind = BeamformerFrameViewKind_Copy; + if (kind == BeamformerPanelKind_FrameViewXPlane) + view_kind = BeamformerFrameViewKind_3DXPlane; + result->u.frame_view = beamformer_ui_frame_view_new(view_kind); } - ui->next_interaction = (Interaction){.kind = InteractionKind_None}; + return result; } /* NOTE(rnp): this only exists to make asan less annoying. do not waste @@ -4102,42 +4974,45 @@ DEBUG_EXPORT BEAMFORMER_DEBUG_UI_DEINIT_FN(beamformer_debug_ui_deinit) function void ui_init(BeamformerCtx *ctx, Arena store) { - BeamformerUI *ui = ctx->ui; + BeamformerUI *ui = ui_context = ctx->ui; if (!ui) { - ui = ctx->ui = push_struct(&store, typeof(*ui)); + ui = ui_context = ctx->ui = push_struct(&store, typeof(*ui)); ui->arena = store; - ui->shared_memory = ctx->shared_memory; - ui->beamformer_context = ctx; + + for EachElement(ui->build_arenas, it) { + ui->build_arenas[it] = sub_arena(&ui->arena, KB(128), KB(4)); + ui->build_arena_savepoints[it] = begin_temp_arena(ui->build_arenas + it); + } + ui->node_freelist = &ui_node_nil; /* TODO(rnp): better font, this one is jank at small sizes */ ui->font = LoadFontFromMemory(".ttf", beamformer_base_font, sizeof(beamformer_base_font), 28, 0, 0); ui->small_font = LoadFontFromMemory(".ttf", beamformer_base_font, sizeof(beamformer_base_font), 20, 0, 0); - ui->floating_widget_sentinal.parent = &ui->floating_widget_sentinal; - ui->floating_widget_sentinal.next = &ui->floating_widget_sentinal; - - Variable *split = ui->regions = add_ui_split(ui, 0, &ui->arena, s8("UI Root"), 0.36f, - RSD_HORIZONTAL, ui->font); - split->region_split.left = add_ui_split(ui, split, &ui->arena, s8(""), 0.475f, - RSD_VERTICAL, ui->font); - - split = split->region_split.right = add_ui_split(ui, split, &ui->arena, s8(""), 0.70f, - RSD_HORIZONTAL, ui->font); + // NOTE(rnp): push default UI layout + // TODO(rnp): load last layout from file and only load default if not present { - split->region_split.left = add_beamformer_frame_view(ui, split, &ui->arena, - BeamformerFrameViewKind_Latest, 0, 0); - split->region_split.right = add_live_controls_view(ui, split, &ui->arena); - } - split = split->parent; + BeamformerUIPanel *node = ui->tree = beamformer_ui_push_panel(0, BeamformerPanelKind_Split); + node->u.split.fraction = 0.35f; + node->u.split.axis = Axis2_X; - split = split->region_split.left; - split->region_split.left = add_beamformer_parameters_view(split, ctx); - split->region_split.right = add_ui_split(ui, split, &ui->arena, s8(""), 0.22f, - RSD_VERTICAL, ui->font); - split = split->region_split.right; + DeferLoop(node = beamformer_ui_push_panel(node, BeamformerPanelKind_Split), node = node->parent) + { + node->u.split.fraction = 0.65f; + node->u.split.axis = Axis2_Y; + + BeamformerUIPanel *left = beamformer_ui_push_panel(node, BeamformerPanelKind_TabGroup); + BeamformerUIPanel *right = beamformer_ui_push_panel(node, BeamformerPanelKind_TabGroup); + beamformer_ui_push_panel(left, BeamformerPanelKind_ParameterListing); + beamformer_ui_push_panel(right, BeamformerPanelKind_ComputeBarGraph); + beamformer_ui_push_panel(right, BeamformerPanelKind_ComputeStats); + } - split->region_split.left = add_compute_progress_bar(split, ctx); - split->region_split.right = add_compute_stats_view(ui, split, &ui->arena, ctx); + DeferLoop(node = beamformer_ui_push_panel(node, BeamformerPanelKind_TabGroup), node = node->parent) + { + beamformer_ui_push_panel(node, BeamformerPanelKind_FrameViewLive); + } + } u32 samples = vk_gpu_info()->max_msaa_samples; vk_image_allocate(&ui->render_3d_image, FRAME_VIEW_RENDER_TARGET_SIZE, 1, samples, VulkanImageUsage_Colour, 0, 0, s8("Render Target Colour")); @@ -4253,9 +5128,6 @@ ui_init(BeamformerCtx *ctx, Arena store) vk_render_model_allocate(&rm->model, unit_cube_indices, countof(unit_cube_indices), model_size, s8("unit_cube_model")); vk_render_model_range_upload(&rm->model, unit_cube_vertices, 0, sizeof(unit_cube_vertices), 0); vk_render_model_range_upload(&rm->model, unit_cube_normals, rm->normals_offset, sizeof(unit_cube_normals), 0); - - /* NOTE(rnp): shrink variable size once this fires */ - assert((uz)(ui->arena.beg - (u8 *)ui) < KB(64)); } for EachElement(beamformer_reloadable_render_shader_info_indices, it) { @@ -4266,34 +5138,21 @@ ui_init(BeamformerCtx *ctx, Arena store) } function void -validate_ui_parameters(BeamformerUI *ui) -{ - if (ui->min_coordinate.x > ui->max_coordinate.x) - swap(ui->min_coordinate.x, ui->max_coordinate.x); - if (ui->min_coordinate.y > ui->max_coordinate.y) - swap(ui->min_coordinate.y, ui->max_coordinate.y); -} - -function void -draw_ui(BeamformerCtx *ctx, BeamformerInput *input, BeamformerFrame *frame_to_draw, BeamformerViewPlaneTag frame_plane) +beamformer_ui_frame(void) { - BeamformerUI *ui = ctx->ui; + BeamformerUI *ui = ui_context = beamformer_context->ui; - if (frame_to_draw) { - mem_copy(ui->latest_plane + BeamformerViewPlaneTag_Count, frame_to_draw, sizeof(*frame_to_draw)); - mem_copy(ui->latest_plane + frame_plane, frame_to_draw, sizeof(*frame_to_draw)); - ui->latest_plane_valid[BeamformerViewPlaneTag_Count] = 1; - ui->latest_plane_valid[frame_plane] = 1; - } else { - ui->latest_plane_valid[BeamformerViewPlaneTag_Count] = 0; - ui->latest_plane_valid[frame_plane] = 0; + { + BeamformerFrame *frame = beamformer_frame_from_index(beamformer_registers()->frame); + memory_copy(ui->latest_plane + frame->view_plane_tag, frame, sizeof(*frame)); } + BeamformerInput *input = beamformer_input; for EachIndex(input->event_count, it) { if (input->event_queue[it].kind == BeamformerInputEventKind_WindowResize) { // TODO(rnp): match window against window list - ctx->window_size.w = input->event_queue[it].window_resize.width; - ctx->window_size.h = input->event_queue[it].window_resize.height; + beamformer_context->window_size.w = input->event_queue[it].window_resize.width; + beamformer_context->window_size.h = input->event_queue[it].window_resize.height; } } @@ -4301,122 +5160,286 @@ draw_ui(BeamformerCtx *ctx, BeamformerInput *input, BeamformerFrame *frame_to_dr u32 selected_block = ui->selected_parameter_block % BeamformerMaxParameterBlocks; u32 selected_mask = 1 << selected_block; - if (ctx->ui_dirty_parameter_blocks & selected_mask) { - BeamformerParameterBlock *pb = beamformer_parameter_block_lock(ui->shared_memory, selected_block, 0); + if (beamformer_context->ui_dirty_parameter_blocks & selected_mask) { + BeamformerParameterBlock *pb = beamformer_parameter_block_lock(beamformer_context->shared_memory, selected_block, 0); if (pb) { - ui->flush_params = 0; + ui->flush_parameters = 0; m4 das_transform; - mem_copy(&ui->params, &pb->parameters_ui, sizeof(ui->params)); - mem_copy(das_transform.E, pb->parameters.das_voxel_transform.E, sizeof(das_transform)); + memory_copy(&ui->parameters, &pb->parameters_ui, sizeof(ui->parameters)); + memory_copy(das_transform.E, pb->parameters.das_voxel_transform.E, sizeof(das_transform)); - atomic_and_u32(&ctx->ui_dirty_parameter_blocks, ~selected_mask); - beamformer_parameter_block_unlock(ui->shared_memory, selected_block); + atomic_and_u32(&beamformer_context->ui_dirty_parameter_blocks, ~selected_mask); + beamformer_parameter_block_unlock(beamformer_context->shared_memory, selected_block); - BeamformerComputePlan *cp = ui->beamformer_context->compute_context.compute_plans[selected_block]; + BeamformerComputePlan *cp = beamformer_context->compute_context.compute_plans[selected_block]; m4 identity = m4_identity(); b32 recompute = !m4_equal(identity, cp->ui_voxel_transform); - mem_copy(cp->ui_voxel_transform.E, identity.E, sizeof(identity)); + memory_copy(cp->ui_voxel_transform.E, identity.E, sizeof(identity)); if (recompute) { - mark_parameter_block_region_dirty(ui->shared_memory, selected_block, + mark_parameter_block_region_dirty(beamformer_context->shared_memory, selected_block, BeamformerParameterBlockRegion_Parameters); - beamformer_queue_compute(ctx, frame_to_draw, selected_block); + beamformer_queue_compute(beamformer_context, + beamformer_frame_from_index(beamformer_registers()->frame), + selected_block); } - v3 U = v3_normalize(das_transform.c[0].xyz); - v3 V = v3_normalize(das_transform.c[1].xyz); - v3 N = cross(V, U); - - ui->off_axis_position = v3_dot(N, das_transform.c[3].xyz); + ui->off_axis_position = plane_offset_from_transform(das_transform); ui->beamform_plane = 0; + } + } - v3 min_coordinate = m4_mul_v3(das_transform, (v3){{0.0f, 0.0f, 0.0f}}); - v3 max_coordinate = m4_mul_v3(das_transform, (v3){{1.0f, 1.0f, 1.0f}}); + /* NOTE: process interactions first because the user interacted with + * the ui that was presented last frame */ + Rect window_rect = {.size = {{(f32)beamformer_context->window_size.w, (f32)beamformer_context->window_size.h}}}; - ui->min_coordinate.x = v3_dot(U, min_coordinate); - ui->min_coordinate.y = v3_dot(V, min_coordinate); + ui->last_mouse = ui->current_mouse; + ui->current_mouse.x = input->mouse_x; + ui->current_mouse.y = input->mouse_y; + for EachElement(ui->input_consumed, it) + ui->input_consumed[it] = 0; - ui->max_coordinate.x = v3_dot(U, max_coordinate); - ui->max_coordinate.y = v3_dot(V, max_coordinate); + if (ui->flush_parameters && beamformer_frame_valid(beamformer_registers()->frame)) { + BeamformerParameterBlock *pb = beamformer_parameter_block_lock(beamformer_context->shared_memory, selected_block, 0); + if (pb) { + ui->flush_parameters = 0; + memory_copy(&pb->parameters_ui, &ui->parameters, sizeof(ui->parameters)); + mark_parameter_block_region_dirty(beamformer_context->shared_memory, selected_block, + BeamformerParameterBlockRegion_Parameters); + beamformer_parameter_block_unlock(beamformer_context->shared_memory, selected_block); + beamformer_queue_compute(beamformer_context, + beamformer_frame_from_index(beamformer_registers()->frame), + selected_block); } } - /* NOTE: process interactions first because the user interacted with - * the ui that was presented last frame */ - Rect window_rect = {.size = {{(f32)ctx->window_size.w, (f32)ctx->window_size.h}}}; - ui_interact(ui, input, window_rect); + /* NOTE(rnp): can't render to a different framebuffer in the middle of BeginDrawing()... */ + update_frame_views(ui, window_rect); + + //////////////////////////// + // NOTE(rnp): Text Input + { + UITextInputState *tis = &ui->text_input_state; + // NOTE(rnp): transition to new node + tis->last_node_key = ui_node_key_zero(); + tis->last_count = 0; + if (tis->changed) { + tis->changed = 0; + ui_text_input_end(); + if (!ui_node_key_nil(tis->next_node_key)) { + tis->node_key = tis->next_node_key; + tis->next_node_key = ui_node_key_zero(); + if (point_in_rect(ui->current_mouse, ui_text_input_rect())) + tis->cursor = tis->mark = ui_text_input_index_from_point(ui->last_mouse.x); + tis->blinker.t = 1.0f; + } + } - if (ui->flush_params) { - validate_ui_parameters(ui); - if (ctx->latest_frame) { - BeamformerParameterBlock *pb = beamformer_parameter_block_lock(ui->shared_memory, selected_block, 0); - if (pb) { - ui->flush_params = 0; + if (!ui_node_key_nil(tis->node_key)) { + beamformer_ui_blinker_update(&tis->blinker, BLINK_SPEED); - iv3 points = ctx->latest_frame->points; - i32 dimension = iv3_dimension(points); + UISignal signal = ui_signal_from_node(ui_node_from_key(tis->node_key)); - // TODO(rnp): this is immediate mode code that should be in the ui building code - m4 new_transform = m4_identity(); - switch (dimension) { - case 1:{}break; + if (signal.flags & UISignalFlag_LeftPressed) { + if (point_in_rect(ui->current_mouse, ui_text_input_rect())) + tis->cursor = tis->mark = ui_text_input_index_from_point(ui->last_mouse.x); + tis->blinker.t = 1.0f; + } - case 2:{ - v3 U = v3_normalize(pb->parameters.das_voxel_transform.c[0].xyz); - v3 V = v3_normalize(pb->parameters.das_voxel_transform.c[1].xyz); - v3 N = cross(V, U); + if (signal.flags & UISignalFlag_LeftDragging) + tis->mark = ui_text_input_index_from_point(ui->last_mouse.x); - new_transform = das_transform_2d_with_normal(N, ui->min_coordinate, ui->max_coordinate, 0); + if (signal.flags & UISignalFlag_DoubleClicked) { + // TODO(rnp): select word + } - v3 rotation_axis = cross(v3_normalize(new_transform.c[0].xyz), N); + if (signal.flags & UISignalFlag_TripleClicked) { + tis->cursor = 0; + tis->mark = tis->count; + } - m4 R = m4_rotation_about_axis(rotation_axis, ui->beamform_plane); - m4 T = m4_translation(v3_scale(m4_mul_v3(R, N), ui->off_axis_position)); + if (ui_text_input_update(input)) + ui_text_input_end(); + } + } - new_transform = m4_mul(m4_mul(T, m4_mul(R, new_transform)), - m4_inverse(pb->parameters.das_voxel_transform)); - }break; + if (ui->context_menu_state_changed) { + ui->context_menu_state_changed = 0; + ui->context_menu_anchor_key = ui->context_menu_next_anchor_key; + ui->context_menu_panel = ui->context_menu_next_panel; + } - case 3:{}break; - } + { + //////////////////////////// + // NOTE(rnp): Build Pass + end_temp_arena(ui->build_arena_savepoints[ui->current_frame_index % countof(ui->build_arenas)]); + // NOTE(rnp): reset last frame's build stacks + { + #define X(type, name, ...) \ + ui->name##_node_stack.top = &ui_##name##_node_nil; \ + ui->name##_node_stack.free = 0; \ + ui->name##_node_stack.count = 0; + UI_STACK_LIST + #undef X + + UIPrefWidth(ui_px(window_rect.size.x, 1.f)) + UIPrefHeight(ui_px(window_rect.size.y, 1.f)) + UIChildLayoutAxis(Axis2_Y) + ui->root_node = ui_node_from_string(0, str8("UI Root Node")); + ui_push_semantic_width(ui_pct(1.f, 0.5f)); + ui_push_semantic_height(ui_pct(1.f, 0.5f)); + } + + ui->drag_root = 0; + ui->drop_target_node = 0; + ui->drag_overlay_root = 0; + ui->drag_overlay_edges_root = 0; + ui->drag_overlay_tab_root = 0; + + beamformer_registers()->split_left_tree = 0; + beamformer_registers()->split_right_tree = 0; + beamformer_registers()->drop_child_index = 0; + + // NOTE(rnp): check for active nodes + { + b32 active = 0; + for EachEnumValue(UIMouseButtonKind, k) + active |= !ui_node_key_equal(ui->active_node_key[k], ui_node_key_zero()); + // NOTE(rnp): clear hot node if there are no active nodes + if (!active) ui->hot_node_key = ui_node_key_zero(); + } + + // NOTE(rnp): context menu + if (!ui_node_key_nil(ui->context_menu_anchor_key)) { + // TODO(rnp): context_menu_open_t + UIPrefWidth(ui_children_sum(1.f)) + UIPrefHeight(ui_children_sum(1.f)) + UIChildLayoutAxis(Axis2_Y) + UIBGColour((v4){.a = 0.8f}) + { + // TODO(rnp): this should be tied to the window state + ui->context_menu_root = ui_node_from_string(UINodeFlag_DrawBackground, str8("context_menu_root")); + } + + UIParent(ui->context_menu_root) + { + ui_build_context_menu_for_panel(ui->context_menu_panel); + } + } + + // NOTE(rnp): drag panel + if (ui->drag_panel) { + ui_build_drag_overlay(window_rect); + + UIPrefWidth(ui_px(640.f, 1.f)) + UIPrefHeight(ui_px(480.f, 1.f)) + UIChildLayoutAxis(Axis2_Y) + UIBGColour((v4){.a = 0.8f}) + { + ui->drag_root = ui_node_from_string(UINodeFlag_DrawBackground, str8("drag_panel_root")); + } - // TODO(rnp): super janky code because of the retained mode parameters list. - // when this code is run in the correct place we can just decide inline - b32 recompute = !memory_equal(&pb->parameters_ui, &ui->params, sizeof(ui->params)); - BeamformerComputePlan *cp = ui->beamformer_context->compute_context.compute_plans[selected_block]; - if (cp) { - recompute |= !m4_equal(new_transform, cp->ui_voxel_transform); - mem_copy(cp->ui_voxel_transform.E, new_transform.E, sizeof(new_transform)); + UIParent(ui->drag_root) + { + UIChildLayoutAxis(Axis2_X) + UIPrefHeight(ui_children_sum(1.f)) + UIPrefWidth(ui_children_sum(1.f)) + UIParent(ui_spacer(0)) + { + ui_padw(UI_NODE_PAD); + ui_panel_label(ui->drag_panel); } + } - mem_copy(&pb->parameters_ui, &ui->params, sizeof(ui->params)); + ui_build_regions(ui->drag_root, ui->drag_panel); + } - mark_parameter_block_region_dirty(ui->shared_memory, selected_block, - BeamformerParameterBlockRegion_Parameters); - beamformer_parameter_block_unlock(ui->shared_memory, selected_block); + ui_build_regions(ui->root_node, ui->tree); + + //////////////////////////// + // NOTE(rnp): Prune Dead UI Nodes + for EachElement(ui->node_hash_table, it) { + UINodeHashBucket *hb = ui->node_hash_table + it; + UINode *next = hb->first; + for (UINode *b = next; !ui_node_is_nil(b); b = next) { + next = b == b->hash_next ? 0 : b->hash_next; + if (b->last_frame_active_index != ui->current_frame_index) { + for EachEnumValue(UIMouseButtonKind, k) + if (ui_node_key_equal(ui->active_node_key[k], b->key)) + ui->active_node_key[k] = ui_node_key_zero(); + + DLLRemove(&ui_node_nil, hb->first, hb->last, b, hash_next, hash_prev); + SLLStackPush(ui->node_freelist, b, next_sibling); + } + } + } - if (recompute) - beamformer_queue_compute(ctx, frame_to_draw, selected_block); + for (BeamformerInputEvent *event = ui_event_next(input, 0); + event; + event = ui_event_next(input, event)) + { + if (event->kind == BeamformerInputEventKind_ButtonPress) { + if (event->button_id == BeamformerButtonID_Escape) + beamformer_context->state = BeamformerState_ShouldClose; + + if (!Between(event->button_id, BeamformerButtonID_ModifierFirst, BeamformerButtonID_ModifierLast)) { + ui_context_menu_close(); + ui->text_input_state.changed = 1; + ui->text_input_state.next_node_key = ui_node_key_zero(); + } } } - } - /* NOTE(rnp): can't render to a different framebuffer in the middle of BeginDrawing()... */ - update_frame_views(ui, window_rect); + //////////////////////////// + // NOTE(rnp): Layout Pass + if (ui->drag_root) { + ui->drag_root->computed_position[Axis2_X] = ui->last_mouse.x; + ui->drag_root->computed_position[Axis2_Y] = ui->last_mouse.y; + ui_layout_nodes(ui->drag_root); + ui_layout_nodes(ui->drag_overlay_edges_root); + if (ui->drag_overlay_tab_root) + ui_layout_nodes(ui->drag_overlay_tab_root); + ui_layout_nodes(ui->drag_overlay_root); + } + + ui_layout_nodes(ui->root_node); + + if (!ui_node_key_nil(ui->context_menu_anchor_key)) { + UINode *anchor = ui_node_from_key(ui->context_menu_anchor_key); + v2 anchor_p = ui_node_final_position(anchor); + ui->context_menu_root->computed_position[Axis2_X] = anchor_p.x; + ui->context_menu_root->computed_position[Axis2_Y] = anchor_p.y + anchor->computed_size[Axis2_Y]; - BeginDrawing(); - v2 mouse = {{input->mouse_x, input->mouse_y}}; - glClearNamedFramebufferfv(0, GL_COLOR, 0, BG_COLOUR.E); - glClearNamedFramebufferfv(0, GL_DEPTH, 0, (f32 []){1}); + ui_layout_nodes(ui->context_menu_root); + } + + BeginDrawing(); + glClearNamedFramebufferfv(0, GL_COLOR, 0, BG_COLOUR.E); + glClearNamedFramebufferfv(0, GL_DEPTH, 0, (f32 []){1}); + ui_draw_nodes(ui->root_node, window_rect); + + if (!ui_node_key_nil(ui->context_menu_anchor_key)) + ui_draw_nodes(ui->context_menu_root, window_rect); + + if (ui->drag_root) { + if (beamformer_registers()->split_left_tree || beamformer_registers()->split_right_tree) + ui_draw_nodes(ui_build_drag_hover_node(), window_rect); + ui_draw_nodes(ui->drag_overlay_root, window_rect); + ui_draw_nodes(ui->drag_overlay_edges_root, window_rect); + if (ui->drag_overlay_tab_root) + ui_draw_nodes(ui->drag_overlay_tab_root, window_rect); + ui_draw_nodes(ui->drag_root, window_rect); + } - draw_ui_regions(ui, window_rect, mouse); - draw_floating_widgets(ui, window_rect, mouse); + // TODO(rnp): hack: until raylib is removed this happens in ui since raylib will cause + // glfw to call the input callbacks during EndDrawing() + input->event_count = 0; + EndDrawing(); - // TODO(rnp): hack: until raylib is removed this happens in ui since raylib will cause - // glfw to call the input callbacks during EndDrawing() - input->event_count = 0; - EndDrawing(); + if (ui->drag_end) + ui_drag_end(); - ui->last_mouse = (v2){{input->mouse_x, input->mouse_y}}; + ui->current_frame_index++; + } } diff --git a/util.c b/util.c @@ -536,16 +536,6 @@ stream_append_f64_e(Stream *s, f64 f) stream_append_u64(s, (u64)Abs(scale)); } -function void -stream_append_v2(Stream *s, v2 v) -{ - stream_append_byte(s, '{'); - stream_append_f64(s, v.x, 100); - stream_append_s8(s, s8(", ")); - stream_append_f64(s, v.y, 100); - stream_append_byte(s, '}'); -} - function Stream arena_stream(Arena a) { @@ -865,16 +855,6 @@ push_str8_fv(Arena *arena, const char *format, va_list args) return result; } -function str8 -push_f64_string(Arena *arena, f64 value, u64 precision) -{ - Stream sb = arena_stream(*arena); - stream_append_f64(&sb, value, precision); - s8 s = arena_stream_commit(arena, &sb); - str8 result = {.length = s.len, .data = s.data}; - return result; -} - /* NOTE(rnp): from Hacker's Delight */ function force_inline u64 round_down_power_of_two(u64 a) @@ -899,68 +879,6 @@ round_up_to(iz value, iz multiple) return result; } -function void -split_rect_horizontal(Rect rect, f32 fraction, Rect *left, Rect *right) -{ - if (left) { - left->pos = rect.pos; - left->size.h = rect.size.h; - left->size.w = rect.size.w * fraction; - } - if (right) { - right->pos = rect.pos; - right->pos.x += rect.size.w * fraction; - right->size.h = rect.size.h; - right->size.w = rect.size.w * (1.0f - fraction); - } -} - -function void -split_rect_vertical(Rect rect, f32 fraction, Rect *top, Rect *bot) -{ - if (top) { - top->pos = rect.pos; - top->size.w = rect.size.w; - top->size.h = rect.size.h * fraction; - } - if (bot) { - bot->pos = rect.pos; - bot->pos.y += rect.size.h * fraction; - bot->size.w = rect.size.w; - bot->size.h = rect.size.h * (1.0f - fraction); - } -} - -function void -cut_rect_horizontal(Rect rect, f32 at, Rect *left, Rect *right) -{ - at = MIN(at, rect.size.w); - if (left) { - *left = rect; - left->size.w = at; - } - if (right) { - *right = rect; - right->pos.x += at; - right->size.w -= at; - } -} - -function void -cut_rect_vertical(Rect rect, f32 at, Rect *top, Rect *bot) -{ - at = MIN(at, rect.size.h); - if (top) { - *top = rect; - top->size.h = at; - } - if (bot) { - *bot = rect; - bot->pos.y += at; - bot->size.h -= at; - } -} - function NumberConversion integer_from_str8(str8 raw) { diff --git a/util.h b/util.h @@ -109,12 +109,9 @@ typedef u64 uptr; #define countof(a) (iz)(sizeof(a) / sizeof(*a)) #define BETWEEN(x, a, b) ((x) >= (a) && (x) <= (b)) #define CLAMP(x, a, b) ((x) < (a) ? (a) : (x) > (b) ? (b) : (x)) -#define CLAMP01(x) CLAMP(x, 0, 1) #define ISPOWEROF2(a) (((a) & ((a) - 1)) == 0) #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) -#define ORONE(x) ((x)? (x) : 1) -#define SIGN(x) ((x) < 0? -1 : 1) #define swap(a, b) do {typeof(a) __tmp = (a); (a) = (b); (b) = __tmp;} while(0) #define Abs(a) ((a) < 0 ? -(a) : (a)) @@ -205,6 +202,13 @@ typedef u64 uptr; #include "intrinsics.c" +typedef enum { + Axis2_X = 0, + Axis2_Y = 1, + Axis2_Count, +} Axis2; +#define axis2_flip(v) (!(v)) + typedef alignas(16) union { u8 U8[16]; u16 U16[8]; diff --git a/util_os_ui.c b/util_os_ui.c @@ -10,6 +10,10 @@ void *GetPlatformWindowHandle(void); typedef void glfw_window_resize_fn(void *window, i32 width, i32 height); glfw_window_resize_fn *glfwSetWindowSizeCallback(void *window, glfw_window_resize_fn *callback); +void *glfwSetCharCallback(void *window, void (*callback)(void *window, u32 codepoint)); +void *glfwSetKeyCallback(void *window, void (*callback)(void *window, i32 key, i32 scancode, i32 action, i32 mods)); +void *glfwSetMouseButtonCallback(void *window, void (*callback)(void *window, i32 button, i32 action, i32 mods)); +void *glfwSetScrollCallback(void *window, void (*callback)(void *window, f64 x_delta, f64 y_delta)); global BeamformerInput *beamformer_input; global glfw_window_resize_fn *raylib_window_resize; @@ -22,10 +26,197 @@ os_push_input_event(BeamformerInput *input, BeamformerInputEvent event) input->event_queue[input->event_count++] = event; } +function BeamformerInputModifiers +os_modifiers_from_glfw(i32 modifiers) +{ + BeamformerInputModifiers result = 0; + result |= ((modifiers & 0x1) != 0) * BeamformerInputModifier_Shift; + result |= ((modifiers & 0x2) != 0) * BeamformerInputModifier_Control; + result |= ((modifiers & 0x4) != 0) * BeamformerInputModifier_Alt; + result |= ((modifiers & 0x8) != 0) * BeamformerInputModifier_Meta; + return result; +} + +function void +os_scroll_callback(void *window, f64 xoff, f64 yoff) +{ + // TODO(rnp): GLFW doesn't pass modifiers through but the os event would have them + os_push_input_event(beamformer_input, (BeamformerInputEvent){ + .kind = BeamformerInputEventKind_MouseScroll, + .scroll = {.x = xoff, .y = yoff}, + }); +} + +function void +os_key_callback(void *window, i32 key, i32 scancode, i32 action, i32 modifiers) +{ + BeamformerInputEvent input_event = { + .kind = action == 0 ? BeamformerInputEventKind_ButtonRelease : BeamformerInputEventKind_ButtonPress, + .modifiers = os_modifiers_from_glfw(modifiers), + }; + + switch (key) { + default:{}break; + case BeamformerButtonID_Space: + case BeamformerButtonID_Apostrophe: + case BeamformerButtonID_Comma: + case BeamformerButtonID_Minus: + case BeamformerButtonID_Period: + case BeamformerButtonID_Slash: + case BeamformerButtonID_0: + case BeamformerButtonID_1: + case BeamformerButtonID_2: + case BeamformerButtonID_3: + case BeamformerButtonID_4: + case BeamformerButtonID_5: + case BeamformerButtonID_6: + case BeamformerButtonID_7: + case BeamformerButtonID_8: + case BeamformerButtonID_9: + case BeamformerButtonID_Semicolon: + case BeamformerButtonID_Equal: + case BeamformerButtonID_A: + case BeamformerButtonID_B: + case BeamformerButtonID_C: + case BeamformerButtonID_D: + case BeamformerButtonID_E: + case BeamformerButtonID_F: + case BeamformerButtonID_G: + case BeamformerButtonID_H: + case BeamformerButtonID_I: + case BeamformerButtonID_J: + case BeamformerButtonID_K: + case BeamformerButtonID_L: + case BeamformerButtonID_M: + case BeamformerButtonID_N: + case BeamformerButtonID_O: + case BeamformerButtonID_P: + case BeamformerButtonID_Q: + case BeamformerButtonID_R: + case BeamformerButtonID_S: + case BeamformerButtonID_T: + case BeamformerButtonID_U: + case BeamformerButtonID_V: + case BeamformerButtonID_W: + case BeamformerButtonID_X: + case BeamformerButtonID_Y: + case BeamformerButtonID_Z: + case BeamformerButtonID_LeftBracket: + case BeamformerButtonID_Backslash: + case BeamformerButtonID_RightBracket: + case BeamformerButtonID_Grave: + { + input_event.button_id = key; + input_event.codepoint = key; + if (Between(key, BeamformerButtonID_A, BeamformerButtonID_Z) && + ((input_event.modifiers & BeamformerInputModifier_Shift) == 0)) + { + input_event.codepoint ^= 0x20; + } + }break; + + case 256:{input_event.button_id = BeamformerButtonID_Escape; }break; + case 257:{input_event.button_id = BeamformerButtonID_Enter; }break; + case 258:{input_event.button_id = BeamformerButtonID_Tab; }break; + case 259:{input_event.button_id = BeamformerButtonID_Backspace; }break; + case 260:{input_event.button_id = BeamformerButtonID_Insert; }break; + case 261:{input_event.button_id = BeamformerButtonID_Delete; }break; + case 262:{input_event.button_id = BeamformerButtonID_Right; }break; + case 263:{input_event.button_id = BeamformerButtonID_Left; }break; + case 264:{input_event.button_id = BeamformerButtonID_Down; }break; + case 265:{input_event.button_id = BeamformerButtonID_Up; }break; + case 266:{input_event.button_id = BeamformerButtonID_PageUp; }break; + case 267:{input_event.button_id = BeamformerButtonID_PageDown; }break; + case 268:{input_event.button_id = BeamformerButtonID_Home; }break; + case 269:{input_event.button_id = BeamformerButtonID_End; }break; + case 280:{input_event.button_id = BeamformerButtonID_CapsLock; }break; + case 281:{input_event.button_id = BeamformerButtonID_ScrollLock; }break; + case 282:{input_event.button_id = BeamformerButtonID_NumLock; }break; + case 283:{input_event.button_id = BeamformerButtonID_PrintScreen; }break; + case 284:{input_event.button_id = BeamformerButtonID_Pause; }break; + case 290:{input_event.button_id = BeamformerButtonID_F1; }break; + case 291:{input_event.button_id = BeamformerButtonID_F2; }break; + case 292:{input_event.button_id = BeamformerButtonID_F3; }break; + case 293:{input_event.button_id = BeamformerButtonID_F4; }break; + case 294:{input_event.button_id = BeamformerButtonID_F5; }break; + case 295:{input_event.button_id = BeamformerButtonID_F6; }break; + case 296:{input_event.button_id = BeamformerButtonID_F7; }break; + case 297:{input_event.button_id = BeamformerButtonID_F8; }break; + case 298:{input_event.button_id = BeamformerButtonID_F9; }break; + case 299:{input_event.button_id = BeamformerButtonID_F10; }break; + case 300:{input_event.button_id = BeamformerButtonID_F11; }break; + case 301:{input_event.button_id = BeamformerButtonID_F12; }break; + case 302:{input_event.button_id = BeamformerButtonID_F13; }break; + case 303:{input_event.button_id = BeamformerButtonID_F14; }break; + case 304:{input_event.button_id = BeamformerButtonID_F15; }break; + case 305:{input_event.button_id = BeamformerButtonID_F16; }break; + case 306:{input_event.button_id = BeamformerButtonID_F17; }break; + case 307:{input_event.button_id = BeamformerButtonID_F18; }break; + case 308:{input_event.button_id = BeamformerButtonID_F19; }break; + case 309:{input_event.button_id = BeamformerButtonID_F20; }break; + case 310:{input_event.button_id = BeamformerButtonID_F21; }break; + case 311:{input_event.button_id = BeamformerButtonID_F22; }break; + case 312:{input_event.button_id = BeamformerButtonID_F23; }break; + case 313:{input_event.button_id = BeamformerButtonID_F24; }break; + case 314:{input_event.button_id = BeamformerButtonID_F25; }break; + case 320:{input_event.button_id = BeamformerButtonID_KP0; }break; + case 321:{input_event.button_id = BeamformerButtonID_KP1; }break; + case 322:{input_event.button_id = BeamformerButtonID_KP2; }break; + case 323:{input_event.button_id = BeamformerButtonID_KP3; }break; + case 324:{input_event.button_id = BeamformerButtonID_KP4; }break; + case 325:{input_event.button_id = BeamformerButtonID_KP5; }break; + case 326:{input_event.button_id = BeamformerButtonID_KP6; }break; + case 327:{input_event.button_id = BeamformerButtonID_KP7; }break; + case 328:{input_event.button_id = BeamformerButtonID_KP8; }break; + case 329:{input_event.button_id = BeamformerButtonID_KP9; }break; + case 330:{input_event.button_id = BeamformerButtonID_KPDecimal; }break; + case 331:{input_event.button_id = BeamformerButtonID_KPDivide; }break; + case 332:{input_event.button_id = BeamformerButtonID_KPMultiply; }break; + case 333:{input_event.button_id = BeamformerButtonID_KPSubtract; }break; + case 334:{input_event.button_id = BeamformerButtonID_KPAdd; }break; + case 335:{input_event.button_id = BeamformerButtonID_KPEnter; }break; + case 336:{input_event.button_id = BeamformerButtonID_KPEqual; }break; + case 340:{input_event.button_id = BeamformerButtonID_LeftShift; }break; + case 341:{input_event.button_id = BeamformerButtonID_LeftControl; }break; + case 342:{input_event.button_id = BeamformerButtonID_LeftAlt; }break; + case 343:{input_event.button_id = BeamformerButtonID_LeftSuper; }break; + case 344:{input_event.button_id = BeamformerButtonID_RightShift; }break; + case 345:{input_event.button_id = BeamformerButtonID_RightControl;}break; + case 346:{input_event.button_id = BeamformerButtonID_RightAlt; }break; + case 347:{input_event.button_id = BeamformerButtonID_RightSuper; }break; + case 348:{input_event.button_id = BeamformerButtonID_Menu; }break; + } + + os_push_input_event(beamformer_input, input_event); +} + +function void +os_mouse_button_callback(void *window, i32 button, i32 action, i32 modifiers) +{ + BeamformerButtonID button_id = BeamformerButtonID_MouseLeft; + switch (button) { + default:{}break; + case 0:{button_id = BeamformerButtonID_MouseLeft; }break; + case 1:{button_id = BeamformerButtonID_MouseRight; }break; + case 2:{button_id = BeamformerButtonID_MouseMiddle;}break; + } + + os_push_input_event(beamformer_input, (BeamformerInputEvent){ + .kind = action == 0 ? BeamformerInputEventKind_ButtonRelease : BeamformerInputEventKind_ButtonPress, + .modifiers = os_modifiers_from_glfw(modifiers), + .button_id = button_id, + }); +} + function void os_window_equip_common(BeamformerInput *input, void *window) { beamformer_input = input; + + glfwSetCharCallback(window, 0); + glfwSetKeyCallback(window, os_key_callback); + glfwSetMouseButtonCallback(window, os_mouse_button_callback); + glfwSetScrollCallback(window, os_scroll_callback); } function void @@ -35,14 +226,4 @@ os_build_frame_input(BeamformerInput *input) if (IsWindowFocused()) new_mouse = GetMousePosition(); input->mouse_x = new_mouse.x; input->mouse_y = new_mouse.y; - - input->input_modifiers = 0; - input->input_modifiers |= BeamformerInputModifier_LeftAlt * IsKeyDown(KEY_LEFT_ALT); - input->input_modifiers |= BeamformerInputModifier_RightAlt * IsKeyDown(KEY_RIGHT_ALT); - input->input_modifiers |= BeamformerInputModifier_LeftControl * IsKeyDown(KEY_LEFT_CONTROL); - input->input_modifiers |= BeamformerInputModifier_RightControl * IsKeyDown(KEY_RIGHT_CONTROL); - input->input_modifiers |= BeamformerInputModifier_LeftShift * IsKeyDown(KEY_LEFT_SHIFT); - input->input_modifiers |= BeamformerInputModifier_RightShift * IsKeyDown(KEY_RIGHT_SHIFT); - input->input_modifiers |= BeamformerInputModifier_LeftMeta * IsKeyDown(KEY_LEFT_SUPER); - input->input_modifiers |= BeamformerInputModifier_RightMeta * IsKeyDown(KEY_RIGHT_SUPER); }