ui.c (75685B)
1 /* See LICENSE for license details. */ 2 /* TODO(rnp): 3 * [ ]: scroll bar for views that don't have enough space 4 * [ ]: compute times through same path as parameter list ? 5 * [ ]: global menu 6 * [ ]: allow views to collapse to just their title bar 7 * [ ]: enforce a minimum region size or allow regions themselves to scroll 8 * [ ]: allow scale bars to collapse 9 * [ ]: move remaining fragment shader stuff into ui 10 * [ ]: refactor: draw_left_aligned_list() 11 * [ ]: refactor: draw_variable_table() 12 * [ ]: refactor: add_variable_no_link() 13 * [ ]: refactor: draw_text_limited should clamp to rect and measure text itself 14 * [ ]: refactor: draw_active_menu should just use draw_variable_list 15 * [ ]: refactor: draw_beamformer_variable -> draw_variable; draw_variable -> draw_layout_variable 16 * [ ]: refactor: scale bars should just be variables 17 * [ ]: refactor: remove scale bar limits (limits should only prevent invalid program state) 18 * [ ]: ui leaks split beamform views on hot-reload 19 * [ ]: add tag based selection to frame views 20 * [ ]: draw the ui with a post-order traversal instead of pre-order traversal 21 */ 22 23 #define BG_COLOUR (v4){.r = 0.15, .g = 0.12, .b = 0.13, .a = 1.0} 24 #define FG_COLOUR (v4){.r = 0.92, .g = 0.88, .b = 0.78, .a = 1.0} 25 #define FOCUSED_COLOUR (v4){.r = 0.86, .g = 0.28, .b = 0.21, .a = 1.0} 26 #define HOVERED_COLOUR (v4){.r = 0.11, .g = 0.50, .b = 0.59, .a = 1.0} 27 #define RULER_COLOUR (v4){.r = 1.00, .g = 0.70, .b = 0.00, .a = 1.0} 28 29 #define MENU_PLUS_COLOUR (v4){.r = 0.33, .g = 0.42, .b = 1.00, .a = 1.0} 30 #define MENU_CLOSE_COLOUR FOCUSED_COLOUR 31 32 #define NORMALIZED_FG_COLOUR colour_from_normalized(FG_COLOUR) 33 34 #define HOVER_SPEED 5.0f 35 36 #define RULER_TEXT_PAD 10.0f 37 #define RULER_TICK_LENGTH 20.0f 38 39 #define UI_SPLIT_HANDLE_THICK 8.0f 40 #define UI_REGION_PAD 32.0f 41 42 /* TODO(rnp) smooth scroll */ 43 #define UI_SCROLL_SPEED 12.0f 44 45 #define LISTING_ITEM_PAD 12.0f 46 #define LISTING_INDENT 20.0f 47 #define LISTING_LINE_PAD 6.0f 48 #define TITLE_BAR_PAD 6.0f 49 50 typedef struct { 51 u8 buf[64]; 52 i32 buf_len; 53 i32 cursor; 54 f32 cursor_blink_t; 55 f32 cursor_blink_scale; 56 Rect rect, hot_rect; 57 Font *font, *hot_font; 58 } InputState; 59 60 typedef struct { 61 v2 at; 62 Font *font, *hot_font; 63 } MenuState; 64 65 typedef enum { 66 IT_NONE, 67 IT_NOP, 68 IT_DISPLAY, 69 IT_DRAG, 70 IT_MENU, 71 IT_SCALE_BAR, 72 IT_SCROLL, 73 IT_SET, 74 IT_TEXT, 75 } InteractionType; 76 77 enum ruler_state { 78 RS_NONE, 79 RS_START, 80 RS_HOLD, 81 }; 82 83 typedef struct v2_sll { 84 struct v2_sll *next; 85 v2 v; 86 } v2_sll; 87 88 typedef struct BeamformerUI BeamformerUI; 89 typedef struct Variable Variable; 90 91 typedef enum { 92 RSD_VERTICAL, 93 RSD_HORIZONTAL, 94 } RegionSplitDirection; 95 96 typedef struct { 97 Variable *left; 98 Variable *right; 99 f32 fraction; 100 RegionSplitDirection direction; 101 } RegionSplit; 102 103 /* TODO(rnp): this should be refactored to not need a BeamformerCtx */ 104 typedef struct { 105 BeamformerCtx *ctx; 106 void *stats; 107 } ComputeStatsView; 108 109 typedef struct { 110 b32 *processing; 111 f32 *progress; 112 f32 display_t; 113 f32 display_t_velocity; 114 } ComputeProgressBar; 115 116 typedef enum { 117 VT_NULL, 118 VT_B32, 119 VT_F32, 120 VT_I32, 121 VT_GROUP, 122 VT_BEAMFORMER_VARIABLE, 123 VT_BEAMFORMER_FRAME_VIEW, 124 VT_COMPUTE_STATS_VIEW, 125 VT_COMPUTE_LATEST_STATS_VIEW, 126 VT_COMPUTE_PROGRESS_BAR, 127 VT_SCALE_BAR, 128 VT_UI_BUTTON, 129 VT_UI_VIEW, 130 VT_UI_REGION_SPLIT, 131 } VariableType; 132 133 typedef enum { 134 VG_LIST, 135 /* NOTE(rnp): special groups for vectors with components 136 * stored in separate memory locations */ 137 VG_V2, 138 VG_V4, 139 } VariableGroupType; 140 141 typedef struct { 142 Variable *first; 143 Variable *last; 144 b32 expanded; 145 f32 max_name_width; 146 VariableGroupType type; 147 } VariableGroup; 148 149 typedef enum { 150 UI_VIEW_CUSTOM_TEXT = 1 << 0, 151 } UIViewFlags; 152 153 typedef struct { 154 /* NOTE(rnp): superset of group, group must come first */ 155 VariableGroup group; 156 Variable *close; 157 Variable *menu; 158 f32 needed_height; 159 f32 offset; 160 UIViewFlags flags; 161 } UIView; 162 163 /* X(id, text) */ 164 #define FRAME_VIEW_BUTTONS \ 165 X(FV_COPY_HORIZONTAL, "Copy Horizontal") \ 166 X(FV_COPY_VERTICAL, "Copy Vertical") 167 168 #define X(id, text) UI_BID_ ##id, 169 typedef enum { 170 UI_BID_CLOSE_VIEW, 171 FRAME_VIEW_BUTTONS 172 } UIButtonID; 173 #undef X 174 175 typedef struct { 176 s8 suffix; 177 /* TODO(rnp): think of something better than this */ 178 union { 179 struct {s8 *names; u32 count;} name_table; 180 struct { 181 f32 display_scale; 182 f32 scroll_scale; 183 v2 limits; 184 } params; 185 }; 186 void *store; 187 VariableType store_type; 188 } BeamformerVariable; 189 190 typedef enum { 191 V_INPUT = 1 << 0, 192 V_TEXT = 1 << 1, 193 V_BUTTON = 1 << 2, 194 V_MENU = 1 << 3, 195 V_CAUSES_COMPUTE = 1 << 29, 196 V_UPDATE_VIEW = 1 << 30, 197 } VariableFlags; 198 199 struct Variable { 200 s8 name; 201 union { 202 void *generic; 203 BeamformerVariable beamformer_variable; 204 ComputeProgressBar compute_progress_bar; 205 ComputeStatsView compute_stats_view; 206 RegionSplit region_split; 207 UIButtonID button; 208 UIView view; 209 VariableGroup group; 210 b32 b32; 211 i32 i32; 212 f32 f32; 213 } u; 214 Variable *next; 215 Variable *parent; 216 VariableFlags flags; 217 VariableType type; 218 219 f32 hover_t; 220 f32 name_width; 221 }; 222 223 typedef enum { 224 SB_LATERAL, 225 SB_AXIAL, 226 } ScaleBarDirection; 227 228 typedef struct { 229 f32 *min_value, *max_value; 230 v2_sll *savepoint_stack; 231 v2 zoom_starting_point; 232 v2 screen_offset; 233 v2 screen_space_to_value; 234 v2 limits; 235 v2 scroll_scale; 236 f32 hover_t; 237 b32 causes_compute; 238 } ScaleBar; 239 240 typedef enum { 241 FVT_INDEXED, 242 FVT_LATEST, 243 FVT_COPY, 244 } BeamformerFrameViewType; 245 246 typedef struct BeamformerFrameView { 247 ScaleBar lateral_scale_bar; 248 ScaleBar axial_scale_bar; 249 250 v4 min_coordinate; 251 v4 max_coordinate; 252 253 Variable threshold; 254 Variable dynamic_range; 255 256 FragmentShaderCtx *ctx; 257 BeamformFrame *frame; 258 struct BeamformerFrameView *prev, *next; 259 260 RenderTexture2D rendered_view; 261 BeamformerFrameViewType type; 262 b32 needs_update; 263 } BeamformerFrameView; 264 265 typedef struct { 266 Variable *hot; 267 Variable *active; 268 InteractionType hot_type; 269 InteractionType type; 270 } InteractionState; 271 272 struct BeamformerUI { 273 Arena arena; 274 275 Font font; 276 Font small_font; 277 278 Variable *regions; 279 Variable *variable_freelist; 280 Variable *scratch_variable; 281 Variable scratch_variables[2]; 282 283 BeamformerFrameView *views; 284 BeamformerFrameView *view_freelist; 285 BeamformFrame *frame_freelist; 286 287 InteractionState interaction; 288 /* TODO(rnp): these can be combined */ 289 MenuState menu_state; 290 InputState text_input_state; 291 292 v2_sll *scale_bar_savepoint_freelist; 293 294 v2 ruler_start_p; 295 v2 ruler_stop_p; 296 u32 ruler_state; 297 298 b32 latest_frame_changed; 299 BeamformFrame *latest_frame; 300 ComputeShaderStats *latest_compute_stats; 301 302 BeamformerUIParameters params; 303 b32 flush_params; 304 305 iptr last_displayed_frame; 306 307 OS *os; 308 }; 309 310 typedef enum { 311 TF_NONE = 0, 312 TF_ROTATED = 1 << 0, 313 TF_LIMITED = 1 << 1, 314 TF_OUTLINED = 1 << 2, 315 } TextFlags; 316 317 typedef struct { 318 Font *font; 319 Rect limits; 320 Color colour; 321 Color outline_colour; 322 f32 outline_thick; 323 f32 rotation; 324 TextFlags flags; 325 } TextSpec; 326 327 static v2 328 measure_text(Font font, s8 text) 329 { 330 v2 result = {.y = font.baseSize}; 331 for (iz i = 0; i < text.len; i++) { 332 /* NOTE: assumes font glyphs are ordered ASCII */ 333 i32 idx = (i32)text.data[i] - 0x20; 334 result.x += font.glyphs[idx].advanceX; 335 if (font.glyphs[idx].advanceX == 0) 336 result.x += (font.recs[idx].width + font.glyphs[idx].offsetX); 337 } 338 return result; 339 } 340 341 static void 342 stream_append_variable_base(Stream *s, VariableType type, void *var, void *scale) 343 { 344 switch (type) { 345 case VT_B32: { 346 s8 *text = var; 347 stream_append_s8(s, text[*(b32 *)scale != 0]); 348 } break; 349 case VT_F32: { 350 f32 val = *(f32 *)var * *(f32 *)scale; 351 stream_append_f64(s, val, 100); 352 } break; 353 default: INVALID_CODE_PATH; 354 } 355 } 356 357 static void 358 stream_append_variable(Stream *s, Variable *var) 359 { 360 switch (var->type) { 361 case VT_UI_BUTTON: 362 case VT_GROUP: stream_append_s8(s, var->name); break; 363 case VT_BEAMFORMER_VARIABLE: { 364 BeamformerVariable *bv = &var->u.beamformer_variable; 365 switch (bv->store_type) { 366 case VT_B32: { 367 stream_append_variable_base(s, VT_B32, bv->name_table.names, bv->store); 368 } break; 369 case VT_F32: { 370 stream_append_variable_base(s, VT_F32, bv->store, &bv->params.display_scale); 371 } break; 372 default: INVALID_CODE_PATH; 373 } 374 } break; 375 case VT_B32: { 376 s8 texts[] = {s8("False"), s8("True")}; 377 stream_append_variable_base(s, VT_B32, texts, &var->u.b32); 378 } break; 379 case VT_F32: { 380 f32 scale = 1; 381 stream_append_variable_base(s, VT_F32, &var->u.f32, &scale); 382 } break; 383 default: INVALID_CODE_PATH; 384 } 385 } 386 387 static void 388 resize_frame_view(BeamformerFrameView *view, uv2 dim) 389 { 390 UnloadRenderTexture(view->rendered_view); 391 /* TODO(rnp): sometimes when accepting data on w32 something happens 392 * and the program will stall in vprintf in TraceLog(...) here. 393 * for now do this to avoid the problem */ 394 //SetTraceLogLevel(LOG_NONE); 395 view->rendered_view = LoadRenderTexture(dim.x, dim.y); 396 //SetTraceLogLevel(LOG_INFO); 397 398 /* TODO(rnp): add some ID for the specific view here */ 399 LABEL_GL_OBJECT(GL_FRAMEBUFFER, view->rendered_view.id, s8("Frame View")); 400 LABEL_GL_OBJECT(GL_TEXTURE, view->rendered_view.texture.id, s8("Frame View Texture")); 401 glGenerateTextureMipmap(view->rendered_view.texture.id); 402 403 //SetTextureFilter(view->rendered_view.texture, TEXTURE_FILTER_ANISOTROPIC_8X); 404 //SetTextureFilter(view->rendered_view.texture, TEXTURE_FILTER_TRILINEAR); 405 //SetTextureFilter(view->rendered_view.texture, TEXTURE_FILTER_BILINEAR); 406 407 /* NOTE(rnp): work around raylib's janky texture sampling */ 408 i32 id = view->rendered_view.texture.id; 409 glTextureParameteri(id, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); 410 glTextureParameteri(id, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); 411 glTextureParameterfv(id, GL_TEXTURE_BORDER_COLOR, (f32 []){0, 0, 0, 1}); 412 } 413 414 static void 415 ui_variable_free(BeamformerUI *ui, Variable *var) 416 { 417 if (var) { 418 var->parent = 0; 419 while (var) { 420 if (var->type == VT_GROUP || var->type == VT_UI_VIEW) { 421 var = var->u.group.first; 422 } else { 423 if (var->type == VT_BEAMFORMER_FRAME_VIEW) { 424 /* TODO(rnp): instead there should be a way of linking these up */ 425 BeamformerFrameView *bv = var->u.generic; 426 glDeleteTextures(1, &bv->frame->texture); 427 bv->frame->texture = 0; 428 DLLRemove(bv); 429 /* TODO(rnp): hack; use a sentinal */ 430 if (bv == ui->views) 431 ui->views = bv->next; 432 SLLPush(bv->frame, ui->frame_freelist); 433 SLLPush(bv, ui->view_freelist); 434 } 435 436 Variable *next = SLLPush(var, ui->variable_freelist); 437 if (next) { 438 var = next; 439 } else { 440 var = var->parent; 441 /* NOTE(rnp): when we assign parent here we have already 442 * released the children. Assign type so we don't loop */ 443 if (var) var->type = VT_NULL; 444 } 445 } 446 } 447 } 448 } 449 450 static void 451 ui_view_free(BeamformerUI *ui, Variable *view) 452 { 453 ASSERT(view->type == VT_UI_VIEW); 454 ui_variable_free(ui, view->u.view.close); 455 ui_variable_free(ui, view->u.view.menu); 456 ui_variable_free(ui, view); 457 } 458 459 static Variable * 460 fill_variable(Variable *var, Variable *group, s8 name, u32 flags, VariableType type, Font font) 461 { 462 var->flags = flags; 463 var->type = type; 464 var->name = name; 465 var->parent = group; 466 var->name_width = measure_text(font, name).x; 467 468 if (group && (group->type == VT_GROUP || group->type == VT_UI_VIEW)) { 469 if (group->u.group.last) group->u.group.last = group->u.group.last->next = var; 470 else group->u.group.last = group->u.group.first = var; 471 472 group->u.group.max_name_width = MAX(group->u.group.max_name_width, var->name_width); 473 } 474 475 return var; 476 } 477 478 static Variable * 479 add_variable(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, u32 flags, 480 VariableType type, Font font) 481 { 482 Variable *result = SLLPop(ui->variable_freelist); 483 if (result) zero_struct(result); 484 else result = push_struct(arena, Variable); 485 return fill_variable(result, group, name, flags, type, font); 486 } 487 488 static Variable * 489 add_variable_group(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, VariableGroupType type, Font font) 490 { 491 Variable *result = add_variable(ui, group, arena, name, V_INPUT, VT_GROUP, font); 492 result->u.group.type = type; 493 return result; 494 } 495 496 static Variable * 497 end_variable_group(Variable *group) 498 { 499 ASSERT(group->type == VT_GROUP); 500 return group->parent; 501 } 502 503 static Variable * 504 add_button(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, UIButtonID id, Font font) 505 { 506 Variable *result = add_variable(ui, group, arena, name, V_INPUT, VT_UI_BUTTON, font); 507 result->u.button = id; 508 return result; 509 } 510 511 static Variable * 512 add_ui_split(BeamformerUI *ui, Variable *parent, Arena *arena, s8 name, f32 fraction, 513 RegionSplitDirection direction, Font font) 514 { 515 Variable *result = add_variable(ui, parent, arena, name, 0, VT_UI_REGION_SPLIT, font); 516 result->u.region_split.direction = direction; 517 result->u.region_split.fraction = fraction; 518 return result; 519 } 520 521 static Variable * 522 add_ui_view(BeamformerUI *ui, Variable *parent, Arena *arena, s8 name, u32 view_flags, b32 closable) 523 { 524 Variable *result = add_variable(ui, parent, arena, name, 0, VT_UI_VIEW, ui->small_font); 525 UIView *view = &result->u.view; 526 view->group.type = VG_LIST; 527 view->flags = view_flags; 528 if (closable) { 529 view->close = add_button(ui, 0, arena, s8(""), UI_BID_CLOSE_VIEW, ui->small_font); 530 /* NOTE(rnp): we do this explicitly so that close doesn't end up in the view group */ 531 view->close->parent = result; 532 } 533 return result; 534 } 535 536 static void 537 add_beamformer_variable_f32(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, s8 suffix, 538 f32 *store, v2 limits, f32 display_scale, f32 scroll_scale, u32 flags, 539 Font font) 540 { 541 Variable *var = add_variable(ui, group, arena, name, flags, VT_BEAMFORMER_VARIABLE, font); 542 BeamformerVariable *bv = &var->u.beamformer_variable; 543 bv->suffix = suffix; 544 bv->store = store; 545 bv->store_type = VT_F32; 546 bv->params.display_scale = display_scale; 547 bv->params.scroll_scale = scroll_scale; 548 bv->params.limits = limits; 549 } 550 551 static void 552 add_beamformer_variable_b32(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, 553 s8 false_text, s8 true_text, b32 *store, u32 flags, Font font) 554 { 555 Variable *var = add_variable(ui, group, arena, name, flags, VT_BEAMFORMER_VARIABLE, font); 556 BeamformerVariable *bv = &var->u.beamformer_variable; 557 bv->store = store; 558 bv->store_type = VT_B32; 559 bv->name_table.names = alloc(arena, s8, 2); 560 bv->name_table.count = 2; 561 bv->name_table.names[0] = false_text; 562 bv->name_table.names[1] = true_text; 563 } 564 565 static Variable * 566 add_beamformer_parameters_view(Variable *parent, BeamformerCtx *ctx) 567 { 568 BeamformerUI *ui = ctx->ui; 569 BeamformerUIParameters *bp = &ui->params; 570 571 v2 v2_inf = {.x = -F32_INFINITY, .y = F32_INFINITY}; 572 573 /* TODO(rnp): this can be closable once we have a way of opening new views */ 574 Variable *result = add_ui_view(ui, parent, &ui->arena, s8("Parameters"), 0, 0); 575 576 add_beamformer_variable_f32(ui, result, &ui->arena, s8("Sampling Frequency:"), s8("[MHz]"), 577 &bp->sampling_frequency, (v2){0}, 1e-6, 0, 0, ui->font); 578 579 add_beamformer_variable_f32(ui, result, &ui->arena, s8("Center Frequency:"), s8("[MHz]"), 580 &bp->center_frequency, (v2){.y = 100e-6}, 1e-6, 1e5, 581 V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); 582 583 add_beamformer_variable_f32(ui, result, &ui->arena, s8("Speed of Sound:"), s8("[m/s]"), 584 &bp->speed_of_sound, (v2){.y = 1e6}, 1, 10, 585 V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); 586 587 result = add_variable_group(ui, result, &ui->arena, s8("Lateral Extent:"), VG_V2, ui->font); 588 { 589 add_beamformer_variable_f32(ui, result, &ui->arena, s8("Min:"), s8("[mm]"), 590 &bp->output_min_coordinate.x, v2_inf, 1e3, 0.5e-3, 591 V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); 592 593 add_beamformer_variable_f32(ui, result, &ui->arena, s8("Max:"), s8("[mm]"), 594 &bp->output_max_coordinate.x, v2_inf, 1e3, 0.5e-3, 595 V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); 596 } 597 result = end_variable_group(result); 598 599 result = add_variable_group(ui, result, &ui->arena, s8("Axial Extent:"), VG_V2, ui->font); 600 { 601 add_beamformer_variable_f32(ui, result, &ui->arena, s8("Min:"), s8("[mm]"), 602 &bp->output_min_coordinate.z, v2_inf, 1e3, 0.5e-3, 603 V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); 604 605 add_beamformer_variable_f32(ui, result, &ui->arena, s8("Max:"), s8("[mm]"), 606 &bp->output_max_coordinate.z, v2_inf, 1e3, 0.5e-3, 607 V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); 608 } 609 result = end_variable_group(result); 610 611 add_beamformer_variable_f32(ui, result, &ui->arena, s8("Off Axis Position:"), s8("[mm]"), 612 &bp->off_axis_pos, (v2){.x = -1e3, .y = 1e3}, 1e3, 613 0.5e-3, V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); 614 615 add_beamformer_variable_b32(ui, result, &ui->arena, s8("Beamform Plane:"), s8("XZ"), s8("YZ"), 616 (b32 *)&bp->beamform_plane, V_INPUT|V_CAUSES_COMPUTE, ui->font); 617 618 add_beamformer_variable_f32(ui, result, &ui->arena, s8("F#:"), s8(""), &bp->f_number, 619 (v2){.y = 1e3}, 1, 0.1, V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); 620 621 add_beamformer_variable_b32(ui, result, &ui->arena, s8("Interpolate:"), s8("False"), s8("True"), 622 &bp->interpolate, V_INPUT|V_CAUSES_COMPUTE, ui->font); 623 624 return result; 625 } 626 627 static Variable * 628 add_beamformer_frame_view(BeamformerUI *ui, Variable *parent, Arena *arena, 629 BeamformerFrameViewType type, b32 closable) 630 { 631 /* TODO(rnp): this can be always closable once we have a way of opening new views */ 632 Variable *result = add_ui_view(ui, parent, arena, s8(""), UI_VIEW_CUSTOM_TEXT, closable); 633 Variable *menu = result->u.view.menu = add_variable_group(ui, 0, &ui->arena, s8(""), 634 VG_LIST, ui->small_font); 635 menu->flags = V_MENU; 636 menu->parent = result; 637 #define X(id, text) add_button(ui, menu, &ui->arena, s8(text), UI_BID_ ##id, ui->small_font); 638 FRAME_VIEW_BUTTONS 639 #undef X 640 641 Variable *var = add_variable(ui, result, &ui->arena, s8(""), 0, VT_BEAMFORMER_FRAME_VIEW, 642 ui->small_font); 643 644 BeamformerFrameView *bv = SLLPop(ui->view_freelist); 645 if (bv) zero_struct(bv); 646 else bv = push_struct(arena, typeof(*bv)); 647 DLLPushDown(bv, ui->views); 648 649 var->u.generic = bv; 650 651 fill_variable(&bv->dynamic_range, var, s8("Dynamic Range:"), V_INPUT|V_TEXT|V_UPDATE_VIEW, 652 VT_F32, ui->small_font); 653 fill_variable(&bv->threshold, var, s8("Threshold:"), V_INPUT|V_TEXT|V_UPDATE_VIEW, 654 VT_F32, ui->small_font); 655 656 bv->type = type; 657 bv->dynamic_range.u.f32 = -50.0f; 658 bv->threshold.u.f32 = 40.0f; 659 660 bv->lateral_scale_bar.limits = (v2){.x = -1, .y = 1}; 661 bv->axial_scale_bar.limits = (v2){.x = 0, .y = 1}; 662 bv->lateral_scale_bar.scroll_scale = (v2){.x = -0.5e-3, .y = 0.5e-3}; 663 bv->axial_scale_bar.scroll_scale = (v2){.x = 0, .y = 1e-3}; 664 bv->lateral_scale_bar.zoom_starting_point = (v2){.x = F32_INFINITY, .y = F32_INFINITY}; 665 bv->axial_scale_bar.zoom_starting_point = (v2){.x = F32_INFINITY, .y = F32_INFINITY}; 666 667 return result; 668 } 669 670 static Variable * 671 add_compute_progress_bar(Variable *parent, BeamformerCtx *ctx) 672 { 673 BeamformerUI *ui = ctx->ui; 674 /* TODO(rnp): this can be closable once we have a way of opening new views */ 675 Variable *result = add_ui_view(ui, parent, &ui->arena, s8(""), UI_VIEW_CUSTOM_TEXT, 0); 676 add_variable(ui, result, &ui->arena, s8(""), 0, VT_COMPUTE_PROGRESS_BAR, ui->small_font); 677 ComputeProgressBar *bar = &result->u.group.first->u.compute_progress_bar; 678 bar->progress = &ctx->csctx.processing_progress; 679 bar->processing = &ctx->csctx.processing_compute; 680 681 return result; 682 } 683 684 static Variable * 685 add_compute_stats_view(BeamformerUI *ui, Variable *parent, Arena *arena, VariableType type) 686 { 687 /* TODO(rnp): this can be closable once we have a way of opening new views */ 688 Variable *result = add_ui_view(ui, parent, arena, s8(""), UI_VIEW_CUSTOM_TEXT, 0); 689 add_variable(ui, result, &ui->arena, s8(""), 0, type, ui->small_font); 690 return result; 691 } 692 693 static void 694 ui_copy_frame(BeamformerUI *ui, Variable *button, RegionSplitDirection direction) 695 { 696 Variable *menu = button->parent; 697 Variable *view = menu->parent; 698 Variable *region = view->parent; 699 ASSERT(region->type == VT_UI_REGION_SPLIT); 700 ASSERT(view->type == VT_UI_VIEW); 701 702 BeamformerFrameView *old = view->u.group.first->u.generic; 703 /* TODO(rnp): hack; it would be better if this was unreachable with a 0 old->frame */ 704 if (!old->frame) 705 return; 706 707 Variable *new_region = add_ui_split(ui, region, &ui->arena, s8(""), 0.5, direction, ui->small_font); 708 709 if (view == region->u.region_split.left) { 710 region->u.region_split.left = new_region; 711 } else { 712 region->u.region_split.right = new_region; 713 } 714 view->parent = new_region; 715 new_region->u.region_split.left = view; 716 new_region->u.region_split.right = add_beamformer_frame_view(ui, new_region, &ui->arena, FVT_COPY, 1); 717 718 BeamformerFrameView *bv = new_region->u.region_split.right->u.group.first->u.generic; 719 bv->lateral_scale_bar.min_value = &bv->min_coordinate.x; 720 bv->lateral_scale_bar.max_value = &bv->max_coordinate.x; 721 bv->axial_scale_bar.min_value = &bv->min_coordinate.z; 722 bv->axial_scale_bar.max_value = &bv->max_coordinate.z; 723 724 bv->ctx = old->ctx; 725 bv->needs_update = 1; 726 bv->threshold.u.f32 = old->threshold.u.f32; 727 bv->dynamic_range.u.f32 = old->dynamic_range.u.f32; 728 bv->min_coordinate = old->frame->min_coordinate; 729 bv->max_coordinate = old->frame->max_coordinate; 730 731 bv->frame = SLLPop(ui->frame_freelist); 732 if (!bv->frame) bv->frame = push_struct(&ui->arena, typeof(*bv->frame)); 733 734 ASSERT(old->frame->in_flight == 0); 735 mem_copy(bv->frame, old->frame, sizeof(*bv->frame)); 736 bv->frame->texture = 0; 737 bv->frame->next = 0; 738 alloc_beamform_frame(0, bv->frame, 0, old->frame->dim, s8("Frame Copy: "), ui->arena); 739 bv->frame->ready_to_present = 1; 740 741 glCopyImageSubData(old->frame->texture, GL_TEXTURE_3D, 0, 0, 0, 0, 742 bv->frame->texture, GL_TEXTURE_3D, 0, 0, 0, 0, 743 bv->frame->dim.x, bv->frame->dim.y, bv->frame->dim.z); 744 glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT); 745 /* TODO(rnp): x vs y here */ 746 resize_frame_view(bv, (uv2){.x = bv->frame->dim.x, .y = bv->frame->dim.z}); 747 } 748 749 static b32 750 view_update(BeamformerUI *ui, BeamformerFrameView *view) 751 { 752 b32 needs_resize = 0; 753 uv2 current = {.w = view->rendered_view.texture.width, .h = view->rendered_view.texture.height}; 754 uv2 target; 755 756 switch (view->type) { 757 case FVT_LATEST: { 758 /* TODO(rnp): x-z or y-z */ 759 target = (uv2){.w = ui->params.output_points.x, .h = ui->params.output_points.z}; 760 needs_resize = !uv2_equal(current, target) && !uv2_equal(target, (uv2){0}); 761 view->needs_update |= view->frame != ui->latest_frame; 762 view->frame = ui->latest_frame; 763 } break; 764 default: { 765 /* TODO(rnp): add method of setting a target size in frame view */ 766 target = (uv2){.w = ui->params.output_points.x, .h = ui->params.output_points.z}; 767 needs_resize = !uv2_equal(current, target) && !uv2_equal(target, (uv2){0}); 768 } break; 769 } 770 771 if (needs_resize && view->frame) { 772 resize_frame_view(view, target); 773 view->needs_update = 1; 774 } 775 776 return (view->ctx->updated || view->needs_update) && view->frame; 777 } 778 779 static void 780 update_frame_views(BeamformerUI *ui) 781 { 782 for (BeamformerFrameView *view = ui->views; view; view = view->next) { 783 if (view_update(ui, view)) { 784 BeamformFrame *frame = view->frame; 785 BeginTextureMode(view->rendered_view); 786 ClearBackground(PINK); 787 BeginShaderMode(view->ctx->shader); 788 glUseProgram(view->ctx->shader.id); 789 glBindTextureUnit(0, frame->texture); 790 glUniform1f(view->ctx->db_cutoff_id, view->dynamic_range.u.f32); 791 glUniform1f(view->ctx->threshold_id, view->threshold.u.f32); 792 DrawTexture(view->rendered_view.texture, 0, 0, WHITE); 793 EndShaderMode(); 794 EndTextureMode(); 795 glGenerateTextureMipmap(view->rendered_view.texture.id); 796 view->needs_update = 0; 797 } 798 } 799 } 800 801 static b32 802 frame_view_ready_to_present(BeamformerFrameView *view) 803 { 804 uv2 render_size = {.w = view->rendered_view.texture.width, 805 .h = view->rendered_view.texture.height}; 806 return !uv2_equal((uv2){0}, render_size); 807 } 808 809 static Color 810 colour_from_normalized(v4 rgba) 811 { 812 return (Color){.r = rgba.r * 255.0f, .g = rgba.g * 255.0f, 813 .b = rgba.b * 255.0f, .a = rgba.a * 255.0f}; 814 } 815 816 static Color 817 fade(Color a, f32 visibility) 818 { 819 a.a = (u8)((f32)a.a * visibility); 820 return a; 821 } 822 823 static v4 824 lerp_v4(v4 a, v4 b, f32 t) 825 { 826 return (v4){ 827 .x = a.x + t * (b.x - a.x), 828 .y = a.y + t * (b.y - a.y), 829 .z = a.z + t * (b.z - a.z), 830 .w = a.w + t * (b.w - a.w), 831 }; 832 } 833 834 static s8 835 push_das_shader_id(Stream *s, DASShaderID shader, u32 transmit_count) 836 { 837 #define X(type, id, pretty, fixed_tx) s8(pretty), 838 static s8 pretty_names[] = { DAS_TYPES }; 839 #undef X 840 #define X(type, id, pretty, fixed_tx) fixed_tx, 841 static u8 fixed_transmits[] = { DAS_TYPES }; 842 #undef X 843 844 if ((u32)shader < (u32)DAS_LAST) { 845 stream_append_s8(s, pretty_names[shader]); 846 if (!fixed_transmits[shader]) { 847 stream_append_byte(s, '-'); 848 stream_append_u64(s, transmit_count); 849 } 850 } 851 852 return stream_to_s8(s); 853 } 854 855 static s8 856 push_custom_view_title(Stream *s, Variable *var) 857 { 858 switch (var->type) { 859 case VT_COMPUTE_STATS_VIEW: 860 case VT_COMPUTE_LATEST_STATS_VIEW: { 861 stream_append_s8(s, s8("Compute Stats")); 862 if (var->type == VT_COMPUTE_LATEST_STATS_VIEW) 863 stream_append_s8(s, s8(": Live")); 864 } break; 865 case VT_COMPUTE_PROGRESS_BAR: { 866 stream_append_s8(s, s8("Compute Progress: ")); 867 stream_append_f64(s, 100 * *var->u.compute_progress_bar.progress, 100); 868 stream_append_byte(s, '%'); 869 } break; 870 case VT_BEAMFORMER_FRAME_VIEW: { 871 BeamformerFrameView *bv = var->u.generic; 872 stream_append_s8(s, s8("Frame View")); 873 switch (bv->type) { 874 case FVT_LATEST: stream_append_s8(s, s8(": Live [")); break; 875 case FVT_COPY: stream_append_s8(s, s8(": Copy [")); break; 876 case FVT_INDEXED: stream_append_s8(s, s8(": [")); break; 877 } 878 stream_append_hex_u64(s, bv->frame? bv->frame->id : 0); 879 stream_append_byte(s, ']'); 880 } break; 881 default: INVALID_CODE_PATH; 882 } 883 return stream_to_s8(s); 884 } 885 886 static v2 887 draw_text_base(Font font, s8 text, v2 pos, Color colour) 888 { 889 v2 off = pos; 890 for (iz i = 0; i < text.len; i++) { 891 /* NOTE: assumes font glyphs are ordered ASCII */ 892 i32 idx = text.data[i] - 0x20; 893 Rectangle dst = { 894 off.x + font.glyphs[idx].offsetX - font.glyphPadding, 895 off.y + font.glyphs[idx].offsetY - font.glyphPadding, 896 font.recs[idx].width + 2.0f * font.glyphPadding, 897 font.recs[idx].height + 2.0f * font.glyphPadding 898 }; 899 Rectangle src = { 900 font.recs[idx].x - font.glyphPadding, 901 font.recs[idx].y - font.glyphPadding, 902 font.recs[idx].width + 2.0f * font.glyphPadding, 903 font.recs[idx].height + 2.0f * font.glyphPadding 904 }; 905 DrawTexturePro(font.texture, src, dst, (Vector2){0}, 0, colour); 906 907 off.x += font.glyphs[idx].advanceX; 908 if (font.glyphs[idx].advanceX == 0) 909 off.x += font.recs[idx].width; 910 } 911 v2 result = {.x = off.x - pos.x, .y = font.baseSize}; 912 return result; 913 } 914 915 /* NOTE(rnp): expensive but of the available options in raylib this gives the best results */ 916 static v2 917 draw_outlined_text(s8 text, v2 pos, TextSpec *ts) 918 { 919 f32 ow = ts->outline_thick; 920 draw_text_base(*ts->font, text, sub_v2(pos, (v2){.x = ow, .y = ow}), ts->outline_colour); 921 draw_text_base(*ts->font, text, sub_v2(pos, (v2){.x = ow, .y = -ow}), ts->outline_colour); 922 draw_text_base(*ts->font, text, sub_v2(pos, (v2){.x = -ow, .y = ow}), ts->outline_colour); 923 draw_text_base(*ts->font, text, sub_v2(pos, (v2){.x = -ow, .y = -ow}), ts->outline_colour); 924 925 v2 result = draw_text_base(*ts->font, text, pos, ts->colour); 926 927 return result; 928 } 929 930 static v2 931 draw_text(s8 text, v2 pos, TextSpec *ts) 932 { 933 if (ts->flags & TF_ROTATED) { 934 rlPushMatrix(); 935 rlTranslatef(pos.x, pos.y, 0); 936 rlRotatef(ts->rotation, 0, 0, 1); 937 pos = (v2){0}; 938 } 939 940 v2 result = measure_text(*ts->font, text); 941 s8 ellipsis = s8("..."); 942 b32 clamped = ts->flags & TF_LIMITED && result.w > ts->limits.size.w; 943 if (clamped) { 944 f32 elipsis_width = measure_text(*ts->font, ellipsis).x; 945 if (elipsis_width < ts->limits.size.w) { 946 /* TODO(rnp): there must be a better way */ 947 while (text.len) { 948 text.len--; 949 f32 width = measure_text(*ts->font, text).x; 950 if (width + elipsis_width < ts->limits.size.w) 951 break; 952 } 953 } else { 954 text.len = 0; 955 ellipsis.len = 0; 956 } 957 } 958 959 if (ts->flags & TF_OUTLINED) result.x = draw_outlined_text(text, pos, ts).x; 960 else result.x = draw_text_base(*ts->font, text, pos, ts->colour).x; 961 962 if (clamped) { 963 pos.x += result.x; 964 if (ts->flags & TF_OUTLINED) result.x += draw_outlined_text(ellipsis, pos, ts).x; 965 else result.x += draw_text_base(*ts->font, ellipsis, pos, 966 ts->colour).x; 967 } 968 969 if (ts->flags & TF_ROTATED) rlPopMatrix(); 970 971 return result; 972 } 973 974 static Rect 975 extend_rect_centered(Rect r, v2 delta) 976 { 977 r.size.w += delta.x; 978 r.size.h += delta.y; 979 r.pos.x -= delta.x / 2; 980 r.pos.y -= delta.y / 2; 981 return r; 982 } 983 984 static Rect 985 shrink_rect_centered(Rect r, v2 delta) 986 { 987 delta.x = MIN(delta.x, r.size.w); 988 delta.y = MIN(delta.y, r.size.h); 989 r.size.w -= delta.x; 990 r.size.h -= delta.y; 991 r.pos.x += delta.x / 2; 992 r.pos.y += delta.y / 2; 993 return r; 994 } 995 996 static Rect 997 scale_rect_centered(Rect r, v2 scale) 998 { 999 Rect or = r; 1000 r.size.w *= scale.x; 1001 r.size.h *= scale.y; 1002 r.pos.x += (or.size.w - r.size.w) / 2; 1003 r.pos.y += (or.size.h - r.size.h) / 2; 1004 return r; 1005 } 1006 1007 static b32 1008 hover_rect(v2 mouse, Rect rect, f32 *hover_t) 1009 { 1010 b32 hovering = CheckCollisionPointRec(mouse.rl, rect.rl); 1011 if (hovering) *hover_t += HOVER_SPEED * dt_for_frame; 1012 else *hover_t -= HOVER_SPEED * dt_for_frame; 1013 *hover_t = CLAMP01(*hover_t); 1014 return hovering; 1015 } 1016 1017 static b32 1018 hover_var(BeamformerUI *ui, v2 mouse, Rect rect, Variable *var) 1019 { 1020 b32 result = hover_rect(mouse, rect, &var->hover_t); 1021 if (result) { 1022 ui->interaction.hot_type = IT_NONE; 1023 ui->interaction.hot = var; 1024 } 1025 return result; 1026 } 1027 1028 static Rect 1029 draw_title_bar(BeamformerUI *ui, Arena arena, Variable *ui_view, Rect r, v2 mouse) 1030 { 1031 ASSERT(ui_view->type == VT_UI_VIEW); 1032 UIView *view = &ui_view->u.view; 1033 1034 s8 title = ui_view->name; 1035 if (view->flags & UI_VIEW_CUSTOM_TEXT) { 1036 Stream buf = arena_stream(&arena); 1037 title = push_custom_view_title(&buf, ui_view->u.group.first); 1038 } 1039 1040 Rect result, title_rect; 1041 cut_rect_vertical(r, ui->small_font.baseSize + TITLE_BAR_PAD, &title_rect, &result); 1042 cut_rect_vertical(result, LISTING_LINE_PAD, 0, &result); 1043 1044 DrawRectangleRec(title_rect.rl, BLACK); 1045 1046 title_rect = shrink_rect_centered(title_rect, (v2){.x = 1.5 * TITLE_BAR_PAD}); 1047 DrawRectangleRounded(title_rect.rl, 0.5, 0, fade(colour_from_normalized(BG_COLOUR), 0.55)); 1048 title_rect = shrink_rect_centered(title_rect, (v2){.x = 3 * TITLE_BAR_PAD}); 1049 1050 if (view->close) { 1051 Rect close; 1052 cut_rect_horizontal(title_rect, title_rect.size.w - title_rect.size.h, &title_rect, &close); 1053 hover_var(ui, mouse, close, view->close); 1054 1055 Color colour = colour_from_normalized(lerp_v4(MENU_CLOSE_COLOUR, FG_COLOUR, view->close->hover_t)); 1056 close = shrink_rect_centered(close, (v2){.x = 16, .y = 16}); 1057 DrawLineEx(close.pos.rl, add_v2(close.pos, close.size).rl, 4, colour); 1058 DrawLineEx(add_v2(close.pos, (v2){.x = close.size.w}).rl, 1059 add_v2(close.pos, (v2){.y = close.size.h}).rl, 4, colour); 1060 } 1061 1062 if (view->menu) { 1063 Rect menu; 1064 cut_rect_horizontal(title_rect, title_rect.size.w - title_rect.size.h, &title_rect, &menu); 1065 if (hover_var(ui, mouse, menu, view->menu)) 1066 ui->menu_state.hot_font = &ui->small_font; 1067 1068 Color colour = colour_from_normalized(lerp_v4(MENU_PLUS_COLOUR, FG_COLOUR, view->menu->hover_t)); 1069 menu = shrink_rect_centered(menu, (v2){.x = 14, .y = 14}); 1070 DrawLineEx(add_v2(menu.pos, (v2){.x = menu.size.w / 2}).rl, 1071 add_v2(menu.pos, (v2){.x = menu.size.w / 2, .y = menu.size.h}).rl, 4, colour); 1072 DrawLineEx(add_v2(menu.pos, (v2){.y = menu.size.h / 2}).rl, 1073 add_v2(menu.pos, (v2){.x = menu.size.w, .y = menu.size.h / 2}).rl, 4, colour); 1074 } 1075 1076 /* TODO(rnp): hover the title text rect and use it to access the global menu */ 1077 v2 title_pos = title_rect.pos; 1078 title_pos.y += 0.5 * TITLE_BAR_PAD; 1079 TextSpec text_spec = {.font = &ui->small_font, .flags = TF_LIMITED, 1080 .colour = NORMALIZED_FG_COLOUR, .limits.size = title_rect.size}; 1081 draw_text(title, title_pos, &text_spec); 1082 1083 return result; 1084 } 1085 1086 /* TODO(rnp): once this has more callers decide if it would be better for this to take 1087 * an orientation rather than force CCW/right-handed */ 1088 static void 1089 draw_ruler(BeamformerUI *ui, Stream *buf, v2 start_point, v2 end_point, 1090 f32 start_value, f32 end_value, f32 *markers, u32 marker_count, 1091 u32 segments, s8 suffix, Color ruler_colour, Color txt_colour) 1092 { 1093 b32 draw_plus = SIGN(start_value) != SIGN(end_value); 1094 1095 end_point = sub_v2(end_point, start_point); 1096 f32 rotation = atan2_f32(end_point.y, end_point.x) * 180 / PI; 1097 1098 rlPushMatrix(); 1099 rlTranslatef(start_point.x, start_point.y, 0); 1100 rlRotatef(rotation, 0, 0, 1); 1101 1102 f32 inc = magnitude_v2(end_point) / segments; 1103 f32 value_inc = (end_value - start_value) / segments; 1104 f32 value = start_value; 1105 1106 v2 sp = {0}, ep = {.y = RULER_TICK_LENGTH}; 1107 v2 tp = {.x = ui->small_font.baseSize / 2, .y = ep.y + RULER_TEXT_PAD}; 1108 TextSpec text_spec = {.font = &ui->small_font, .rotation = 90, .colour = txt_colour, .flags = TF_ROTATED}; 1109 for (u32 j = 0; j <= segments; j++) { 1110 DrawLineEx(sp.rl, ep.rl, 3, ruler_colour); 1111 1112 stream_reset(buf, 0); 1113 if (draw_plus && value > 0) stream_append_byte(buf, '+'); 1114 stream_append_f64(buf, value, 10); 1115 stream_append_s8(buf, suffix); 1116 draw_text(stream_to_s8(buf), tp, &text_spec); 1117 1118 value += value_inc; 1119 sp.x += inc; 1120 ep.x += inc; 1121 tp.x += inc; 1122 } 1123 1124 ep.y += RULER_TICK_LENGTH; 1125 for (u32 i = 0; i < marker_count; i++) { 1126 if (markers[i] < F32_INFINITY) { 1127 ep.x = sp.x = markers[i]; 1128 DrawLineEx(sp.rl, ep.rl, 3, colour_from_normalized(RULER_COLOUR)); 1129 DrawCircleV(ep.rl, 3, colour_from_normalized(RULER_COLOUR)); 1130 } 1131 } 1132 1133 rlPopMatrix(); 1134 } 1135 1136 static void 1137 do_scale_bar(BeamformerUI *ui, Stream *buf, ScaleBar *sb, ScaleBarDirection direction, 1138 v2 mouse, Rect draw_rect, f32 start_value, f32 end_value, s8 suffix) 1139 { 1140 InteractionState *is = &ui->interaction; 1141 1142 v2 txt_s = measure_text(ui->small_font, s8("-288.8 mm")); 1143 1144 Rect tick_rect = draw_rect; 1145 v2 start_pos = tick_rect.pos; 1146 v2 end_pos = tick_rect.pos; 1147 v2 relative_mouse = sub_v2(mouse, tick_rect.pos); 1148 1149 f32 markers[2]; 1150 u32 marker_count = 1; 1151 1152 u32 tick_count; 1153 if (direction == SB_AXIAL) { 1154 tick_rect.size.x = RULER_TEXT_PAD + RULER_TICK_LENGTH + txt_s.x; 1155 tick_count = tick_rect.size.y / (1.5 * ui->small_font.baseSize); 1156 start_pos.y += tick_rect.size.y; 1157 markers[0] = tick_rect.size.y - sb->zoom_starting_point.y; 1158 markers[1] = tick_rect.size.y - relative_mouse.y; 1159 sb->screen_offset = (v2){.y = tick_rect.pos.y}; 1160 sb->screen_space_to_value = (v2){.y = (*sb->max_value - *sb->min_value) / tick_rect.size.y}; 1161 } else { 1162 tick_rect.size.y = RULER_TEXT_PAD + RULER_TICK_LENGTH + txt_s.x; 1163 tick_count = tick_rect.size.x / (1.5 * ui->small_font.baseSize); 1164 end_pos.x += tick_rect.size.x; 1165 markers[0] = sb->zoom_starting_point.x; 1166 markers[1] = relative_mouse.x; 1167 /* TODO(rnp): screen space to value space transformation helper */ 1168 sb->screen_offset = (v2){.x = tick_rect.pos.x}; 1169 sb->screen_space_to_value = (v2){.x = (*sb->max_value - *sb->min_value) / tick_rect.size.x}; 1170 } 1171 1172 if (hover_rect(mouse, tick_rect, &sb->hover_t)) { 1173 Variable *var = zero_struct(ui->scratch_variable); 1174 var->u.generic = sb; 1175 var->type = VT_SCALE_BAR; 1176 is->hot_type = IT_SCALE_BAR; 1177 is->hot = var; 1178 1179 marker_count = 2; 1180 } 1181 1182 draw_ruler(ui, buf, start_pos, end_pos, start_value, end_value, markers, marker_count, 1183 tick_count, suffix, colour_from_normalized(FG_COLOUR), 1184 colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, sb->hover_t))); 1185 } 1186 1187 static v2 1188 draw_beamformer_variable(BeamformerUI *ui, Arena arena, Variable *var, v2 at, v2 mouse, 1189 v4 base_colour, TextSpec text_spec) 1190 { 1191 Stream buf = arena_stream(&arena); 1192 stream_append_variable(&buf, var); 1193 1194 v2 text_size = measure_text(*text_spec.font, stream_to_s8(&buf)); 1195 if (var->flags & V_INPUT) { 1196 Rect text_rect = {.pos = at, .size = text_size}; 1197 text_rect = extend_rect_centered(text_rect, (v2){.x = 8}); 1198 1199 if (hover_var(ui, mouse, text_rect, var) && (var->flags & V_TEXT)) { 1200 InputState *is = &ui->text_input_state; 1201 is->hot_rect = text_rect; 1202 is->hot_font = text_spec.font; 1203 } 1204 1205 text_spec.colour = colour_from_normalized(lerp_v4(base_colour, HOVERED_COLOUR, var->hover_t)); 1206 } 1207 return draw_text(stream_to_s8(&buf), at, &text_spec); 1208 } 1209 1210 static void 1211 draw_beamformer_frame_view(BeamformerUI *ui, Arena a, Variable *var, Rect display_rect, v2 mouse) 1212 { 1213 ASSERT(var->type == VT_BEAMFORMER_FRAME_VIEW); 1214 BeamformerUIParameters *bp = &ui->params; 1215 InteractionState *is = &ui->interaction; 1216 BeamformerFrameView *view = var->u.generic; 1217 BeamformFrame *frame = view->frame; 1218 1219 v2 txt_s = measure_text(ui->small_font, s8("-288.8 mm")); 1220 1221 f32 pad = 1.2 * txt_s.x + RULER_TICK_LENGTH; 1222 Rect vr = display_rect; 1223 vr.pos.x += 0.5 * ui->small_font.baseSize; 1224 vr.pos.y += 0.5 * ui->small_font.baseSize; 1225 vr.size.h = MAX(0, display_rect.size.h - pad); 1226 vr.size.w = MAX(0, display_rect.size.w - pad); 1227 1228 /* TODO(rnp): ideally we hook up both versions to view->min/max */ 1229 v4 min = (view->type == FVT_LATEST)? bp->output_min_coordinate : view->min_coordinate; 1230 v4 max = (view->type == FVT_LATEST)? bp->output_max_coordinate : view->max_coordinate; 1231 1232 /* TODO(rnp): make this depend on the requested draw orientation (x-z or y-z or x-y) */ 1233 v2 output_dim = { 1234 .x = frame->max_coordinate.x - frame->min_coordinate.x, 1235 .y = frame->max_coordinate.z - frame->min_coordinate.z, 1236 }; 1237 v2 requested_dim = {.x = max.x - min.x, .y = max.z - min.z}; 1238 1239 f32 aspect = requested_dim.h / requested_dim.w; 1240 if (display_rect.size.h < (vr.size.w * aspect) + pad) { 1241 vr.size.w = vr.size.h / aspect; 1242 } else { 1243 vr.size.h = vr.size.w * aspect; 1244 } 1245 vr.pos.x += (display_rect.size.w - (vr.size.w + pad)) / 2; 1246 vr.pos.y += (display_rect.size.h - (vr.size.h + pad)) / 2; 1247 1248 Texture *output = &view->rendered_view.texture; 1249 v2 pixels_per_meter = { 1250 .w = (f32)output->width / output_dim.w, 1251 .h = (f32)output->height / output_dim.h, 1252 }; 1253 1254 v2 texture_points = mul_v2(pixels_per_meter, requested_dim); 1255 /* TODO(rnp): this also depends on x-y, y-z, x-z */ 1256 v2 texture_start = { 1257 .x = pixels_per_meter.x * 0.5 * (output_dim.x - requested_dim.x), 1258 .y = pixels_per_meter.y * (frame->max_coordinate.z - max.z), 1259 }; 1260 1261 Rectangle tex_r = {texture_start.x, texture_start.y, texture_points.x, -texture_points.y}; 1262 NPatchInfo tex_np = { tex_r, 0, 0, 0, 0, NPATCH_NINE_PATCH }; 1263 DrawTextureNPatch(*output, tex_np, vr.rl, (Vector2){0}, 0, WHITE); 1264 1265 v2 start_pos = vr.pos; 1266 start_pos.y += vr.size.y; 1267 1268 if (vr.size.w > 0) { 1269 Arena tmp = a; 1270 Stream buf = arena_stream(&tmp); 1271 do_scale_bar(ui, &buf, &view->lateral_scale_bar, SB_LATERAL, mouse, 1272 (Rect){.pos = start_pos, .size = vr.size}, 1273 *view->lateral_scale_bar.min_value * 1e3, 1274 *view->lateral_scale_bar.max_value * 1e3, s8(" mm")); 1275 } 1276 1277 start_pos = vr.pos; 1278 start_pos.x += vr.size.x; 1279 1280 if (vr.size.h > 0) { 1281 Arena tmp = a; 1282 Stream buf = arena_stream(&tmp); 1283 do_scale_bar(ui, &buf, &view->axial_scale_bar, SB_AXIAL, mouse, 1284 (Rect){.pos = start_pos, .size = vr.size}, 1285 *view->axial_scale_bar.max_value * 1e3, 1286 *view->axial_scale_bar.min_value * 1e3, s8(" mm")); 1287 } 1288 1289 v2 pixels_to_mm = output_dim; 1290 pixels_to_mm.x /= vr.size.x * 1e-3; 1291 pixels_to_mm.y /= vr.size.y * 1e-3; 1292 1293 TextSpec text_spec = {.font = &ui->small_font, .flags = TF_LIMITED|TF_OUTLINED, 1294 .colour = colour_from_normalized(RULER_COLOUR), 1295 .outline_thick = 1, .outline_colour = BLACK, 1296 .limits.size.x = vr.size.w}; 1297 1298 b32 drew_coordinates = 0; 1299 f32 remaining_width = vr.size.w; 1300 if (CheckCollisionPointRec(mouse.rl, vr.rl)) { 1301 is->hot_type = IT_DISPLAY; 1302 is->hot = var; 1303 1304 v2 relative_mouse = sub_v2(mouse, vr.pos); 1305 v2 mm = mul_v2(relative_mouse, pixels_to_mm); 1306 mm.x += 1e3 * min.x; 1307 mm.y += 1e3 * min.z; 1308 1309 Arena tmp = a; 1310 Stream buf = arena_stream(&tmp); 1311 stream_append_v2(&buf, mm); 1312 1313 text_spec.limits.size.w -= 4; 1314 v2 txt_s = measure_text(*text_spec.font, stream_to_s8(&buf)); 1315 v2 txt_p = { 1316 .x = vr.pos.x + vr.size.w - txt_s.w - 4, 1317 .y = vr.pos.y + vr.size.h - txt_s.h - 4, 1318 }; 1319 txt_p.x = MAX(vr.pos.x, txt_p.x); 1320 remaining_width -= draw_text(stream_to_s8(&buf), txt_p, &text_spec).w; 1321 text_spec.limits.size.w += 4; 1322 drew_coordinates = 1; 1323 } 1324 1325 { 1326 Arena tmp = a; 1327 Stream buf = arena_stream(&tmp); 1328 s8 shader = push_das_shader_id(&buf, frame->das_shader_id, frame->compound_count); 1329 text_spec.font = &ui->font; 1330 text_spec.limits.size.w -= 16; 1331 v2 txt_s = measure_text(*text_spec.font, shader); 1332 v2 txt_p = { 1333 .x = vr.pos.x + vr.size.w - txt_s.w - 16, 1334 .y = vr.pos.y + 4, 1335 }; 1336 txt_p.x = MAX(vr.pos.x, txt_p.x); 1337 draw_text(stream_to_s8(&buf), txt_p, &text_spec); 1338 text_spec.font = &ui->small_font; 1339 text_spec.limits.size.w += 16; 1340 } 1341 1342 /* TODO(rnp): store converted ruler points instead of screen points */ 1343 if (ui->ruler_state != RS_NONE && CheckCollisionPointRec(ui->ruler_start_p.rl, vr.rl)) { 1344 v2 end_p; 1345 if (ui->ruler_state == RS_START) end_p = mouse; 1346 else end_p = ui->ruler_stop_p; 1347 1348 end_p = clamp_v2_rect(end_p, vr); 1349 v2 pixel_delta = sub_v2(ui->ruler_start_p, end_p); 1350 v2 mm_delta = mul_v2(pixels_to_mm, pixel_delta); 1351 1352 DrawCircleV(ui->ruler_start_p.rl, 3, text_spec.colour); 1353 DrawLineEx(end_p.rl, ui->ruler_start_p.rl, 2, text_spec.colour); 1354 DrawCircleV(end_p.rl, 3, text_spec.colour); 1355 1356 Arena tmp = a; 1357 Stream buf = arena_stream(&tmp); 1358 stream_append_f64(&buf, magnitude_v2(mm_delta), 100); 1359 stream_append_s8(&buf, s8(" mm")); 1360 1361 v2 txt_p = ui->ruler_start_p; 1362 v2 txt_s = measure_text(*text_spec.font, stream_to_s8(&buf)); 1363 if (pixel_delta.y < 0) txt_p.y -= txt_s.y; 1364 if (pixel_delta.x < 0) txt_p.x -= txt_s.x; 1365 draw_text(stream_to_s8(&buf), txt_p, &text_spec); 1366 } 1367 1368 if (remaining_width > view->dynamic_range.name_width || !drew_coordinates) { 1369 f32 max_prefix_width = MAX(view->threshold.name_width, view->dynamic_range.name_width); 1370 1371 v2 end = add_v2(vr.pos, vr.size); 1372 f32 start_y = MAX(end.y - 4 - 2 * text_spec.font->baseSize, vr.pos.y); 1373 end.y -= text_spec.font->baseSize; 1374 v2 at = {.x = vr.pos.x + 4, .y = start_y}; 1375 1376 at.y += draw_text(view->dynamic_range.name, at, &text_spec).y; 1377 if (at.y < end.y) at.y += draw_text(view->threshold.name, at, &text_spec).y; 1378 1379 at.y = start_y; 1380 at.x += max_prefix_width + 8; 1381 text_spec.limits.size.x = end.x - at.x; 1382 1383 v2 size = draw_beamformer_variable(ui, a, &view->dynamic_range, at, mouse, 1384 RULER_COLOUR, text_spec); 1385 1386 f32 max_center_width = size.w; 1387 at.y += size.h; 1388 1389 if (at.y < end.y) { 1390 size = draw_beamformer_variable(ui, a, &view->threshold, at, mouse, 1391 RULER_COLOUR, text_spec); 1392 max_center_width = MAX(size.w, max_center_width); 1393 } 1394 1395 at.y = start_y; 1396 at.x += max_center_width + 8; 1397 text_spec.limits.size.x = end.x - at.x; 1398 draw_text(s8(" [dB]"), at, &text_spec); 1399 } 1400 } 1401 1402 static v2 1403 draw_compute_progress_bar(BeamformerUI *ui, Arena arena, ComputeProgressBar *state, Rect r) 1404 { 1405 if (*state->processing) state->display_t_velocity += 65 * dt_for_frame; 1406 else state->display_t_velocity -= 45 * dt_for_frame; 1407 1408 state->display_t_velocity = CLAMP(state->display_t_velocity, -10, 10); 1409 state->display_t += state->display_t_velocity * dt_for_frame; 1410 state->display_t = CLAMP01(state->display_t); 1411 1412 if (state->display_t > (1.0 / 255.0)) { 1413 Rect outline = {.pos = r.pos, .size = {.w = r.size.w, .h = ui->font.baseSize}}; 1414 outline = scale_rect_centered(outline, (v2){.x = 0.96, .y = 0.7}); 1415 Rect filled = outline; 1416 filled.size.w *= *state->progress; 1417 DrawRectangleRounded(filled.rl, 2, 0, fade(colour_from_normalized(HOVERED_COLOUR), 1418 state->display_t)); 1419 DrawRectangleRoundedLinesEx(outline.rl, 2, 0, 3, fade(BLACK, state->display_t)); 1420 } 1421 1422 v2 result = {.x = r.size.w, .y = ui->font.baseSize}; 1423 return result; 1424 } 1425 1426 static v2 1427 draw_compute_stats_view(BeamformerCtx *ctx, Arena arena, ComputeShaderStats *stats, Rect r) 1428 { 1429 static s8 labels[CS_LAST] = { 1430 #define X(e, n, s, h, pn) [CS_##e] = s8(pn ":"), 1431 COMPUTE_SHADERS 1432 #undef X 1433 }; 1434 1435 BeamformerUI *ui = ctx->ui; 1436 s8 compute_total = s8("Compute Total:"); 1437 f32 compute_total_width = measure_text(ui->font, compute_total).w; 1438 f32 max_label_width = compute_total_width; 1439 1440 f32 *label_widths = alloc(&arena, f32, ARRAY_COUNT(labels)); 1441 for (u32 i = 0; i < ARRAY_COUNT(labels); i++) { 1442 label_widths[i] = measure_text(ui->font, labels[i]).x; 1443 max_label_width = MAX(label_widths[i], max_label_width); 1444 } 1445 1446 v2 at = r.pos; 1447 Stream buf = stream_alloc(&arena, 64); 1448 f32 compute_time_sum = 0; 1449 u32 stages = ctx->params->compute_stages_count; 1450 TextSpec text_spec = {.font = &ui->font, .colour = NORMALIZED_FG_COLOUR, .flags = TF_LIMITED}; 1451 for (u32 i = 0; i < stages; i++) { 1452 u32 index = ctx->params->compute_stages[i]; 1453 text_spec.limits.size.x = r.size.w; 1454 draw_text(labels[index], at, &text_spec); 1455 text_spec.limits.size.x -= LISTING_ITEM_PAD + max_label_width; 1456 1457 stream_reset(&buf, 0); 1458 stream_append_f64_e(&buf, stats->times[index]); 1459 stream_append_s8(&buf, s8(" [s]")); 1460 v2 rpos = {.x = r.pos.x + max_label_width + LISTING_ITEM_PAD, .y = at.y}; 1461 at.y += draw_text(stream_to_s8(&buf), rpos, &text_spec).h; 1462 1463 compute_time_sum += stats->times[index]; 1464 } 1465 1466 stream_reset(&buf, 0); 1467 stream_append_f64_e(&buf, compute_time_sum); 1468 stream_append_s8(&buf, s8(" [s]")); 1469 v2 rpos = {.x = at.x + max_label_width + LISTING_ITEM_PAD, .y = at.y}; 1470 text_spec.limits.size.w = r.size.w; 1471 draw_text(compute_total, at, &text_spec); 1472 text_spec.limits.size.w -= LISTING_ITEM_PAD - max_label_width; 1473 at.y -= draw_text(stream_to_s8(&buf), rpos, &text_spec).h; 1474 1475 v2 result = {.x = r.size.w, .y = at.y - r.pos.y}; 1476 return result; 1477 } 1478 1479 static void 1480 draw_ui_view(BeamformerUI *ui, Variable *ui_view, Rect r, v2 mouse) 1481 { 1482 ASSERT(ui_view->type == VT_UI_VIEW); 1483 UIView *view = &ui_view->u.view; 1484 1485 /* TODO(rnp): this should get moved up to draw_variable */ 1486 hover_var(ui, mouse, r, ui_view); 1487 1488 if (view->needed_height - r.size.h < view->offset) 1489 view->offset = view->needed_height - r.size.h; 1490 1491 if (view->needed_height - r.size.h < 0) 1492 view->offset = 0; 1493 1494 r.pos.y -= view->offset; 1495 1496 f32 start_height = r.size.h; 1497 Variable *var = view->group.first; 1498 f32 x_off = view->group.max_name_width; 1499 TextSpec text_spec = {.font = &ui->font, .colour = NORMALIZED_FG_COLOUR, .flags = TF_LIMITED}; 1500 while (var) { 1501 s8 suffix = {0}; 1502 v2 at = r.pos; 1503 f32 advance = 0; 1504 switch (var->type) { 1505 case VT_BEAMFORMER_VARIABLE: { 1506 BeamformerVariable *bv = &var->u.beamformer_variable; 1507 1508 suffix = bv->suffix; 1509 text_spec.limits.size.w = r.size.w + r.pos.x - at.x; 1510 advance = draw_text(var->name, at, &text_spec).y; 1511 at.x += x_off + LISTING_ITEM_PAD; 1512 text_spec.limits.size.w = r.size.w + r.pos.x - at.x; 1513 at.x += draw_beamformer_variable(ui, ui->arena, var, at, mouse, 1514 FG_COLOUR, text_spec).x; 1515 1516 while (var) { 1517 if (var->next) { 1518 var = var->next; 1519 break; 1520 } 1521 var = var->parent; 1522 r.pos.x -= LISTING_INDENT; 1523 r.size.x += LISTING_INDENT; 1524 x_off += LISTING_INDENT; 1525 } 1526 } break; 1527 case VT_GROUP: { 1528 VariableGroup *g = &var->u.group; 1529 1530 text_spec.limits.size.w = r.size.w + r.pos.x - at.x; 1531 advance = draw_beamformer_variable(ui, ui->arena, var, at, mouse, 1532 FG_COLOUR, text_spec).y; 1533 at.x += x_off + LISTING_ITEM_PAD; 1534 if (g->expanded) { 1535 cut_rect_horizontal(r, LISTING_INDENT, 0, &r); 1536 x_off -= LISTING_INDENT; 1537 var = g->first; 1538 } else { 1539 Variable *v = g->first; 1540 1541 ASSERT(!v || v->type == VT_BEAMFORMER_VARIABLE); 1542 /* NOTE(rnp): assume the suffix is the same for all elements */ 1543 if (v) suffix = v->u.beamformer_variable.suffix; 1544 1545 switch (g->type) { 1546 case VG_LIST: break; 1547 case VG_V2: 1548 case VG_V4: { 1549 text_spec.limits.size.w = r.size.w + r.pos.x - at.x; 1550 at.x += draw_text(s8("{"), at, &text_spec).x; 1551 while (v) { 1552 text_spec.limits.size.w = r.size.w + r.pos.x - at.x; 1553 at.x += draw_beamformer_variable(ui, ui->arena, v, 1554 at, mouse, FG_COLOUR, text_spec).x; 1555 1556 v = v->next; 1557 if (v) { 1558 text_spec.limits.size.w = r.size.w + r.pos.x - at.x; 1559 at.x += draw_text(s8(", "), at, &text_spec).x; 1560 } 1561 } 1562 text_spec.limits.size.w = r.size.w + r.pos.x - at.x; 1563 at.x += draw_text(s8("}"), at, &text_spec).x; 1564 } break; 1565 } 1566 1567 var = var->next; 1568 } 1569 } break; 1570 case VT_BEAMFORMER_FRAME_VIEW: { 1571 BeamformerFrameView *bv = var->u.generic; 1572 if (frame_view_ready_to_present(bv)) 1573 draw_beamformer_frame_view(ui, ui->arena, var, r, mouse); 1574 var = var->next; 1575 } break; 1576 case VT_COMPUTE_PROGRESS_BAR: { 1577 ComputeProgressBar *bar = &var->u.compute_progress_bar; 1578 advance = draw_compute_progress_bar(ui, ui->arena, bar, r).y; 1579 var = var->next; 1580 } break; 1581 case VT_COMPUTE_LATEST_STATS_VIEW: 1582 case VT_COMPUTE_STATS_VIEW: { 1583 ComputeShaderStats *stats = var->u.compute_stats_view.stats; 1584 if (var->type == VT_COMPUTE_LATEST_STATS_VIEW) 1585 stats = *(ComputeShaderStats **)stats; 1586 advance = draw_compute_stats_view(var->u.compute_stats_view.ctx, ui->arena, stats, r).y; 1587 var = var->next; 1588 } break; 1589 default: INVALID_CODE_PATH; 1590 } 1591 1592 if (suffix.len) { 1593 v2 suffix_s = measure_text(ui->font, suffix); 1594 if (r.size.w + r.pos.x - LISTING_ITEM_PAD - suffix_s.x > at.x) { 1595 v2 suffix_p = {.x = r.pos.x + r.size.w - suffix_s.w, .y = r.pos.y}; 1596 draw_text(suffix, suffix_p, &text_spec); 1597 } 1598 } 1599 1600 /* NOTE(rnp): we want to let this overflow to the desired size */ 1601 r.pos.y += advance + LISTING_LINE_PAD; 1602 r.size.y -= advance + LISTING_LINE_PAD; 1603 } 1604 view->needed_height = start_height - r.size.h; 1605 } 1606 1607 static void 1608 draw_active_text_box(BeamformerUI *ui, Variable *var) 1609 { 1610 InputState *is = &ui->text_input_state; 1611 Rect box = is->rect; 1612 1613 s8 text = {.len = is->buf_len, .data = is->buf}; 1614 v2 text_size = measure_text(*is->font, text); 1615 v2 text_position = {.x = box.pos.x, .y = box.pos.y + (box.size.h - text_size.h) / 2}; 1616 1617 f32 cursor_width = (is->cursor == is->buf_len) ? 16 : 4; 1618 f32 cursor_offset = measure_text(*is->font, (s8){.data = text.data, .len = is->cursor}).w; 1619 cursor_offset += text_position.x; 1620 1621 box.size.w = MAX(box.size.w, text_size.w + cursor_width); 1622 Rect background = extend_rect_centered(box, (v2){.x = 12, .y = 8}); 1623 box = extend_rect_centered(box, (v2){.x = 8, .y = 4}); 1624 1625 Rect cursor = { 1626 .pos = {.x = cursor_offset, .y = text_position.y}, 1627 .size = {.w = cursor_width, .h = text_size.h}, 1628 }; 1629 1630 v4 cursor_colour = FOCUSED_COLOUR; 1631 cursor_colour.a = CLAMP01(is->cursor_blink_t); 1632 1633 Color c = colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, var->hover_t)); 1634 TextSpec text_spec = {.font = is->font, .colour = c}; 1635 1636 DrawRectangleRounded(background.rl, 0.2, 0, fade(BLACK, 0.8)); 1637 DrawRectangleRounded(box.rl, 0.2, 0, colour_from_normalized(BG_COLOUR)); 1638 draw_text(text, text_position, &text_spec); 1639 DrawRectanglePro(cursor.rl, (Vector2){0}, 0, colour_from_normalized(cursor_colour)); 1640 } 1641 1642 static void 1643 draw_active_menu(BeamformerUI *ui, Arena arena, Variable *menu, v2 mouse, Rect window) 1644 { 1645 ASSERT(menu->type == VT_GROUP); 1646 MenuState *ms = &ui->menu_state; 1647 1648 f32 max_label_width = 0; 1649 1650 Variable *item = menu->u.group.first; 1651 u32 item_count = 0; 1652 while (item) { 1653 max_label_width = MAX(max_label_width, item->name_width); 1654 item = item->next; 1655 item_count++; 1656 } 1657 1658 v2 at = ms->at; 1659 f32 menu_width = max_label_width + 8; 1660 f32 menu_height = item_count * ms->font->baseSize + (item_count) * 2; 1661 1662 if (at.x + menu_width > window.size.w) 1663 at.x = window.size.w - menu_width - 16; 1664 if (at.y + menu_height > window.size.h) 1665 at.y = window.size.h - menu_height - 12; 1666 /* TODO(rnp): scroll menu if it doesn't fit on screen */ 1667 1668 Rect menu_rect = {.pos = at, .size = {.w = menu_width, .h = menu_height}}; 1669 Rect bg_rect = extend_rect_centered(menu_rect, (v2){.x = 12, .y = 8}); 1670 menu_rect = extend_rect_centered(menu_rect, (v2){.x = 6, .y = 4}); 1671 DrawRectangleRounded(bg_rect.rl, 0.1, 0, fade(BLACK, 0.8)); 1672 DrawRectangleRounded(menu_rect.rl, 0.1, 0, colour_from_normalized(BG_COLOUR)); 1673 1674 /* TODO(rnp): last element has too much vertical space */ 1675 item = menu->u.group.first; 1676 TextSpec text_spec = {.font = &ui->small_font, .colour = NORMALIZED_FG_COLOUR, 1677 .limits.size.w = menu_width}; 1678 while (item) { 1679 at.y += draw_beamformer_variable(ui, arena, item, at, mouse, FG_COLOUR, text_spec).y; 1680 item = item->next; 1681 if (item) { 1682 DrawLineEx((v2){.x = at.x - 3, .y = at.y}.rl, 1683 add_v2(at, (v2){.w = menu_width + 3}).rl, 2, fade(BLACK, 0.8)); 1684 at.y += 2; 1685 } 1686 } 1687 } 1688 1689 static void 1690 draw_variable(BeamformerUI *ui, Variable *var, Rect draw_rect, v2 mouse) 1691 { 1692 if (var->type != VT_UI_REGION_SPLIT) { 1693 v2 shrink = {.x = UI_REGION_PAD, .y = UI_REGION_PAD}; 1694 draw_rect = shrink_rect_centered(draw_rect, shrink); 1695 BeginScissorMode(draw_rect.pos.x, draw_rect.pos.y, draw_rect.size.w, draw_rect.size.h); 1696 draw_rect = draw_title_bar(ui, ui->arena, var, draw_rect, mouse); 1697 EndScissorMode(); 1698 } 1699 1700 /* TODO(rnp): post order traversal of the ui tree will remove the need for this */ 1701 if (!CheckCollisionPointRec(mouse.rl, draw_rect.rl)) 1702 mouse = (v2){.x = F32_INFINITY, .y = F32_INFINITY}; 1703 1704 BeginScissorMode(draw_rect.pos.x, draw_rect.pos.y, draw_rect.size.w, draw_rect.size.h); 1705 switch (var->type) { 1706 case VT_UI_VIEW: { 1707 draw_ui_view(ui, var, draw_rect, mouse); 1708 } break; 1709 case VT_UI_REGION_SPLIT: { 1710 RegionSplit *rs = &var->u.region_split; 1711 1712 Rect split, hover; 1713 switch (rs->direction) { 1714 case RSD_VERTICAL: { 1715 split_rect_vertical(draw_rect, rs->fraction, 0, &split); 1716 split.pos.x += UI_REGION_PAD; 1717 split.pos.y -= UI_SPLIT_HANDLE_THICK / 2; 1718 split.size.h = UI_SPLIT_HANDLE_THICK; 1719 split.size.w -= 2 * UI_REGION_PAD; 1720 hover = extend_rect_centered(split, (v2){.y = 0.75 * UI_REGION_PAD}); 1721 } break; 1722 case RSD_HORIZONTAL: { 1723 split_rect_horizontal(draw_rect, rs->fraction, 0, &split); 1724 split.pos.x -= UI_SPLIT_HANDLE_THICK / 2; 1725 split.pos.y += UI_REGION_PAD; 1726 split.size.w = UI_SPLIT_HANDLE_THICK; 1727 split.size.h -= 2 * UI_REGION_PAD; 1728 hover = extend_rect_centered(split, (v2){.x = 0.75 * UI_REGION_PAD}); 1729 } break; 1730 } 1731 1732 hover_var(ui, mouse, hover, var); 1733 1734 v4 colour = HOVERED_COLOUR; 1735 colour.a = var->hover_t; 1736 DrawRectangleRounded(split.rl, 0.6, 0, colour_from_normalized(colour)); 1737 } break; 1738 default: break; 1739 } 1740 EndScissorMode(); 1741 } 1742 1743 static void 1744 draw_ui_regions(BeamformerUI *ui, Rect window, v2 mouse) 1745 { 1746 struct region_stack_item { 1747 Variable *var; 1748 Rect rect; 1749 } *region_stack; 1750 1751 TempArena arena_savepoint = begin_temp_arena(&ui->arena); 1752 i32 stack_index = 0; 1753 1754 region_stack = alloc(&ui->arena, typeof(*region_stack), 256); 1755 region_stack[0].var = ui->regions; 1756 region_stack[0].rect = window; 1757 1758 while (stack_index != -1) { 1759 struct region_stack_item *rsi = region_stack + stack_index--; 1760 Rect rect = rsi->rect; 1761 draw_variable(ui, rsi->var, rect, mouse); 1762 1763 if (rsi->var->type == VT_UI_REGION_SPLIT) { 1764 Rect first, second; 1765 RegionSplit *rs = &rsi->var->u.region_split; 1766 switch (rs->direction) { 1767 case RSD_VERTICAL: { 1768 split_rect_vertical(rect, rs->fraction, &first, &second); 1769 } break; 1770 case RSD_HORIZONTAL: { 1771 split_rect_horizontal(rect, rs->fraction, &first, &second); 1772 } break; 1773 } 1774 1775 stack_index++; 1776 region_stack[stack_index].var = rs->right; 1777 region_stack[stack_index].rect = second; 1778 stack_index++; 1779 region_stack[stack_index].var = rs->left; 1780 region_stack[stack_index].rect = first; 1781 } 1782 1783 ASSERT(stack_index < 256); 1784 } 1785 1786 end_temp_arena(arena_savepoint); 1787 } 1788 1789 static void 1790 ui_store_variable_base(VariableType type, void *store, void *new_value, void *limits) 1791 { 1792 switch (type) { 1793 case VT_B32: { 1794 *(b32 *)store = *(b32 *)new_value; 1795 } break; 1796 case VT_F32: { 1797 v2 *lim = limits; 1798 f32 val = *(f32 *)new_value; 1799 *(f32 *)store = CLAMP(val, lim->x, lim->y); 1800 } break; 1801 default: INVALID_CODE_PATH; 1802 } 1803 } 1804 1805 static void 1806 ui_store_variable(Variable *var, void *new_value) 1807 { 1808 switch (var->type) { 1809 case VT_F32: { 1810 v2 limits = {.x = -F32_INFINITY, .y = F32_INFINITY}; 1811 ui_store_variable_base(VT_F32, &var->u.f32, new_value, &limits); 1812 } break; 1813 case VT_BEAMFORMER_VARIABLE: { 1814 BeamformerVariable *bv = &var->u.beamformer_variable; 1815 ui_store_variable_base(bv->store_type, bv->store, new_value, &bv->params.limits); 1816 } break; 1817 default: INVALID_CODE_PATH; 1818 } 1819 } 1820 1821 static void 1822 scroll_interaction_base(VariableType type, void *store, f32 delta) 1823 { 1824 switch (type) { 1825 case VT_B32: { *(b32 *)store = !*(b32 *)store; } break; 1826 case VT_F32: { *(f32 *)store += delta; } break; 1827 case VT_I32: { *(i32 *)store += delta; } break; 1828 default: INVALID_CODE_PATH; 1829 } 1830 } 1831 1832 static void 1833 scroll_interaction(Variable *var, f32 delta) 1834 { 1835 switch (var->type) { 1836 case VT_BEAMFORMER_VARIABLE: { 1837 BeamformerVariable *bv = &var->u.beamformer_variable; 1838 scroll_interaction_base(bv->store_type, bv->store, delta * bv->params.scroll_scale); 1839 ui_store_variable(var, bv->store); 1840 } break; 1841 case VT_UI_VIEW: { 1842 scroll_interaction_base(VT_F32, &var->u.view.offset, UI_SCROLL_SPEED * delta); 1843 var->u.view.offset = MAX(0, var->u.view.offset); 1844 } break; 1845 default: scroll_interaction_base(var->type, &var->u, delta); break; 1846 } 1847 } 1848 1849 static void 1850 begin_menu_input(MenuState *ms, v2 mouse) 1851 { 1852 ms->at = mouse; 1853 ms->font = ms->hot_font; 1854 } 1855 1856 static void 1857 begin_text_input(InputState *is, Variable *var, v2 mouse) 1858 { 1859 Stream s = {.cap = ARRAY_COUNT(is->buf), .data = is->buf}; 1860 stream_append_variable(&s, var); 1861 is->buf_len = s.widx; 1862 is->rect = is->hot_rect; 1863 is->font = is->hot_font; 1864 1865 /* NOTE: extra offset to help with putting a cursor at idx 0 */ 1866 #define TEXT_HALF_CHAR_WIDTH 10 1867 f32 hover_p = CLAMP01((mouse.x - is->rect.pos.x) / is->rect.size.w); 1868 f32 x_off = TEXT_HALF_CHAR_WIDTH, x_bounds = is->rect.size.w * hover_p; 1869 i32 i; 1870 for (i = 0; i < is->buf_len && x_off < x_bounds; i++) { 1871 /* NOTE: assumes font glyphs are ordered ASCII */ 1872 i32 idx = is->buf[i] - 0x20; 1873 x_off += is->font->glyphs[idx].advanceX; 1874 if (is->font->glyphs[idx].advanceX == 0) 1875 x_off += is->font->recs[idx].width; 1876 } 1877 is->cursor = i; 1878 } 1879 1880 static void 1881 end_text_input(InputState *is, Variable *var) 1882 { 1883 f32 scale = 1; 1884 if (var->type == VT_BEAMFORMER_VARIABLE) { 1885 BeamformerVariable *bv = &var->u.beamformer_variable; 1886 ASSERT(bv->store_type == VT_F32); 1887 scale = bv->params.display_scale; 1888 var->hover_t = 0; 1889 } 1890 f32 value = parse_f64((s8){.len = is->buf_len, .data = is->buf}) / scale; 1891 ui_store_variable(var, &value); 1892 } 1893 1894 static void 1895 update_text_input(InputState *is, Variable *var) 1896 { 1897 ASSERT(is->cursor != -1); 1898 1899 is->cursor_blink_t += is->cursor_blink_scale * dt_for_frame; 1900 if (is->cursor_blink_t >= 1) is->cursor_blink_scale = -1.5f; 1901 if (is->cursor_blink_t <= 0) is->cursor_blink_scale = 1.5f; 1902 1903 var->hover_t -= 2 * HOVER_SPEED * dt_for_frame; 1904 var->hover_t = CLAMP01(var->hover_t); 1905 1906 /* NOTE: handle multiple input keys on a single frame */ 1907 i32 key = GetCharPressed(); 1908 while (key > 0) { 1909 if ((is->buf_len == ARRAY_COUNT(is->buf)) || (is->cursor == ARRAY_COUNT(is->buf) - 1)) 1910 break; 1911 1912 b32 allow_key = ((key >= '0' && key <= '9') || (key == '.') || 1913 (key == '-' && is->cursor == 0)); 1914 if (allow_key) { 1915 mem_move(is->buf + is->cursor + 1, 1916 is->buf + is->cursor, 1917 is->buf_len - is->cursor + 1); 1918 1919 is->buf[is->cursor++] = key; 1920 is->buf_len++; 1921 } 1922 key = GetCharPressed(); 1923 } 1924 1925 if ((IsKeyPressed(KEY_LEFT) || IsKeyPressedRepeat(KEY_LEFT)) && is->cursor > 0) 1926 is->cursor--; 1927 1928 if ((IsKeyPressed(KEY_RIGHT) || IsKeyPressedRepeat(KEY_RIGHT)) && is->cursor < is->buf_len) 1929 is->cursor++; 1930 1931 if ((IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE)) && is->cursor > 0) { 1932 is->cursor--; 1933 if (is->cursor < ARRAY_COUNT(is->buf) - 1) { 1934 mem_move(is->buf + is->cursor, 1935 is->buf + is->cursor + 1, 1936 is->buf_len - is->cursor); 1937 } 1938 is->buf_len--; 1939 } 1940 1941 if ((IsKeyPressed(KEY_DELETE) || IsKeyPressedRepeat(KEY_DELETE)) && is->cursor < is->buf_len) { 1942 mem_move(is->buf + is->cursor, 1943 is->buf + is->cursor + 1, 1944 is->buf_len - is->cursor); 1945 is->buf_len--; 1946 } 1947 } 1948 1949 static void 1950 display_interaction_end(BeamformerUI *ui) 1951 { 1952 b32 is_hot = ui->interaction.hot_type == IT_DISPLAY; 1953 b32 is_active = ui->interaction.type == IT_DISPLAY; 1954 if ((is_active && is_hot) || ui->ruler_state == RS_HOLD) 1955 return; 1956 ui->ruler_state = RS_NONE; 1957 } 1958 1959 static void 1960 display_interaction(BeamformerUI *ui, v2 mouse, f32 scroll) 1961 { 1962 b32 mouse_left_pressed = IsMouseButtonPressed(MOUSE_BUTTON_LEFT); 1963 b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT); 1964 b32 is_hot = ui->interaction.hot_type == IT_DISPLAY; 1965 b32 is_active = ui->interaction.type == IT_DISPLAY; 1966 1967 if (scroll) { 1968 ASSERT(ui->interaction.active->type == VT_BEAMFORMER_FRAME_VIEW); 1969 BeamformerFrameView *bv = ui->interaction.active->u.generic; 1970 bv->threshold.u.f32 += scroll; 1971 bv->needs_update = 1; 1972 } 1973 1974 if (mouse_left_pressed && is_active) { 1975 ui->ruler_state++; 1976 switch (ui->ruler_state) { 1977 case RS_START: ui->ruler_start_p = mouse; break; 1978 case RS_HOLD: ui->ruler_stop_p = mouse; break; 1979 default: 1980 ui->ruler_state = RS_NONE; 1981 break; 1982 } 1983 } else if (mouse_right_pressed && is_hot) { 1984 ui->ruler_state = RS_NONE; 1985 } 1986 } 1987 1988 static void 1989 scale_bar_interaction(BeamformerCtx *ctx, v2 mouse) 1990 { 1991 BeamformerUI *ui = ctx->ui; 1992 InteractionState *is = &ui->interaction; 1993 ScaleBar *sb = is->active->u.generic; 1994 b32 mouse_left_pressed = IsMouseButtonPressed(MOUSE_BUTTON_LEFT); 1995 b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT); 1996 f32 mouse_wheel = GetMouseWheelMoveV().y; 1997 1998 if (mouse_left_pressed) { 1999 if (sb->zoom_starting_point.x == F32_INFINITY) { 2000 sb->zoom_starting_point = sub_v2(mouse, sb->screen_offset); 2001 } else { 2002 v2 relative_mouse = sub_v2(mouse, sb->screen_offset); 2003 f32 min = magnitude_v2(mul_v2(sb->zoom_starting_point, sb->screen_space_to_value)); 2004 f32 max = magnitude_v2(mul_v2(relative_mouse, sb->screen_space_to_value)); 2005 if (min > max) { f32 tmp = min; min = max; max = tmp; } 2006 2007 min += *sb->min_value; 2008 max += *sb->min_value; 2009 2010 v2_sll *savepoint = SLLPop(ui->scale_bar_savepoint_freelist); 2011 if (!savepoint) savepoint = push_struct(&ui->arena, v2_sll); 2012 2013 savepoint->v.x = *sb->min_value; 2014 savepoint->v.y = *sb->max_value; 2015 SLLPush(savepoint, sb->savepoint_stack); 2016 2017 *sb->min_value = MAX(min, sb->limits.x); 2018 *sb->max_value = MIN(max, sb->limits.y); 2019 2020 sb->zoom_starting_point = (v2){.x = F32_INFINITY, .y = F32_INFINITY}; 2021 if (sb->causes_compute) 2022 ui->flush_params = 1; 2023 } 2024 } 2025 2026 if (mouse_right_pressed) { 2027 v2_sll *savepoint = sb->savepoint_stack; 2028 if (savepoint) { 2029 if (sb->causes_compute) 2030 ui->flush_params = 1; 2031 *sb->min_value = savepoint->v.x; 2032 *sb->max_value = savepoint->v.y; 2033 sb->savepoint_stack = SLLPush(savepoint, ui->scale_bar_savepoint_freelist); 2034 } 2035 sb->zoom_starting_point = (v2){.x = F32_INFINITY, .y = F32_INFINITY}; 2036 } 2037 2038 if (mouse_wheel) { 2039 *sb->min_value += mouse_wheel * sb->scroll_scale.x; 2040 *sb->max_value += mouse_wheel * sb->scroll_scale.y; 2041 *sb->min_value = MAX(sb->limits.x, *sb->min_value); 2042 *sb->max_value = MIN(sb->limits.y, *sb->max_value); 2043 if (sb->causes_compute) 2044 ui->flush_params = 1; 2045 } 2046 } 2047 2048 static void 2049 ui_button_interaction(BeamformerUI *ui, Variable *button) 2050 { 2051 ASSERT(button->type == VT_UI_BUTTON); 2052 switch (button->u.button) { 2053 case UI_BID_FV_COPY_HORIZONTAL: ui_copy_frame(ui, button, RSD_HORIZONTAL); break; 2054 case UI_BID_FV_COPY_VERTICAL: ui_copy_frame(ui, button, RSD_VERTICAL); break; 2055 case UI_BID_CLOSE_VIEW: { 2056 Variable *view = button->parent; 2057 Variable *region = view->parent; 2058 ASSERT(view->type == VT_UI_VIEW && region->type == VT_UI_REGION_SPLIT); 2059 2060 Variable *parent = region->parent; 2061 Variable *remaining = region->u.region_split.left; 2062 if (remaining == view) remaining = region->u.region_split.right; 2063 2064 ui_view_free(ui, view); 2065 2066 ASSERT(parent->type == VT_UI_REGION_SPLIT); 2067 if (parent->u.region_split.left == region) { 2068 parent->u.region_split.left = remaining; 2069 } else { 2070 parent->u.region_split.right = remaining; 2071 } 2072 remaining->parent = parent; 2073 2074 SLLPush(region, ui->variable_freelist); 2075 } break; 2076 } 2077 } 2078 2079 static void 2080 ui_begin_interact(BeamformerUI *ui, BeamformerInput *input, b32 scroll, b32 mouse_left_pressed) 2081 { 2082 InteractionState *is = &ui->interaction; 2083 if (is->hot_type != IT_NONE) { 2084 is->type = is->hot_type; 2085 } else if (is->hot) { 2086 switch (is->hot->type) { 2087 case VT_NULL: is->type = IT_NOP; break; 2088 case VT_B32: is->type = IT_SET; break; 2089 case VT_UI_REGION_SPLIT: { is->type = IT_DRAG; } break; 2090 case VT_UI_VIEW: { if (scroll) is->type = IT_SCROLL; } break; 2091 case VT_UI_BUTTON: { ui_button_interaction(ui, is->hot); } break; 2092 case VT_GROUP: { 2093 if (mouse_left_pressed && is->hot->flags & V_MENU) { 2094 is->type = IT_MENU; 2095 begin_menu_input(&ui->menu_state, input->mouse); 2096 } else { 2097 is->type = IT_SET; 2098 } 2099 } break; 2100 case VT_BEAMFORMER_VARIABLE: { 2101 if (is->hot->u.beamformer_variable.store_type == VT_B32) { 2102 is->type = IT_SET; 2103 break; 2104 } 2105 } /* FALLTHROUGH */ 2106 case VT_F32: { 2107 if (scroll) { 2108 is->type = IT_SCROLL; 2109 } else if (mouse_left_pressed && is->hot->flags & V_TEXT) { 2110 is->type = IT_TEXT; 2111 begin_text_input(&ui->text_input_state, is->hot, input->mouse); 2112 } 2113 } break; 2114 default: INVALID_CODE_PATH; 2115 } 2116 } 2117 if (is->type != IT_NONE) { 2118 is->active = is->hot; 2119 if ((iptr)is->hot == (iptr)ui->scratch_variables) 2120 ui->scratch_variable = ui->scratch_variables + 1; 2121 else 2122 ui->scratch_variable = ui->scratch_variables + 0; 2123 } 2124 } 2125 2126 static void 2127 ui_end_interact(BeamformerCtx *ctx, v2 mouse) 2128 { 2129 BeamformerUI *ui = ctx->ui; 2130 InteractionState *is = &ui->interaction; 2131 switch (is->type) { 2132 case IT_NOP: break; 2133 case IT_SET: { 2134 switch (is->active->type) { 2135 case VT_GROUP: { 2136 is->active->u.group.expanded = !is->active->u.group.expanded; 2137 } break; 2138 case VT_B32: { 2139 is->active->u.b32 = !is->active->u.b32; 2140 } break; 2141 case VT_BEAMFORMER_VARIABLE: { 2142 ASSERT(is->active->u.beamformer_variable.store_type == VT_B32); 2143 b32 *val = is->active->u.beamformer_variable.store; 2144 *val = !(*val); 2145 } break; 2146 default: INVALID_CODE_PATH; 2147 } 2148 } break; 2149 case IT_DISPLAY: display_interaction_end(ui); break; 2150 case IT_SCROLL: scroll_interaction(is->active, GetMouseWheelMoveV().y); break; 2151 case IT_TEXT: end_text_input(&ui->text_input_state, is->active); break; 2152 case IT_MENU: break; 2153 case IT_SCALE_BAR: break; 2154 case IT_DRAG: break; 2155 default: INVALID_CODE_PATH; 2156 } 2157 2158 if (is->active->flags & V_CAUSES_COMPUTE) 2159 ui->flush_params = 1; 2160 if (is->active->flags & V_UPDATE_VIEW) { 2161 Variable *frame_view = is->active->parent; 2162 ASSERT(frame_view && frame_view->type == VT_BEAMFORMER_FRAME_VIEW); 2163 ((BeamformerFrameView *)frame_view->u.generic)->needs_update = 1; 2164 } 2165 2166 is->type = IT_NONE; 2167 is->active = 0; 2168 } 2169 2170 static void 2171 ui_interact(BeamformerCtx *ctx, BeamformerInput *input) 2172 { 2173 BeamformerUI *ui = ctx->ui; 2174 InteractionState *is = &ui->interaction; 2175 b32 mouse_left_pressed = IsMouseButtonPressed(MOUSE_BUTTON_LEFT); 2176 b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT); 2177 b32 wheel_moved = GetMouseWheelMoveV().y != 0; 2178 if (mouse_right_pressed || mouse_left_pressed || wheel_moved) { 2179 if (is->type != IT_NONE) 2180 ui_end_interact(ctx, input->mouse); 2181 ui_begin_interact(ui, input, wheel_moved, mouse_left_pressed); 2182 } 2183 2184 if (IsKeyPressed(KEY_ENTER) && is->type == IT_TEXT) 2185 ui_end_interact(ctx, input->mouse); 2186 2187 switch (is->type) { 2188 case IT_NONE: break; 2189 case IT_NOP: break; 2190 case IT_MENU: break; 2191 case IT_DISPLAY: display_interaction(ui, input->mouse, GetMouseWheelMoveV().y); break; 2192 case IT_SCROLL: ui_end_interact(ctx, input->mouse); break; 2193 case IT_SET: ui_end_interact(ctx, input->mouse); break; 2194 case IT_TEXT: update_text_input(&ui->text_input_state, is->active); break; 2195 case IT_DRAG: { 2196 if (!IsMouseButtonDown(MOUSE_BUTTON_LEFT) && !IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) { 2197 ui_end_interact(ctx, input->mouse); 2198 } else { 2199 v2 ws = (v2){.w = ctx->window_size.w, .h = ctx->window_size.h}; 2200 v2 dMouse = sub_v2(input->mouse, input->last_mouse); 2201 dMouse = mul_v2(dMouse, (v2){.x = 1.0f / ws.w, .y = 1.0f / ws.h}); 2202 2203 switch (is->active->type) { 2204 case VT_UI_REGION_SPLIT: { 2205 f32 min_fraction; 2206 RegionSplit *rs = &is->active->u.region_split; 2207 switch (rs->direction) { 2208 case RSD_VERTICAL: { 2209 min_fraction = (UI_SPLIT_HANDLE_THICK + 0.5 * UI_REGION_PAD) / ws.h; 2210 rs->fraction += dMouse.y; 2211 } break; 2212 case RSD_HORIZONTAL: { 2213 min_fraction = (UI_SPLIT_HANDLE_THICK + 0.5 * UI_REGION_PAD) / ws.w; 2214 rs->fraction += dMouse.x; 2215 } break; 2216 } 2217 rs->fraction = CLAMP(rs->fraction, min_fraction, 1 - min_fraction); 2218 } break; 2219 default: break; 2220 } 2221 2222 if (is->active != is->hot) { 2223 is->active->hover_t += HOVER_SPEED * dt_for_frame; 2224 is->active->hover_t = CLAMP01(is->active->hover_t); 2225 } 2226 } 2227 } break; 2228 case IT_SCALE_BAR: scale_bar_interaction(ctx, input->mouse); break; 2229 } 2230 2231 is->hot_type = IT_NONE; 2232 is->hot = 0; 2233 } 2234 2235 static void 2236 ui_init(BeamformerCtx *ctx, Arena store) 2237 { 2238 /* NOTE(rnp): store the ui at the base of the passed in arena and use the rest for 2239 * temporary allocations within the ui. If needed we can recall this function to 2240 * completely clear the ui state. The is that if we store pointers to static data 2241 * such as embedded font data we will need to reset them when the executable reloads. 2242 * We could also build some sort of ui structure here and store it then iterate over 2243 * it to actually draw the ui. If we reload we may have changed it so we should 2244 * rebuild it */ 2245 2246 BeamformerUI *ui = ctx->ui; 2247 2248 /* NOTE(rnp): unload old data from GPU */ 2249 if (ui) { 2250 UnloadFont(ui->font); 2251 UnloadFont(ui->small_font); 2252 2253 for (BeamformerFrameView *view = ui->views; view; view = view->next) 2254 if (view->rendered_view.id) 2255 UnloadRenderTexture(view->rendered_view); 2256 } 2257 2258 ui = ctx->ui = push_struct(&store, typeof(*ui)); 2259 ui->os = &ctx->os; 2260 ui->arena = store; 2261 2262 /* TODO: build these into the binary */ 2263 /* TODO(rnp): better font, this one is jank at small sizes */ 2264 ui->font = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 28, 0, 0); 2265 ui->small_font = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 20, 0, 0); 2266 2267 ui->scratch_variable = ui->scratch_variables + 0; 2268 Variable *split = ui->regions = add_ui_split(ui, 0, &ui->arena, s8("UI Root"), 0.4, 2269 RSD_HORIZONTAL, ui->font); 2270 split->u.region_split.left = add_ui_split(ui, split, &ui->arena, s8(""), 0.475, 2271 RSD_VERTICAL, ui->font); 2272 split->u.region_split.right = add_beamformer_frame_view(ui, split, &ui->arena, FVT_LATEST, 0); 2273 2274 BeamformerFrameView *bv = split->u.region_split.right->u.group.first->u.generic; 2275 bv->lateral_scale_bar.min_value = &ui->params.output_min_coordinate.x; 2276 bv->lateral_scale_bar.max_value = &ui->params.output_max_coordinate.x; 2277 bv->axial_scale_bar.min_value = &ui->params.output_min_coordinate.z; 2278 bv->axial_scale_bar.max_value = &ui->params.output_max_coordinate.z; 2279 bv->axial_scale_bar.causes_compute = 1; 2280 bv->lateral_scale_bar.causes_compute = 1; 2281 bv->ctx = &ctx->fsctx; 2282 2283 split = split->u.region_split.left; 2284 split->u.region_split.left = add_beamformer_parameters_view(split, ctx); 2285 split->u.region_split.right = add_ui_split(ui, split, &ui->arena, s8(""), 0.22, 2286 RSD_VERTICAL, ui->font); 2287 split = split->u.region_split.right; 2288 2289 split->u.region_split.left = add_compute_progress_bar(split, ctx); 2290 split->u.region_split.right = add_compute_stats_view(ui, split, &ui->arena, 2291 VT_COMPUTE_LATEST_STATS_VIEW); 2292 2293 ComputeStatsView *compute_stats = &split->u.region_split.right->u.group.first->u.compute_stats_view; 2294 compute_stats->ctx = ctx; 2295 compute_stats->stats = &ui->latest_compute_stats; 2296 2297 ctx->ui_read_params = 1; 2298 } 2299 2300 static void 2301 validate_ui_parameters(BeamformerUIParameters *p) 2302 { 2303 if (p->output_min_coordinate.x > p->output_max_coordinate.x) 2304 SWAP(p->output_min_coordinate.x, p->output_max_coordinate.x) 2305 if (p->output_min_coordinate.z > p->output_max_coordinate.z) 2306 SWAP(p->output_min_coordinate.z, p->output_max_coordinate.z) 2307 } 2308 2309 static void 2310 draw_ui(BeamformerCtx *ctx, BeamformerInput *input, BeamformFrame *frame_to_draw, 2311 ComputeShaderStats *latest_compute_stats) 2312 { 2313 BeamformerUI *ui = ctx->ui; 2314 2315 if (frame_to_draw->ready_to_present) ui->latest_frame = frame_to_draw; 2316 ui->latest_compute_stats = latest_compute_stats; 2317 2318 /* TODO(rnp): there should be a better way of detecting this */ 2319 if (ctx->ui_read_params) { 2320 mem_copy(&ui->params, &ctx->params->raw.output_min_coordinate, sizeof(ui->params)); 2321 ui->flush_params = 0; 2322 ctx->ui_read_params = 0; 2323 } 2324 2325 /* NOTE: process interactions first because the user interacted with 2326 * the ui that was presented last frame */ 2327 ui_interact(ctx, input); 2328 2329 if (ui->flush_params) { 2330 validate_ui_parameters(&ui->params); 2331 if (!ctx->csctx.processing_compute) { 2332 mem_copy(&ctx->params->raw.output_min_coordinate, &ui->params, sizeof(ui->params)); 2333 ui->flush_params = 0; 2334 ctx->params->upload = 1; 2335 ctx->start_compute = 1; 2336 } 2337 } 2338 2339 /* NOTE(rnp): can't render to a different framebuffer in the middle of BeginDrawing()... */ 2340 update_frame_views(ui); 2341 2342 BeginDrawing(); 2343 ClearBackground(colour_from_normalized(BG_COLOUR)); 2344 2345 v2 mouse = input->mouse; 2346 Rect window_rect = {.size = {.w = ctx->window_size.w, .h = ctx->window_size.h}}; 2347 2348 draw_ui_regions(ui, window_rect, mouse); 2349 if (ui->interaction.type == IT_TEXT) 2350 draw_active_text_box(ui, ui->interaction.active); 2351 if (ui->interaction.type == IT_MENU) 2352 draw_active_menu(ui, ui->arena, ui->interaction.active, mouse, window_rect); 2353 EndDrawing(); 2354 }