ui.c (29881B)
1 /* See LICENSE for license details. */ 2 static Color 3 colour_from_normalized(v4 rgba) 4 { 5 return (Color){.r = rgba.r * 255.0f, .g = rgba.g * 255.0f, 6 .b = rgba.b * 255.0f, .a = rgba.a * 255.0f}; 7 } 8 9 static Color 10 fade(Color a, f32 alpha) 11 { 12 a.a = (u8)((f32)a.a * alpha); 13 return a; 14 } 15 16 static f32 17 lerp(f32 a, f32 b, f32 t) 18 { 19 return a + t * (b - a); 20 } 21 22 static v4 23 lerp_v4(v4 a, v4 b, f32 t) 24 { 25 return (v4){ 26 .x = a.x + t * (b.x - a.x), 27 .y = a.y + t * (b.y - a.y), 28 .z = a.z + t * (b.z - a.z), 29 .w = a.w + t * (b.w - a.w), 30 }; 31 } 32 33 static v2 34 measure_text(Font font, s8 text) 35 { 36 v2 result = {.y = font.baseSize}; 37 for (size i = 0; i < text.len; i++) { 38 /* NOTE: assumes font glyphs are ordered ASCII */ 39 i32 idx = (i32)text.data[i] - 0x20; 40 result.x += font.glyphs[idx].advanceX; 41 if (font.glyphs[idx].advanceX == 0) 42 result.x += (font.recs[idx].width + font.glyphs[idx].offsetX); 43 } 44 return result; 45 } 46 47 static v2 48 draw_text(Font font, s8 text, v2 pos, f32 rotation, Color colour) 49 { 50 rlPushMatrix(); 51 52 rlTranslatef(pos.x, pos.y, 0); 53 rlRotatef(rotation, 0, 0, 1); 54 55 v2 off = {0}; 56 for (size i = 0; i < text.len; i++) { 57 /* NOTE: assumes font glyphs are ordered ASCII */ 58 i32 idx = text.data[i] - 0x20; 59 Rectangle dst = { 60 off.x + font.glyphs[idx].offsetX - font.glyphPadding, 61 off.y + font.glyphs[idx].offsetY - font.glyphPadding, 62 font.recs[idx].width + 2.0f * font.glyphPadding, 63 font.recs[idx].height + 2.0f * font.glyphPadding 64 }; 65 Rectangle src = { 66 font.recs[idx].x - font.glyphPadding, 67 font.recs[idx].y - font.glyphPadding, 68 font.recs[idx].width + 2.0f * font.glyphPadding, 69 font.recs[idx].height + 2.0f * font.glyphPadding 70 }; 71 DrawTexturePro(font.texture, src, dst, (Vector2){0}, 0, colour); 72 73 off.x += font.glyphs[idx].advanceX; 74 if (font.glyphs[idx].advanceX == 0) 75 off.x += font.recs[idx].width; 76 } 77 rlPopMatrix(); 78 v2 result = {.x = off.x, .y = font.baseSize}; 79 return result; 80 } 81 82 static Rect 83 scale_rect_centered(Rect r, v2 scale) 84 { 85 Rect or = r; 86 r.size.w *= scale.x; 87 r.size.h *= scale.y; 88 r.pos.x += (or.size.w - r.size.w) / 2; 89 r.pos.y += (or.size.h - r.size.h) / 2; 90 return r; 91 } 92 93 static v2 94 center_align_text_in_rect(Rect r, s8 text, Font font) 95 { 96 v2 ts = measure_text(font, text); 97 v2 delta = { .w = r.size.w - ts.w, .h = r.size.h - ts.h }; 98 return (v2) { 99 .x = r.pos.x + 0.5 * delta.w, 100 .y = r.pos.y + 0.5 * delta.h, 101 }; 102 } 103 104 static b32 105 hover_text(v2 mouse, Rect text_rect, f32 *hover_t, b32 can_advance) 106 { 107 b32 hovering = CheckCollisionPointRec(mouse.rl, text_rect.rl); 108 if (hovering && can_advance) *hover_t += TEXT_HOVER_SPEED * dt_for_frame; 109 else *hover_t -= TEXT_HOVER_SPEED * dt_for_frame; 110 *hover_t = CLAMP01(*hover_t); 111 return hovering; 112 } 113 114 /* NOTE: This is kinda sucks no matter how you spin it. If we want the values to be 115 * left aligned in the center column we need to know the longest prefix length but 116 * without either hardcoding one of the prefixes as the longest one or measuring all 117 * of them we can't know this ahead of time. For now we hardcode this and manually 118 * adjust when needed */ 119 #define LISTING_LEFT_COLUMN_WIDTH 270.0f 120 #define LISTING_LINE_PAD 6.0f 121 122 static Rect 123 do_value_listing(s8 prefix, s8 suffix, Arena a, f32 value, Font font, Rect r) 124 { 125 v2 suffix_s = measure_text(font, suffix); 126 v2 suffix_p = {.x = r.pos.x + r.size.w - suffix_s.w, .y = r.pos.y}; 127 128 Stream buf = arena_stream(&a); 129 stream_append_f64(&buf, value, 100); 130 v2 txt_p = {.x = r.pos.x + LISTING_LEFT_COLUMN_WIDTH, .y = r.pos.y}; 131 132 draw_text(font, prefix, r.pos, 0, colour_from_normalized(FG_COLOUR)); 133 draw_text(font, stream_to_s8(&buf), txt_p, 0, colour_from_normalized(FG_COLOUR)); 134 draw_text(font, suffix, suffix_p, 0, colour_from_normalized(FG_COLOUR)); 135 r.pos.y += suffix_s.h + LISTING_LINE_PAD; 136 r.size.y -= suffix_s.h + LISTING_LINE_PAD; 137 138 return r; 139 } 140 141 static Rect 142 do_text_input_listing(s8 prefix, s8 suffix, Variable var, BeamformerUI *ui, Rect r, 143 v2 mouse, f32 *hover_t) 144 { 145 InputState *is = &ui->text_input_state; 146 b32 text_input_active = (ui->interaction.state == IS_TEXT) && 147 (var.store == ui->interaction.active.store); 148 149 Arena arena = ui->arena_for_frame; 150 Stream buf = arena_stream(&arena); 151 v2 txt_s; 152 153 if (text_input_active) { 154 txt_s = measure_text(ui->font, (s8){.len = is->buf_len, .data = is->buf}); 155 } else { 156 stream_append_variable(&buf, &var); 157 txt_s = measure_text(ui->font, stream_to_s8(&buf)); 158 } 159 160 Rect edit_rect = { 161 .pos = {.x = r.pos.x + LISTING_LEFT_COLUMN_WIDTH, .y = r.pos.y}, 162 .size = {.x = txt_s.w + TEXT_BOX_EXTRA_X, .y = txt_s.h} 163 }; 164 165 b32 hovering = hover_text(mouse, edit_rect, hover_t, !text_input_active); 166 if (hovering) 167 ui->interaction.hot = var; 168 169 /* TODO: where should this go? */ 170 if (text_input_active && is->cursor == -1) { 171 /* NOTE: extra offset to help with putting a cursor at idx 0 */ 172 #define TEXT_HALF_CHAR_WIDTH 10 173 f32 hover_p = CLAMP01((mouse.x - edit_rect.pos.x) / edit_rect.size.w); 174 f32 x_off = TEXT_HALF_CHAR_WIDTH, x_bounds = edit_rect.size.w * hover_p; 175 i32 i; 176 for (i = 0; i < is->buf_len && x_off < x_bounds; i++) { 177 /* NOTE: assumes font glyphs are ordered ASCII */ 178 i32 idx = is->buf[i] - 0x20; 179 x_off += ui->font.glyphs[idx].advanceX; 180 if (ui->font.glyphs[idx].advanceX == 0) 181 x_off += ui->font.recs[idx].width; 182 } 183 is->cursor = i; 184 } 185 186 Color colour = colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, *hover_t)); 187 188 if (text_input_active) { 189 s8 buf = {.len = is->buf_len, .data = is->buf}; 190 v2 ts = measure_text(ui->font, buf); 191 v2 pos = {.x = edit_rect.pos.x, .y = edit_rect.pos.y + (edit_rect.size.y - ts.y) / 2}; 192 193 #define MAX_DISP_CHARS 7 194 i32 buf_delta = is->buf_len - MAX_DISP_CHARS; 195 if (buf_delta < 0) buf_delta = 0; 196 buf.len -= buf_delta; 197 buf.data += buf_delta; 198 { 199 /* NOTE: drop a char if the subtext still doesn't fit */ 200 v2 nts = measure_text(ui->font, buf); 201 if (nts.w > 0.96 * edit_rect.size.w) { 202 buf.data++; 203 buf.len--; 204 } 205 } 206 draw_text(ui->font, buf, pos, 0, colour); 207 208 v4 bg = FOCUSED_COLOUR; 209 bg.a = 0; 210 Color cursor_colour = colour_from_normalized(lerp_v4(bg, FOCUSED_COLOUR, 211 CLAMP01(is->cursor_blink_t))); 212 buf.len = is->cursor - buf_delta; 213 v2 sts = measure_text(ui->font, buf); 214 f32 cursor_x = pos.x + sts.x; 215 f32 cursor_width; 216 if (is->cursor == is->buf_len) cursor_width = 20; 217 else cursor_width = 4; 218 Rect cursor_r = { 219 .pos = {.x = cursor_x, .y = pos.y}, 220 .size = {.w = cursor_width, .h = ts.h}, 221 }; 222 223 DrawRectanglePro(cursor_r.rl, (Vector2){0}, 0, cursor_colour); 224 } else { 225 draw_text(ui->font, stream_to_s8(&buf), edit_rect.pos, 0, colour); 226 } 227 228 v2 suffix_s = measure_text(ui->font, suffix); 229 v2 suffix_p = {.x = r.pos.x + r.size.w - suffix_s.w, .y = r.pos.y}; 230 draw_text(ui->font, prefix, r.pos, 0, colour_from_normalized(FG_COLOUR)); 231 draw_text(ui->font, suffix, suffix_p, 0, colour_from_normalized(FG_COLOUR)); 232 233 r.pos.y += suffix_s.h + LISTING_LINE_PAD; 234 r.size.y -= suffix_s.h + LISTING_LINE_PAD; 235 236 return r; 237 } 238 239 static Rect 240 do_text_toggle_listing(s8 prefix, s8 text0, s8 text1, Variable var, 241 BeamformerUI *ui, Rect r, v2 mouse, f32 *hover_t) 242 { 243 b32 toggle = *(b32 *)var.store; 244 v2 txt_s; 245 if (toggle) txt_s = measure_text(ui->font, text1); 246 else txt_s = measure_text(ui->font, text0); 247 248 Rect edit_rect = { 249 .pos = {.x = r.pos.x + LISTING_LEFT_COLUMN_WIDTH, .y = r.pos.y}, 250 .size = {.x = txt_s.w + TEXT_BOX_EXTRA_X, .y = txt_s.h} 251 }; 252 253 if (hover_text(mouse, edit_rect, hover_t, 1)) 254 ui->interaction.hot = var; 255 256 Color colour = colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, *hover_t)); 257 draw_text(ui->font, prefix, r.pos, 0, colour_from_normalized(FG_COLOUR)); 258 draw_text(ui->font, toggle? text1: text0, edit_rect.pos, 0, colour); 259 260 r.pos.y += txt_s.h + LISTING_LINE_PAD; 261 r.size.y -= txt_s.h + LISTING_LINE_PAD; 262 263 return r; 264 } 265 266 static b32 267 do_text_button(BeamformerUI *ui, s8 text, Rect r, v2 mouse, f32 *hover_t) 268 { 269 b32 hovering = hover_text(mouse, r, hover_t, 1); 270 b32 pressed = 0; 271 pressed |= (hovering && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)); 272 pressed |= (hovering && IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)); 273 274 f32 param = lerp(1, 1.04, *hover_t); 275 v2 bscale = (v2){ 276 .x = param + RECT_BTN_BORDER_WIDTH / r.size.w, 277 .y = param + RECT_BTN_BORDER_WIDTH / r.size.h, 278 }; 279 Rect sr = scale_rect_centered(r, (v2){.x = param, .y = param}); 280 Rect sb = scale_rect_centered(r, bscale); 281 DrawRectangleRounded(sb.rl, RECT_BTN_ROUNDNESS, 0, RECT_BTN_BORDER_COLOUR); 282 DrawRectangleRounded(sr.rl, RECT_BTN_ROUNDNESS, 0, RECT_BTN_COLOUR); 283 284 v2 tpos = center_align_text_in_rect(r, text, ui->font); 285 v2 spos = {.x = tpos.x + 1.75, .y = tpos.y + 2}; 286 v4 colour = lerp_v4(FG_COLOUR, HOVERED_COLOUR, *hover_t); 287 288 draw_text(ui->font, text, spos, 0, fade(BLACK, 0.8)); 289 draw_text(ui->font, text, tpos, 0, colour_from_normalized(colour)); 290 291 return pressed; 292 } 293 294 static void 295 draw_settings_ui(BeamformerCtx *ctx, Rect r, v2 mouse) 296 { 297 BeamformerUI *ui = ctx->ui; 298 BeamformerParameters *bp = &ctx->params->raw; 299 300 f32 minx = bp->output_min_coordinate.x + 1e-6, maxx = bp->output_max_coordinate.x - 1e-6; 301 f32 minz = bp->output_min_coordinate.z + 1e-6, maxz = bp->output_max_coordinate.z - 1e-6; 302 303 Rect draw_r = r; 304 draw_r.pos.y += 20; 305 draw_r.pos.x += 20; 306 draw_r.size.x -= 20; 307 draw_r.size.y -= 20; 308 309 draw_r = do_value_listing(s8("Sampling Frequency:"), s8("[MHz]"), ui->arena_for_frame, 310 bp->sampling_frequency * 1e-6, ui->font, draw_r); 311 312 static f32 hover_t[15]; 313 i32 idx = 0; 314 315 Variable var; 316 317 var.store = &bp->center_frequency; 318 var.type = VT_F32; 319 var.f32_limits = (v2){.y = 100e6}; 320 var.flags = V_CAUSES_COMPUTE; 321 var.display_scale = 1e-6; 322 var.scroll_scale = 1e5; 323 draw_r = do_text_input_listing(s8("Center Frequency:"), s8("[MHz]"), var, ui, draw_r, 324 mouse, hover_t + idx++); 325 326 var.store = &bp->speed_of_sound; 327 var.type = VT_F32; 328 var.f32_limits = (v2){.y = 1e6}; 329 var.flags = V_CAUSES_COMPUTE; 330 var.display_scale = 1; 331 var.scroll_scale = 10; 332 draw_r = do_text_input_listing(s8("Speed of Sound:"), s8("[m/s]"), var, ui, draw_r, 333 mouse, hover_t + idx++); 334 335 var.store = &bp->output_min_coordinate.x; 336 var.type = VT_F32; 337 var.f32_limits = (v2){.x = -1e3, .y = maxx}; 338 var.flags = V_CAUSES_COMPUTE; 339 var.display_scale = 1e3; 340 var.scroll_scale = 0.5e-3; 341 draw_r = do_text_input_listing(s8("Min Lateral Point:"), s8("[mm]"), var, ui, draw_r, 342 mouse, hover_t + idx++); 343 344 var.store = &bp->output_max_coordinate.x; 345 var.type = VT_F32; 346 var.f32_limits = (v2){.x = minx, .y = 1e3}; 347 var.flags = V_CAUSES_COMPUTE; 348 var.display_scale = 1e3; 349 var.scroll_scale = 0.5e-3; 350 draw_r = do_text_input_listing(s8("Max Lateral Point:"), s8("[mm]"), var, ui, draw_r, 351 mouse, hover_t + idx++); 352 353 var.store = &bp->output_min_coordinate.z; 354 var.type = VT_F32; 355 var.f32_limits = (v2){.y = maxz}; 356 var.flags = V_CAUSES_COMPUTE; 357 var.display_scale = 1e3; 358 var.scroll_scale = 0.5e-3; 359 draw_r = do_text_input_listing(s8("Min Axial Point:"), s8("[mm]"), var, ui, draw_r, 360 mouse, hover_t + idx++); 361 362 var.store = &bp->output_max_coordinate.z; 363 var.type = VT_F32; 364 var.f32_limits = (v2){.x = minz, .y = 1e3}; 365 var.flags = V_CAUSES_COMPUTE; 366 var.display_scale = 1e3; 367 var.scroll_scale = 0.5e-3; 368 draw_r = do_text_input_listing(s8("Max Axial Point:"), s8("[mm]"), var, ui, draw_r, 369 mouse, hover_t + idx++); 370 371 var.store = &bp->off_axis_pos; 372 var.type = VT_F32; 373 var.f32_limits = (v2){.x = minx, .y = maxx}; 374 var.flags = V_CAUSES_COMPUTE; 375 var.display_scale = 1e3; 376 var.scroll_scale = 0.5e-3; 377 draw_r = do_text_input_listing(s8("Off Axis Position:"), s8("[mm]"), var, ui, draw_r, 378 mouse, hover_t + idx++); 379 380 var = (Variable){0}; 381 var.store = &bp->beamform_plane; 382 var.type = VT_B32; 383 var.flags = V_CAUSES_COMPUTE; 384 draw_r = do_text_toggle_listing(s8("Beamform Plane:"), s8("XZ"), s8("YZ"), var, ui, 385 draw_r, mouse, hover_t + idx++); 386 387 var.store = &bp->f_number; 388 var.type = VT_F32; 389 var.f32_limits = (v2){.y = 1e3}; 390 var.flags = V_CAUSES_COMPUTE; 391 var.display_scale = 1; 392 var.scroll_scale = 0.1; 393 draw_r = do_text_input_listing(s8("F#:"), s8(""), var, ui, draw_r, mouse, hover_t + idx++); 394 395 var.store = &ctx->fsctx.db; 396 var.type = VT_F32; 397 var.f32_limits = (v2){.x = -120}; 398 var.flags = V_GEN_MIPMAPS; 399 var.display_scale = 1; 400 var.scroll_scale = 1; 401 draw_r = do_text_input_listing(s8("Dynamic Range:"), s8("[dB]"), var, ui, draw_r, 402 mouse, hover_t + idx++); 403 404 var.store = &ctx->fsctx.threshold; 405 var.type = VT_F32; 406 var.f32_limits = (v2){.y = 240}; 407 var.flags = V_GEN_MIPMAPS; 408 var.display_scale = 1; 409 var.scroll_scale = 1; 410 draw_r = do_text_input_listing(s8("Threshold:"), s8(""), var, ui, draw_r, 411 mouse, hover_t + idx++); 412 413 draw_r.pos.y += 2 * LISTING_LINE_PAD; 414 draw_r.size.y -= 2 * LISTING_LINE_PAD; 415 416 #if 0 417 /* TODO: work this into the work queue */ 418 bmv = (BPModifiableValue){&ctx->partial_compute_ctx.volume_dim.x, bmv_store_power_of_two, 419 .ilimits = (iv2){.x = 1, .y = ctx->gl.max_3d_texture_dim}, 420 MV_INT, 1, 1}; 421 draw_r = do_text_input_listing(s8("Export Dimension X:"), s8(""), bmv, ctx, arena, 422 draw_r, mouse, hover_t + idx++); 423 424 bmv = (BPModifiableValue){&ctx->partial_compute_ctx.volume_dim.y, bmv_store_power_of_two, 425 .ilimits = (iv2){.x = 1, .y = ctx->gl.max_3d_texture_dim}, 426 MV_INT, 1, 1}; 427 draw_r = do_text_input_listing(s8("Export Dimension Y:"), s8(""), bmv, ctx, arena, 428 draw_r, mouse, hover_t + idx++); 429 430 bmv = (BPModifiableValue){&ctx->partial_compute_ctx.volume_dim.z, bmv_store_power_of_two, 431 .ilimits = (iv2){.x = 1, .y = ctx->gl.max_3d_texture_dim}, 432 MV_INT, 1, 1}; 433 draw_r = do_text_input_listing(s8("Export Dimension Z:"), s8(""), bmv, ctx, arena, 434 draw_r, mouse, hover_t + idx++); 435 436 Rect btn_r = draw_r; 437 btn_r.size.h = ctx->font.baseSize * 1.3; 438 btn_r.size.w *= 0.6; 439 if (do_text_button(ctx, s8("Dump Raw Volume"), btn_r, mouse, hover_t + idx++)) { 440 if (!ctx->partial_compute_ctx.state) { 441 } 442 } 443 #endif 444 445 /* NOTE: if C compilers didn't suck this would be a static assert */ 446 ASSERT(idx <= ARRAY_COUNT(hover_t)); 447 } 448 449 static void 450 draw_debug_overlay(BeamformerCtx *ctx, Arena arena, Rect r) 451 { 452 static s8 labels[CS_LAST] = { 453 #define X(e, n, s, h, pn) [CS_##e] = s8(pn ":"), 454 COMPUTE_SHADERS 455 #undef X 456 }; 457 458 BeamformerUI *ui = ctx->ui; 459 ComputeShaderCtx *cs = &ctx->csctx; 460 uv2 ws = ctx->window_size; 461 462 Stream buf = stream_alloc(&arena, 64); 463 v2 pos = {.x = 20, .y = ws.h - 10}; 464 465 f32 compute_time_sum = 0; 466 u32 stages = ctx->params->compute_stages_count; 467 for (u32 i = 0; i < stages; i++) { 468 u32 index = ctx->params->compute_stages[i]; 469 pos.y -= measure_text(ui->font, labels[index]).y; 470 draw_text(ui->font, labels[index], pos, 0, colour_from_normalized(FG_COLOUR)); 471 472 buf.widx = 0; 473 stream_append_f64_e(&buf, cs->last_frame_time[index]); 474 stream_append_s8(&buf, s8(" [s]")); 475 v2 txt_fs = measure_text(ui->font, stream_to_s8(&buf)); 476 v2 rpos = {.x = r.pos.x + r.size.w - txt_fs.w, .y = pos.y}; 477 draw_text(ui->font, stream_to_s8(&buf), rpos, 0, colour_from_normalized(FG_COLOUR)); 478 479 compute_time_sum += cs->last_frame_time[index]; 480 } 481 482 static s8 totals[2] = {s8("Compute Total:"), s8("Volume Total:")}; 483 f32 times[2] = {compute_time_sum, ctx->partial_compute_ctx.runtime}; 484 for (u32 i = 0; i < ARRAY_COUNT(totals); i++) { 485 pos.y -= measure_text(ui->font, totals[i]).y; 486 draw_text(ui->font, totals[i], pos, 0, colour_from_normalized(FG_COLOUR)); 487 488 buf.widx = 0; 489 stream_append_f64_e(&buf, times[i]); 490 stream_append_s8(&buf, s8(" [s]")); 491 v2 txt_fs = measure_text(ui->font, stream_to_s8(&buf)); 492 v2 rpos = {.x = r.pos.x + r.size.w - txt_fs.w, .y = pos.y}; 493 draw_text(ui->font, stream_to_s8(&buf), rpos, 0, colour_from_normalized(FG_COLOUR)); 494 } 495 496 { 497 static v2 pos = {.x = 32, .y = 128}; 498 static v2 scale = {.x = 1.0, .y = 1.0}; 499 static u32 txt_idx = 0; 500 static s8 txt[2] = { s8("-_-"), s8("^_^") }; 501 static v2 ts[2]; 502 if (ts[0].x == 0) { 503 ts[0] = measure_text(ui->font, txt[0]); 504 ts[1] = measure_text(ui->font, txt[1]); 505 } 506 507 pos.x += 130 * dt_for_frame * scale.x; 508 pos.y += 120 * dt_for_frame * scale.y; 509 510 if (pos.x > (ws.w - ts[txt_idx].x) || pos.x < 0) { 511 txt_idx = !txt_idx; 512 pos.x = CLAMP(pos.x, 0, ws.w - ts[txt_idx].x); 513 scale.x *= -1.0; 514 } 515 516 if (pos.y > (ws.h - ts[txt_idx].y) || pos.y < 0) { 517 txt_idx = !txt_idx; 518 pos.y = CLAMP(pos.y, 0, ws.h - ts[txt_idx].y); 519 scale.y *= -1.0; 520 } 521 522 draw_text(ui->font, txt[txt_idx], pos, 0, RED); 523 } 524 } 525 526 static void 527 ui_store_variable(Variable *var, void *new_value) 528 { 529 /* TODO: special cases (eg. power of 2) */ 530 switch (var->type) { 531 case VT_F32: { 532 f32 f32_val = *(f32 *)new_value; 533 f32 *f32_var = var->store; 534 *f32_var = CLAMP(f32_val, var->f32_limits.x, var->f32_limits.y); 535 } break; 536 case VT_I32: { 537 i32 i32_val = *(i32 *)new_value; 538 i32 *i32_var = var->store; 539 *i32_var = CLAMP(i32_val, var->i32_limits.x, var->i32_limits.y); 540 } break; 541 default: INVALID_CODE_PATH; 542 } 543 } 544 545 static void 546 begin_text_input(InputState *is, Variable *var) 547 { 548 ASSERT(var->store != NULL); 549 550 Stream s = {.cap = ARRAY_COUNT(is->buf), .data = is->buf}; 551 stream_append_variable(&s, var); 552 ASSERT(!s.errors); 553 is->buf_len = s.widx; 554 is->cursor = -1; 555 } 556 557 static void 558 end_text_input(InputState *is, Variable *var) 559 { 560 f32 value = parse_f64((s8){.len = is->buf_len, .data = is->buf}) / var->display_scale; 561 ui_store_variable(var, &value); 562 } 563 564 static void 565 update_text_input(InputState *is) 566 { 567 if (is->cursor == -1) 568 return; 569 570 is->cursor_blink_t += is->cursor_blink_scale * dt_for_frame; 571 if (is->cursor_blink_t >= 1) is->cursor_blink_scale = -1.5f; 572 if (is->cursor_blink_t <= 0) is->cursor_blink_scale = 1.5f; 573 574 /* NOTE: handle multiple input keys on a single frame */ 575 i32 key = GetCharPressed(); 576 while (key > 0) { 577 if (is->buf_len == ARRAY_COUNT(is->buf)) 578 break; 579 580 b32 allow_key = ((key >= '0' && key <= '9') || (key == '.') || 581 (key == '-' && is->cursor == 0)); 582 if (allow_key) { 583 mem_move(is->buf + is->cursor, 584 is->buf + is->cursor + 1, 585 is->buf_len - is->cursor + 1); 586 587 is->buf[is->cursor++] = key; 588 is->buf_len++; 589 } 590 key = GetCharPressed(); 591 } 592 593 if ((IsKeyPressed(KEY_LEFT) || IsKeyPressedRepeat(KEY_LEFT)) && is->cursor > 0) 594 is->cursor--; 595 596 if ((IsKeyPressed(KEY_RIGHT) || IsKeyPressedRepeat(KEY_RIGHT)) && is->cursor < is->buf_len) 597 is->cursor++; 598 599 if ((IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE)) && is->cursor > 0) { 600 is->cursor--; 601 mem_move(is->buf + is->cursor + 1, 602 is->buf + is->cursor, 603 is->buf_len - is->cursor); 604 is->buf_len--; 605 } 606 607 if ((IsKeyPressed(KEY_DELETE) || IsKeyPressedRepeat(KEY_DELETE)) && is->cursor < is->buf_len) { 608 mem_move(is->buf + is->cursor + 1, 609 is->buf + is->cursor, 610 is->buf_len - is->cursor); 611 is->buf_len--; 612 } 613 } 614 615 static b32 616 ui_can_start_compute(BeamformerCtx *ctx) 617 { 618 BeamformFrame *displayed = ctx->beamform_frames + ctx->displayed_frame_index; 619 b32 result = ctx->beamform_work_queue.compute_in_flight == 0; 620 result &= (displayed->dim.x != 0 || displayed->dim.y != 0); 621 result &= displayed->dim.z != 0; 622 return result; 623 } 624 625 static void 626 ui_start_compute(BeamformerCtx *ctx) 627 { 628 /* NOTE: we do not allow ui to start a work if no work was previously completed */ 629 Arena a = {0}; 630 if (ui_can_start_compute(ctx)) { 631 beamform_work_queue_push(ctx, &a, BW_RECOMPUTE); 632 BeamformFrameIterator bfi = beamform_frame_iterator(ctx); 633 for (BeamformFrame *frame = frame_next(&bfi); frame; frame = frame_next(&bfi)) { 634 if (frame->dim.w && frame->textures[frame->dim.w - 1]) 635 glClearTexImage(frame->textures[frame->dim.w - 1], 0, 636 GL_RED, GL_FLOAT, 0); 637 } 638 } 639 ctx->params->upload = 1; 640 } 641 642 static void 643 ui_gen_mipmaps(BeamformerCtx *ctx) 644 { 645 if (ctx->fsctx.output.texture.id) 646 ctx->flags |= GEN_MIPMAPS; 647 } 648 649 static void 650 ui_begin_interact(BeamformerUI *ui, BeamformerInput *input, b32 scroll) 651 { 652 InteractionState *is = &ui->interaction; 653 if (is->hot_state != IS_NONE) { 654 is->state = is->hot_state; 655 } else { 656 switch (is->hot.type) { 657 case VT_NULL: is->state = IS_NOP; break; 658 case VT_B32: is->state = IS_SET; break; 659 case VT_GROUP: is->state = IS_SET; break; 660 case VT_F32: { 661 if (scroll) { 662 is->state = IS_SCROLL; 663 } else { 664 is->state = IS_TEXT; 665 begin_text_input(&ui->text_input_state, &is->hot); 666 } 667 } break; 668 } 669 } 670 if (is->state != IS_NONE) { 671 is->active = is->hot; 672 } 673 } 674 675 static void 676 ui_end_interact(BeamformerCtx *ctx, BeamformerUI *ui) 677 { 678 InteractionState *is = &ui->interaction; 679 switch (is->state) { 680 case IS_NONE: break; 681 case IS_NOP: break; 682 case IS_SET: { 683 switch (is->active.type) { 684 case VT_B32: { 685 b32 *val = is->active.store; 686 *val = !(*val); 687 } break; 688 } 689 } break; 690 case IS_DISPLAY: 691 is->last_mouse_click_p = (v2){0}; 692 /* FALLTHROUGH */ 693 case IS_SCROLL: { 694 f32 delta = GetMouseWheelMove() * is->active.scroll_scale; 695 switch (is->active.type) { 696 case VT_B32: { 697 b32 *old_val = is->active.store; 698 b32 new_val = !(*old_val); 699 ui_store_variable(&is->active, &new_val); 700 } break; 701 case VT_F32: { 702 f32 *old_val = is->active.store; 703 f32 new_val = *old_val + delta; 704 ui_store_variable(&is->active, &new_val); 705 } break; 706 case VT_I32: { 707 i32 *old_val = is->active.store; 708 i32 new_val = *old_val + delta; 709 ui_store_variable(&is->active, &new_val); 710 } break; 711 } 712 } break; 713 case IS_TEXT: end_text_input(&ui->text_input_state, &is->active); break; 714 } 715 716 if (is->active.flags & V_CAUSES_COMPUTE) 717 ui_start_compute(ctx); 718 719 if (is->active.flags & V_GEN_MIPMAPS) 720 ui_gen_mipmaps(ctx); 721 722 is->state = IS_NONE; 723 is->active = NULL_VARIABLE; 724 } 725 726 static void 727 ui_interact(BeamformerCtx *ctx, BeamformerInput *input) 728 { 729 BeamformerUI *ui = ctx->ui; 730 InteractionState *is = &ui->interaction; 731 b32 mouse_left_pressed = IsMouseButtonPressed(MOUSE_BUTTON_LEFT); 732 b32 wheel_moved = GetMouseWheelMove(); 733 if (mouse_left_pressed || wheel_moved) { 734 if (is->state != IS_NONE) 735 ui_end_interact(ctx, ui); 736 ui_begin_interact(ui, input, wheel_moved); 737 } 738 739 if (IsKeyPressed(KEY_ENTER) && is->state == IS_TEXT) 740 ui_end_interact(ctx, ui); 741 742 switch (is->state) { 743 case IS_DISPLAY: { 744 b32 should_end = wheel_moved || IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) || 745 (is->active.store != is->hot.store); 746 if (should_end) { 747 ui_end_interact(ctx, ui); 748 } else if (mouse_left_pressed) { 749 is->last_mouse_click_p = input->mouse; 750 } 751 } break; 752 case IS_SCROLL: ui_end_interact(ctx, ui); break; 753 case IS_SET: ui_end_interact(ctx, ui); break; 754 case IS_TEXT: update_text_input(&ui->text_input_state); break; 755 case IS_DRAG: { 756 if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) { 757 ui_end_interact(ctx, ui); 758 } else { 759 switch (is->active.type) { 760 } 761 } 762 } break; 763 } 764 765 is->hot_state = IS_NONE; 766 is->hot = NULL_VARIABLE; 767 } 768 769 static void 770 ui_init(BeamformerCtx *ctx, Arena store) 771 { 772 /* NOTE: store the ui at the base of the passed in arena and use the rest for 773 * temporary allocations within the ui. If needed we can recall this function 774 * to completely clear the ui state */ 775 BeamformerUI *ui = ctx->ui = alloc(&store, typeof(*ctx->ui), 1); 776 ui->arena_for_frame = store; 777 778 /* TODO: build these into the binary */ 779 ui->font = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 28, 0, 0); 780 ui->small_font = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 22, 0, 0); 781 } 782 783 static void 784 draw_ui(BeamformerCtx *ctx, BeamformerInput *input) 785 { 786 BeamformerParameters *bp = &ctx->params->raw; 787 BeamformerUI *ui = ctx->ui; 788 789 end_temp_arena(ui->frame_temporary_arena); 790 ui->frame_temporary_arena = begin_temp_arena(&ui->arena_for_frame); 791 792 /* NOTE: process interactions first because the user interacted with 793 * the ui that was presented last frame */ 794 ui_interact(ctx, input); 795 796 BeginDrawing(); 797 ClearBackground(colour_from_normalized(BG_COLOUR)); 798 799 Texture *output = &ctx->fsctx.output.texture; 800 801 /* TODO: this depends on the direction being rendered (x vs y) */ 802 v2 output_dim = { 803 .x = bp->output_max_coordinate.x - bp->output_min_coordinate.x, 804 .y = bp->output_max_coordinate.z - bp->output_min_coordinate.z, 805 }; 806 807 v2 mouse = input->mouse; 808 Rect wr = {.size = {.w = (f32)ctx->window_size.w, .h = (f32)ctx->window_size.h}}; 809 Rect lr = wr, rr = wr; 810 lr.size.w = INFO_COLUMN_WIDTH; 811 rr.size.w = wr.size.w - lr.size.w; 812 rr.pos.x = lr.pos.x + lr.size.w; 813 814 Rect vr = INVERTED_INFINITY_RECT; 815 if (output_dim.x > 1e-6 && output_dim.y > 1e-6) { 816 Stream buf = stream_alloc(&ui->arena_for_frame, 64); 817 stream_append_f64(&buf, -188.8f, 10); 818 stream_append_s8(&buf, s8(" mm")); 819 v2 txt_s = measure_text(ui->small_font, stream_to_s8(&buf)); 820 buf.widx = 0; 821 822 rr.pos.x += 0.02 * rr.size.w; 823 rr.pos.y += 0.02 * rr.size.h; 824 rr.size.w *= 0.96; 825 rr.size.h *= 0.96; 826 827 f32 tick_len = 20; 828 f32 pad = 1.2 * txt_s.x + tick_len; 829 830 vr = rr; 831 vr.pos.x += 0.5 * txt_s.y; 832 vr.pos.y += 0.5 * txt_s.y; 833 vr.size.h = rr.size.h - pad; 834 vr.size.w = rr.size.w - pad; 835 836 f32 aspect = output_dim.h / output_dim.w; 837 if (rr.size.h < (vr.size.w * aspect) + pad) { 838 vr.size.w = vr.size.h / aspect; 839 } else { 840 vr.size.h = vr.size.w * aspect; 841 } 842 vr.pos.x += (rr.size.w - (vr.size.w + pad)) / 2; 843 vr.pos.y += (rr.size.h - (vr.size.h + pad)) / 2; 844 845 Rectangle tex_r = { 0.0f, 0.0f, (f32)output->width, -(f32)output->height }; 846 NPatchInfo tex_np = { tex_r, 0, 0, 0, 0, NPATCH_NINE_PATCH }; 847 DrawTextureNPatch(*output, tex_np, vr.rl, (Vector2){0}, 0, WHITE); 848 849 static f32 txt_colour_t[2]; 850 for (u32 i = 0; i < 2; i++) { 851 u32 line_count = vr.size.E[i] / (1.5 * txt_s.h); 852 f32 inc = vr.size.E[i] / line_count; 853 v2 start_pos = vr.pos; 854 start_pos.E[!i] += vr.size.E[!i]; 855 856 v2 end_pos = start_pos; 857 end_pos.E[!i] += tick_len; 858 859 /* NOTE: Center the Text with the Tick center */ 860 f32 txt_pos_scale[2] = {1, -1}; 861 v2 txt_pos = end_pos; 862 txt_pos.E[i] += txt_pos_scale[i] * txt_s.y/2; 863 txt_pos.E[!i] += 10; 864 865 Rect tick_rect = {.pos = start_pos, .size = vr.size}; 866 tick_rect.size.E[!i] = 10 + tick_len + txt_s.x; 867 868 /* TODO: don't do this nonsense; this code will need to get 869 * split into a seperate function */ 870 /* TODO: pass this through the interaction system */ 871 u32 coord_idx = i == 0? 0 : 2; 872 if (hover_text(mouse, tick_rect, txt_colour_t + i, 1)) { 873 f32 scale[2] = {0.5e-3, 1e-3}; 874 f32 size_delta = GetMouseWheelMove() * scale[i]; 875 if (coord_idx == 0) 876 bp->output_min_coordinate.E[coord_idx] -= size_delta; 877 bp->output_max_coordinate.E[coord_idx] += size_delta; 878 if (size_delta) 879 ui_start_compute(ctx); 880 } 881 882 f32 mm = bp->output_min_coordinate.E[coord_idx] * 1e3; 883 f32 mm_inc = inc * output_dim.E[i] * 1e3 / vr.size.E[i]; 884 885 Color txt_colour = colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, 886 txt_colour_t[i])); 887 888 f32 rot[2] = {90, 0}; 889 for (u32 j = 0; j <= line_count; j++) { 890 DrawLineEx(start_pos.rl, end_pos.rl, 3, colour_from_normalized(FG_COLOUR)); 891 buf.widx = 0; 892 if (i == 0 && mm > 0) stream_append_byte(&buf, '+'); 893 stream_append_f64(&buf, mm, 10); 894 stream_append_s8(&buf, s8(" mm")); 895 draw_text(ui->small_font, stream_to_s8(&buf), txt_pos, 896 rot[i], txt_colour); 897 start_pos.E[i] += inc; 898 end_pos.E[i] += inc; 899 txt_pos.E[i] += inc; 900 mm += mm_inc; 901 } 902 } 903 } 904 905 draw_settings_ui(ctx, lr, mouse); 906 draw_debug_overlay(ctx, ui->arena_for_frame, lr); 907 908 if (CheckCollisionPointRec(mouse.rl, vr.rl)) { 909 InteractionState *is = &ui->interaction; 910 is->hot_state = IS_DISPLAY; 911 is->hot.store = &ctx->fsctx.threshold; 912 is->hot.type = VT_F32; 913 is->hot.f32_limits = (v2){.y = 240}; 914 is->hot.flags = V_GEN_MIPMAPS; 915 is->hot.display_scale = 1; 916 is->hot.scroll_scale = 1; 917 918 /* NOTE: check and draw Ruler */ 919 if (CheckCollisionPointRec(is->last_mouse_click_p.rl, vr.rl)) { 920 Stream buf = arena_stream(&ui->arena_for_frame); 921 922 Color colour = colour_from_normalized(RULER_COLOUR); 923 DrawCircleV(is->last_mouse_click_p.rl, 3, colour); 924 DrawLineEx(mouse.rl, is->last_mouse_click_p.rl, 2, colour); 925 v2 pixels_to_mm = output_dim; 926 pixels_to_mm.x /= vr.size.x * 1e-3; 927 pixels_to_mm.y /= vr.size.y * 1e-3; 928 929 v2 pixel_delta = sub_v2(is->last_mouse_click_p, mouse); 930 v2 mm_delta = mul_v2(pixels_to_mm, pixel_delta); 931 932 stream_append_f64(&buf, magnitude_v2(mm_delta), 100); 933 stream_append_s8(&buf, s8(" mm")); 934 v2 txt_p = is->last_mouse_click_p; 935 v2 txt_s = measure_text(ui->small_font, stream_to_s8(&buf)); 936 if (pixel_delta.y < 0) txt_p.y -= txt_s.y; 937 if (pixel_delta.x < 0) txt_p.x -= txt_s.x; 938 draw_text(ui->small_font, stream_to_s8(&buf), txt_p, 0, colour); 939 } 940 } 941 EndDrawing(); 942 }