531 lines
18 KiB
C++
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;
|
|
}
|