Files
autosample/src/main.cpp
2026-02-25 16:40:14 -05:00

531 lines
18 KiB
C++

// Unity build - include all src files here
// -mta
// [h]
#include "base/base_inc.h"
#include "platform/platform.h"
#include "renderer/renderer.h"
#include "midi/midi.h"
#include "ui/ui_core.h"
#include "ui/ui_widgets.h"
// [cpp]
#include "base/base_inc.cpp"
#include "ui/ui_core.cpp"
#include "ui/ui_widgets.cpp"
#include "platform/platform_win32.cpp"
#include "renderer/renderer_dx12.cpp"
#include "midi/midi_win32.cpp"
#include "menus.cpp"
////////////////////////////////
// Win32 input gathering
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
struct InputState {
Vec2F32 mouse;
Vec2F32 scroll_delta;
B32 mouse_down;
B32 was_mouse_down;
};
static InputState g_input;
static void input_gather(void *window_handle) {
HWND hwnd = (HWND)window_handle;
// Mouse position
POINT cursor;
GetCursorPos(&cursor);
ScreenToClient(hwnd, &cursor);
g_input.mouse = v2f32((F32)cursor.x, (F32)cursor.y);
// Mouse button
g_input.was_mouse_down = g_input.mouse_down;
g_input.mouse_down = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0;
// Scroll (TODO: hook WM_MOUSEWHEEL for real scroll deltas)
g_input.scroll_delta = v2f32(0, 0);
}
////////////////////////////////
// Clay text config helpers
static Clay_TextElementConfig g_text_config_normal;
static Clay_TextElementConfig g_text_config_title;
static Clay_TextElementConfig g_text_config_dim;
static void init_text_configs() {
g_text_config_normal = {};
g_text_config_normal.textColor = g_theme.text;
g_text_config_normal.fontSize = 15;
g_text_config_normal.wrapMode = CLAY_TEXT_WRAP_NONE;
g_text_config_title = {};
g_text_config_title.textColor = g_theme.text;
g_text_config_title.fontSize = 15;
g_text_config_title.wrapMode = CLAY_TEXT_WRAP_NONE;
g_text_config_dim = {};
g_text_config_dim.textColor = g_theme.text_dim;
g_text_config_dim.fontSize = 15;
g_text_config_dim.wrapMode = CLAY_TEXT_WRAP_NONE;
}
////////////////////////////////
// App state — all mutable state the frame function needs
struct AppState {
PlatformWindow *window;
Renderer *renderer;
MidiEngine *midi;
UI_Context *ui;
S32 last_w, last_h;
B32 show_browser;
B32 show_props;
B32 show_log;
B32 show_midi_devices;
LARGE_INTEGER freq;
LARGE_INTEGER last_time;
// Demo widget state
B32 demo_checkbox_a;
B32 demo_checkbox_b;
int32_t demo_radio_sel;
int32_t demo_dropdown_sel;
char demo_text_a[128];
char demo_text_b[128];
int32_t demo_button_count;
};
////////////////////////////////
// Panel builders
static void build_panel_title_bar(Clay_ElementId id, Clay_String title) {
CLAY(id,
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(24) },
.padding = { 8, 8, 0, 0 },
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
},
.backgroundColor = g_theme.title_bar,
.border = { .color = g_theme.border, .width = { .bottom = 1 } }
) {
CLAY_TEXT(title, &g_text_config_title);
}
}
static void build_browser_panel(B32 show) {
if (!show) return;
CLAY(CLAY_ID("BrowserPanel"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIXED(200), .height = CLAY_SIZING_GROW() },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.backgroundColor = g_theme.bg_medium,
.border = { .color = g_theme.border, .width = { .right = 1 } }
) {
build_panel_title_bar(CLAY_ID("BrowserTitleBar"), CLAY_STRING("Browser"));
CLAY(CLAY_ID("BrowserContent"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.padding = { 8, 8, 6, 6 },
.childGap = 4,
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
) {
CLAY_TEXT(CLAY_STRING("Instruments"), &g_text_config_normal);
}
}
}
static void build_main_panel(AppState *app) {
CLAY(CLAY_ID("MainPanel"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.backgroundColor = g_theme.bg_light
) {
build_panel_title_bar(CLAY_ID("MainTitleBar"), CLAY_STRING("Main"));
CLAY(CLAY_ID("MainContent"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.padding = { 16, 16, 12, 12 },
.childGap = 12,
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
) {
// Section: Buttons
ui_label("LblButtons", "Buttons");
CLAY(CLAY_ID("ButtonRow"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
.childGap = 8,
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
) {
if (ui_button("BtnHello", "Click Me")) {
app->demo_button_count++;
}
if (ui_button("BtnReset", "Reset")) {
app->demo_button_count = 0;
}
}
// Show click count
static char btn_count_buf[64];
snprintf(btn_count_buf, sizeof(btn_count_buf), "Button clicked %d times", app->demo_button_count);
ui_label("LblBtnCount", btn_count_buf);
// Separator
CLAY(CLAY_ID("Sep1"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
.backgroundColor = g_theme.border
) {}
// Section: Checkboxes
ui_label("LblCheckboxes", "Checkboxes");
ui_checkbox("ChkA", "Enable feature A", &app->demo_checkbox_a);
ui_checkbox("ChkB", "Enable feature B", &app->demo_checkbox_b);
CLAY(CLAY_ID("Sep2"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
.backgroundColor = g_theme.border
) {}
// Section: Radio buttons
ui_label("LblRadio", "Output Format");
static const char *radio_options[] = { "WAV", "AIFF", "FLAC" };
ui_radio_group("RadioFmt", radio_options, 3, &app->demo_radio_sel);
CLAY(CLAY_ID("Sep3"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
.backgroundColor = g_theme.border
) {}
// Section: Text inputs
ui_label("LblText", "Text Inputs");
CLAY(CLAY_ID("TextRow"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
.childGap = 8,
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
) {
CLAY(CLAY_ID("TextCol1"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
.childGap = 4,
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
) {
ui_label("LblName", "Name:");
ui_text_input("TxtName", app->demo_text_a, sizeof(app->demo_text_a));
}
CLAY(CLAY_ID("TextCol2"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
.childGap = 4,
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
) {
ui_label("LblPath", "Output Path:");
ui_text_input("TxtPath", app->demo_text_b, sizeof(app->demo_text_b));
}
}
CLAY(CLAY_ID("Sep4"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
.backgroundColor = g_theme.border
) {}
// Section: Dropdown
ui_label("LblDropdown", "Sample Rate");
CLAY(CLAY_ID("DropdownWrapper"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIXED(200), .height = CLAY_SIZING_FIT() },
}
) {
static const char *rate_options[] = { "44100 Hz", "48000 Hz", "88200 Hz", "96000 Hz", "192000 Hz" };
ui_dropdown("DropRate", rate_options, 5, &app->demo_dropdown_sel);
}
}
}
}
static void build_properties_panel(B32 show) {
if (!show) return;
CLAY(CLAY_ID("PropertiesPanel"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.backgroundColor = g_theme.bg_medium,
.border = { .color = g_theme.border, .width = { .bottom = 1 } }
) {
build_panel_title_bar(CLAY_ID("PropertiesTitleBar"), CLAY_STRING("Properties"));
CLAY(CLAY_ID("PropertiesContent"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.padding = { 8, 8, 6, 6 },
.childGap = 4,
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
) {
CLAY_TEXT(CLAY_STRING("Details"), &g_text_config_normal);
}
}
}
static void build_midi_panel(B32 show, MidiEngine *midi) {
if (!show) return;
CLAY(CLAY_ID("MidiPanel"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.backgroundColor = g_theme.bg_medium
) {
build_panel_title_bar(CLAY_ID("MidiTitleBar"), CLAY_STRING("MIDI Devices"));
CLAY(CLAY_ID("MidiContent"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.padding = { 8, 8, 6, 6 },
.childGap = 4,
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
) {
// Refresh button
CLAY(CLAY_ID("MidiRefreshBtn"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(28) },
.padding = { 12, 12, 0, 0 },
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
},
.backgroundColor = Clay_Hovered() ? g_theme.accent_hover : g_theme.bg_lighter,
.cornerRadius = CLAY_CORNER_RADIUS(3)
) {
CLAY_TEXT(CLAY_STRING("Refresh"), &g_text_config_normal);
}
// Device list - use static buffers so strings persist for Clay rendering
static char device_bufs[64][128];
int32_t device_count = midi_get_device_count(midi);
for (int32_t i = 0; i < device_count && i < 64; i++) {
MidiDeviceInfo *dev = midi_get_device(midi, i);
int len = snprintf(device_bufs[i], sizeof(device_bufs[i]), "[%s] %s", dev->is_input ? "IN" : "OUT", dev->name);
Clay_String device_str = { .isStaticallyAllocated = false, .length = len, .chars = device_bufs[i] };
CLAY(CLAY_IDI("MidiDevice", i),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
.padding = { 4, 4, 2, 2 },
}
) {
CLAY_TEXT(device_str, &g_text_config_normal);
}
}
if (device_count == 0) {
CLAY_TEXT(CLAY_STRING("No MIDI devices found"), &g_text_config_dim);
}
}
}
}
static void build_log_panel(B32 show) {
if (!show) return;
CLAY(CLAY_ID("LogPanel"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(180) },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.backgroundColor = g_theme.bg_medium,
.border = { .color = g_theme.border, .width = { .top = 1 } }
) {
build_panel_title_bar(CLAY_ID("LogTitleBar"), CLAY_STRING("Log"));
CLAY(CLAY_ID("LogContent"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.padding = { 8, 8, 6, 6 },
.childGap = 4,
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
) {
CLAY_TEXT(CLAY_STRING("Output / Log"), &g_text_config_normal);
}
}
}
////////////////////////////////
// Build the full UI layout for one frame
static void build_ui(AppState *app) {
CLAY(CLAY_ID("Root"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
) {
// Top row: browser | main | right column
CLAY(CLAY_ID("TopRow"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
) {
build_browser_panel(app->show_browser);
build_main_panel(app);
if (app->show_props || app->show_midi_devices) {
CLAY(CLAY_ID("RightColumn"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIXED(250), .height = CLAY_SIZING_GROW() },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.border = { .color = g_theme.border, .width = { .left = 1 } }
) {
build_properties_panel(app->show_props);
build_midi_panel(app->show_midi_devices, app->midi);
}
}
}
build_log_panel(app->show_log);
}
}
////////////////////////////////
// Render one frame: resize if needed, build UI, submit to GPU.
// Called from the main loop and from WM_SIZE during live resize.
static void do_frame(AppState *app) {
// Timing
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
F32 dt = (F32)(now.QuadPart - app->last_time.QuadPart) / (F32)app->freq.QuadPart;
app->last_time = now;
if (dt > 0.1f) dt = 0.1f;
// Resize
S32 w, h;
platform_get_size(app->window, &w, &h);
if (w != app->last_w || h != app->last_h) {
renderer_resize(app->renderer, w, h);
app->last_w = w;
app->last_h = h;
}
if (!renderer_begin_frame(app->renderer))
return;
// Gather input
input_gather(platform_get_native_handle(app->window));
PlatformInputEvents input_events = platform_get_input_events(app->window);
ui_widgets_begin_frame(input_events, g_input.mouse_down, g_input.was_mouse_down);
// Build UI with Clay
ui_begin_frame(app->ui, (F32)w, (F32)h, g_input.mouse, g_input.mouse_down,
g_input.scroll_delta, dt);
build_ui(app);
Clay_RenderCommandArray render_commands = ui_end_frame(app->ui);
// Render
renderer_end_frame(app->renderer, render_commands);
}
////////////////////////////////
// Platform frame callback for live resize
static void frame_callback(void *user_data) {
do_frame((AppState *)user_data);
}
////////////////////////////////
// Entry point
int main(int argc, char **argv) {
(void)argc;
(void)argv;
PlatformWindowDesc window_desc = {};
PlatformWindow *window = platform_create_window(&window_desc);
if (!window)
return 1;
S32 w, h;
platform_get_size(window, &w, &h);
RendererDesc renderer_desc = {};
renderer_desc.window_handle = platform_get_native_handle(window);
renderer_desc.width = w;
renderer_desc.height = h;
Renderer *renderer = renderer_create(&renderer_desc);
if (!renderer) {
platform_destroy_window(window);
return 1;
}
MidiEngine *midi = midi_create();
// Initialize UI (Clay)
ui_init_theme();
UI_Context *ui = ui_create((F32)w, (F32)h);
ui_set_measure_text_fn(ui, renderer_measure_text, renderer);
init_text_configs();
setup_menus(window);
ui_widgets_init();
AppState app = {};
app.window = window;
app.renderer = renderer;
app.midi = midi;
app.ui = ui;
app.last_w = w;
app.last_h = h;
app.show_browser = 1;
app.show_props = 1;
app.show_log = 1;
app.show_midi_devices = 1;
app.demo_dropdown_sel = 1; // default to 48000 Hz
snprintf(app.demo_text_a, sizeof(app.demo_text_a), "My Instrument");
snprintf(app.demo_text_b, sizeof(app.demo_text_b), "C:\\Samples\\output");
QueryPerformanceFrequency(&app.freq);
QueryPerformanceCounter(&app.last_time);
platform_set_frame_callback(window, frame_callback, &app);
while (platform_poll_events(window)) {
// Menu commands
int32_t menu_cmd = platform_poll_menu_command(window);
switch (menu_cmd) {
case MENU_FILE_EXIT: goto exit_app;
case MENU_VIEW_BROWSER: app.show_browser = !app.show_browser; break;
case MENU_VIEW_PROPERTIES:app.show_props = !app.show_props; break;
case MENU_VIEW_LOG: app.show_log = !app.show_log; break;
case MENU_VIEW_MIDI_DEVICES: app.show_midi_devices = !app.show_midi_devices; break;
default: break;
}
do_frame(&app);
}
exit_app:
midi_destroy(midi);
ui_destroy(ui);
renderer_destroy(renderer);
platform_destroy_window(window);
return 0;
}