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