Files
autosample/src/main.cpp
2026-03-04 01:25:59 -05:00

1197 lines
47 KiB
C++

// Unity build - include all src files here
// -mta
#ifdef __APPLE__
#include <mach/mach_time.h>
#endif
// [h]
#include "base/base_inc.h"
#include "platform/platform.h"
#include "renderer/renderer.h"
#include "midi/midi.h"
#include "audio/audio.h"
#include "ui/ui_core.h"
#include "ui/ui_icons.h"
#include "ui/ui_widgets.h"
// [cpp]
#include "base/base_inc.cpp"
#include "ui/ui_core.cpp"
#include "ui/ui_icons.cpp"
#include "ui/ui_widgets.cpp"
#ifdef __APPLE__
#include "platform/platform_macos.mm"
#include "renderer/renderer_metal.mm"
#include "midi/midi_coremidi.cpp"
#include "audio/audio_coreaudio.cpp"
#else
#include "platform/platform_win32.cpp"
#include "renderer/renderer_dx12.cpp"
#include "midi/midi_win32.cpp"
#include "audio/audio_asio.cpp"
#endif
#include "menus.cpp"
////////////////////////////////
// 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;
AudioEngine *audio;
UI_Context *ui;
S32 last_w, last_h;
B32 show_browser;
B32 show_props;
B32 show_log;
B32 show_midi_devices;
#ifdef __APPLE__
uint64_t freq_numer;
uint64_t freq_denom;
uint64_t last_time;
#else
LARGE_INTEGER freq;
LARGE_INTEGER last_time;
#endif
// Tab state
S32 right_panel_tab; // 0 = Properties, 1 = MIDI Devices
// 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;
// Modal / window demo state
B32 show_settings_window;
B32 show_about_window;
B32 show_confirm_dialog;
S32 settings_theme_sel;
S32 settings_theme_prev;
B32 settings_vsync;
B32 settings_autosave;
// Accent color selection
S32 accent_sel;
S32 accent_prev;
// Corner radius selection
S32 radius_sel;
// Knob demo state
F32 demo_knob_unsigned;
F32 demo_knob_signed;
// Slider/fader demo state
F32 demo_slider_h;
F32 demo_slider_v;
F32 demo_fader;
// Scrollbar drag state
B32 scrollbar_dragging;
F32 scrollbar_drag_start_y;
F32 scrollbar_drag_start_scroll;
// Audio device selection
S32 audio_device_sel; // dropdown index: 0 = None, 1+ = device
S32 audio_device_prev; // previous selection for change detection
// UI scale (Cmd+/Cmd- to zoom)
F32 ui_scale;
// Panel sizes (in unscaled px, will be multiplied by uis())
F32 browser_width; // default 200
F32 right_col_width; // default 250
F32 log_height; // default 180
// Panel drag state
S32 panel_drag; // 0=none, 1=browser, 2=right, 3=log
F32 panel_drag_start_mouse; // mouse X or Y when drag started
F32 panel_drag_start_size; // panel size when drag started
};
////////////////////////////////
// Panel builders
static void build_browser_panel(AppState *app) {
if (!app->show_browser) return;
Clay_Color bp_top = g_theme.bg_medium;
Clay_Color bp_bot = {(float)Max((int)bp_top.r-8,0), (float)Max((int)bp_top.g-8,0), (float)Max((int)bp_top.b-8,0), 255};
CustomGradientData *bp_grad = alloc_gradient(bp_top, bp_bot);
CLAY(CLAY_ID("BrowserPanel"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIXED(uis(app->browser_width)), .height = CLAY_SIZING_GROW() },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.backgroundColor = bp_top,
.custom = { .customData = bp_grad },
) {
{
S32 sel = 0;
static const char *browser_tabs[] = { "Browser" };
ui_tab_bar("BrowserTabRow", browser_tabs, 1, &sel);
}
CLAY(CLAY_ID("BrowserContent"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.padding = { uip(8), uip(8), uip(6), uip(6) },
.childGap = uip(4),
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
) {
// Top highlight (beveled edge)
CLAY(CLAY_ID("BrowserHighlight"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
.backgroundColor = g_theme.bg_lighter
) {}
CLAY_TEXT(CLAY_STRING("Instruments"), &g_text_config_normal);
}
}
}
static void build_main_panel(AppState *app) {
Clay_Color mp_top = g_theme.bg_light;
Clay_Color mp_bot = {(float)Max((int)mp_top.r-8,0), (float)Max((int)mp_top.g-8,0), (float)Max((int)mp_top.b-8,0), 255};
CustomGradientData *mp_grad = alloc_gradient(mp_top, mp_bot);
CLAY(CLAY_ID("MainPanel"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.backgroundColor = mp_top,
.custom = { .customData = mp_grad }
) {
{
S32 sel = 0;
static const char *main_tabs[] = { "Main" };
ui_tab_bar("MainTabRow", main_tabs, 1, &sel);
}
CLAY(CLAY_ID("MainScrollArea"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
) {
CLAY(CLAY_ID("MainContent"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.padding = { uip(16), uip(16), uip(12), uip(12) },
.childGap = uip(12),
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() },
) {
// Top highlight (beveled edge)
CLAY(CLAY_ID("MainHighlight"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
.backgroundColor = g_theme.bg_lighter
) {}
// Section: Buttons
ui_label("LblButtons", "Buttons");
CLAY(CLAY_ID("ButtonRow"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
.childGap = uip(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 = uip(8),
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
) {
CLAY(CLAY_ID("TextCol1"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
.childGap = uip(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 = uip(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(uis(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);
}
CLAY(CLAY_ID("Sep5"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
.backgroundColor = g_theme.border
) {}
// Section: Knobs
ui_label("LblKnobs", "Knobs");
CLAY(CLAY_ID("KnobRow"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
.childGap = uip(16),
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
) {
ui_knob("KnobVolume", "Volume", &app->demo_knob_unsigned, 100.0f, 0, 75.0f, 1);
ui_knob("KnobPan", "Pan", &app->demo_knob_signed, 50.0f, 1, 0.0f, 1);
}
CLAY(CLAY_ID("Sep6"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
.backgroundColor = g_theme.border
) {}
// Section: Sliders
ui_label("LblSliders", "Sliders");
CLAY(CLAY_ID("SliderHRow"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
.childGap = uip(16),
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
) {
ui_slider_h("SliderH", "Horizontal", &app->demo_slider_h, 100.0f, 0, 50.0f, 1);
}
CLAY(CLAY_ID("SliderVRow"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
.childGap = uip(24),
.childAlignment = { .y = CLAY_ALIGN_Y_TOP },
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
) {
ui_slider_v("SliderV", "Vertical", &app->demo_slider_v, 100.0f, 0, 75.0f, 1);
ui_fader("Fader1", "Fader", &app->demo_fader, 50.0f, 1, 0.0f, 1);
}
CLAY(CLAY_ID("Sep7"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
.backgroundColor = g_theme.border
) {}
// Section: Windows & Modals
ui_label("LblWindows", "Windows & Modals");
CLAY(CLAY_ID("WindowBtnRow"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
.childGap = uip(8),
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
) {
if (ui_button("BtnSettings", "Settings")) {
app->show_settings_window = 1;
}
if (ui_button("BtnAbout", "About")) {
app->show_about_window = 1;
}
if (ui_button("BtnConfirm", "Confirm Dialog")) {
app->show_confirm_dialog = 1;
}
}
}
// Scrollbar
{
Clay_ScrollContainerData scroll_data = Clay_GetScrollContainerData(CLAY_ID("MainContent"));
if (scroll_data.found && scroll_data.contentDimensions.height > scroll_data.scrollContainerDimensions.height) {
float track_h = scroll_data.scrollContainerDimensions.height;
float content_h = scroll_data.contentDimensions.height;
float visible_ratio = track_h / content_h;
float thumb_h = Max(visible_ratio * track_h, uis(24));
float scroll_range = content_h - track_h;
float scroll_pct = scroll_range > 0 ? -scroll_data.scrollPosition->y / scroll_range : 0;
float thumb_y = scroll_pct * (track_h - thumb_h);
float bar_w = uis(8);
// Handle scrollbar drag
Clay_ElementId thumb_id = CLAY_ID("MainScrollThumb");
Clay_ElementId track_id = CLAY_ID("MainScrollTrack");
B32 thumb_hovered = Clay_PointerOver(thumb_id);
B32 track_hovered = Clay_PointerOver(track_id);
PlatformInput input = g_wstate.input;
B32 mouse_clicked = input.mouse_down && !input.was_mouse_down;
if (mouse_clicked && thumb_hovered) {
app->scrollbar_dragging = true;
app->scrollbar_drag_start_y = input.mouse_pos.y;
app->scrollbar_drag_start_scroll = scroll_data.scrollPosition->y;
} else if (mouse_clicked && track_hovered && !thumb_hovered) {
// Click on track: jump scroll position so thumb centers on click
Clay_BoundingBox track_bb = Clay_GetElementData(track_id).boundingBox;
float click_rel = input.mouse_pos.y - track_bb.y;
float target_pct = (click_rel - thumb_h / 2) / (track_h - thumb_h);
if (target_pct < 0) target_pct = 0;
if (target_pct > 1) target_pct = 1;
scroll_data.scrollPosition->y = -target_pct * scroll_range;
// Start dragging from new position
app->scrollbar_dragging = true;
app->scrollbar_drag_start_y = input.mouse_pos.y;
app->scrollbar_drag_start_scroll = scroll_data.scrollPosition->y;
}
if (!input.mouse_down) {
app->scrollbar_dragging = false;
}
if (app->scrollbar_dragging) {
float dy = input.mouse_pos.y - app->scrollbar_drag_start_y;
float scroll_per_px = scroll_range / (track_h - thumb_h);
float new_scroll = app->scrollbar_drag_start_scroll - dy * scroll_per_px;
if (new_scroll > 0) new_scroll = 0;
if (new_scroll < -scroll_range) new_scroll = -scroll_range;
scroll_data.scrollPosition->y = new_scroll;
}
// Thumb color: highlight on hover or drag
Clay_Color thumb_color = g_theme.scrollbar_grab;
if (app->scrollbar_dragging || thumb_hovered) {
thumb_color = Clay_Color{
(float)Min((int)thumb_color.r + 30, 255),
(float)Min((int)thumb_color.g + 30, 255),
(float)Min((int)thumb_color.b + 30, 255),
thumb_color.a
};
}
CLAY(track_id,
.layout = {
.sizing = { .width = CLAY_SIZING_FIXED(bar_w), .height = CLAY_SIZING_GROW() },
},
.backgroundColor = g_theme.scrollbar_bg
) {
CLAY(thumb_id,
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(thumb_h) },
},
.backgroundColor = thumb_color,
.cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS),
.floating = {
.offset = { 0, thumb_y },
.attachPoints = {
.element = CLAY_ATTACH_POINT_LEFT_TOP,
.parent = CLAY_ATTACH_POINT_LEFT_TOP,
},
.attachTo = CLAY_ATTACH_TO_PARENT,
},
) {}
}
} else {
app->scrollbar_dragging = false;
}
}
} // MainScrollArea
}
}
static void build_right_panel(AppState *app) {
static const char *right_tabs[] = { "Properties", "MIDI Devices" };
Clay_Color rp_top = g_theme.bg_medium;
Clay_Color rp_bot = {(float)Max((int)rp_top.r-8,0), (float)Max((int)rp_top.g-8,0), (float)Max((int)rp_top.b-8,0), 255};
CustomGradientData *rp_grad = alloc_gradient(rp_top, rp_bot);
CLAY(CLAY_ID("RightPanel"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.backgroundColor = rp_top,
.custom = { .customData = rp_grad }
) {
ui_tab_bar("RightTabs", right_tabs, 2, &app->right_panel_tab);
if (app->right_panel_tab == 0) {
// Properties content
CLAY(CLAY_ID("PropertiesContent"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.padding = { uip(8), uip(8), uip(6), uip(6) },
.childGap = uip(4),
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
) {
// Top highlight (beveled edge)
CLAY(CLAY_ID("PropsHighlight"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
.backgroundColor = g_theme.bg_lighter
) {}
CLAY_TEXT(CLAY_STRING("Details"), &g_text_config_normal);
}
} else {
// MIDI Devices content
MidiEngine *midi = app->midi;
CLAY(CLAY_ID("MidiContent"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.padding = { uip(8), uip(8), uip(6), uip(6) },
.childGap = uip(6),
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
) {
// Top highlight (beveled edge)
CLAY(CLAY_ID("MidiHighlight"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
.backgroundColor = g_theme.bg_lighter
) {}
// Refresh button
Clay_ElementId refresh_eid = CLAY_ID("MidiRefreshBtn");
B32 refresh_hovered = Clay_PointerOver(refresh_eid);
CLAY(refresh_eid,
.layout = {
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(uis(28)) },
.padding = { uip(12), uip(12), 0, 0 },
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
},
.backgroundColor = refresh_hovered ? g_theme.accent_hover : g_theme.bg_lighter,
.cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS)
) {
CLAY_TEXT(CLAY_STRING("Refresh"), &g_text_config_normal);
}
if (refresh_hovered && g_wstate.mouse_clicked) {
midi_refresh_devices(midi);
}
static char device_bufs[64][128];
int32_t device_count = midi_get_device_count(midi);
// --- Inputs section ---
CLAY_TEXT(CLAY_STRING("Inputs"), &g_text_config_dim);
static const char *note_names[] = {"C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"};
static char note_bufs[64][8];
static char vel_bufs[64][8];
static Clay_TextElementConfig box_text_config;
box_text_config = {};
box_text_config.textColor = Clay_Color{255, 255, 255, 255};
box_text_config.fontSize = FONT_SIZE_SMALL;
box_text_config.wrapMode = CLAY_TEXT_WRAP_NONE;
B32 has_inputs = 0;
for (int32_t i = 0; i < device_count && i < 64; i++) {
MidiDeviceInfo *dev = midi_get_device(midi, i);
if (!dev->is_input) continue;
has_inputs = 1;
int len = snprintf(device_bufs[i], sizeof(device_bufs[i]), "%s", dev->name);
Clay_String device_str = { .isStaticallyAllocated = false, .length = len, .chars = device_bufs[i] };
// Velocity-based color: blue (vel 0) → green (mid) → red (vel 127)
// Idle = dark gray
Clay_Color box_color;
if (dev->active) {
float t = (float)dev->velocity / 127.0f;
float r, g, b;
if (t < 0.5f) {
float s = t * 2.0f;
r = 40.0f + s * (76.0f - 40.0f);
g = 120.0f + s * (175.0f - 120.0f);
b = 220.0f + s * (80.0f - 220.0f);
} else {
float s = (t - 0.5f) * 2.0f;
r = 76.0f + s * (220.0f - 76.0f);
g = 175.0f + s * (50.0f - 175.0f);
b = 80.0f + s * (40.0f - 80.0f);
}
box_color = Clay_Color{r, g, b, 255};
} else if (dev->releasing) {
box_color = Clay_Color{255, 255, 255, 255};
} else {
box_color = Clay_Color{60, 60, 60, 255};
}
// Box text: note name when held, "OFF" when releasing, "---" when idle
int nlen;
if (dev->active) {
int pitch = dev->note % 12;
int octave = (dev->note / 12) - 1;
nlen = snprintf(note_bufs[i], sizeof(note_bufs[i]), "%s%d", note_names[pitch], octave);
} else if (dev->releasing) {
nlen = snprintf(note_bufs[i], sizeof(note_bufs[i]), "OFF");
} else {
nlen = snprintf(note_bufs[i], sizeof(note_bufs[i]), "---");
}
Clay_String note_str = { .isStaticallyAllocated = false, .length = nlen, .chars = note_bufs[i] };
// Box text color: dark for white bg (releasing), white otherwise
Clay_TextElementConfig *box_txt = &box_text_config;
static Clay_TextElementConfig box_text_dark;
box_text_dark = box_text_config;
box_text_dark.textColor = Clay_Color{30, 30, 30, 255};
if (dev->releasing) box_txt = &box_text_dark;
// Velocity text
int vlen;
if (dev->active)
vlen = snprintf(vel_bufs[i], sizeof(vel_bufs[i]), "%d", dev->velocity);
else
vlen = snprintf(vel_bufs[i], sizeof(vel_bufs[i]), "");
Clay_String vel_str = { .isStaticallyAllocated = false, .length = vlen, .chars = vel_bufs[i] };
CLAY(CLAY_IDI("MidiIn", i),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
.padding = { uip(4), uip(4), uip(2), uip(2) },
.childGap = uip(6),
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
) {
// Note name box (colored by velocity)
CLAY(CLAY_IDI("MidiInNote", i),
.layout = {
.sizing = { .width = CLAY_SIZING_FIXED(uis(36)), .height = CLAY_SIZING_FIXED(uis(18)) },
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
},
.backgroundColor = box_color,
.cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS)
) {
CLAY_TEXT(note_str, box_txt);
}
// Velocity number
CLAY_TEXT(vel_str, &g_text_config_dim);
// Device name
CLAY_TEXT(device_str, &g_text_config_normal);
}
}
if (!has_inputs) {
CLAY_TEXT(CLAY_STRING(" No MIDI inputs"), &g_text_config_dim);
}
// --- Outputs section ---
CLAY_TEXT(CLAY_STRING("Outputs"), &g_text_config_dim);
B32 has_outputs = 0;
for (int32_t i = 0; i < device_count && i < 64; i++) {
MidiDeviceInfo *dev = midi_get_device(midi, i);
if (dev->is_input) continue;
has_outputs = 1;
int len = snprintf(device_bufs[i], sizeof(device_bufs[i]), "%s", dev->name);
Clay_String device_str = { .isStaticallyAllocated = false, .length = len, .chars = device_bufs[i] };
CLAY(CLAY_IDI("MidiOut", i),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
.padding = { uip(4), uip(4), uip(2), uip(2) },
}
) {
CLAY_TEXT(device_str, &g_text_config_normal);
}
}
if (!has_outputs) {
CLAY_TEXT(CLAY_STRING(" No MIDI outputs"), &g_text_config_dim);
}
}
}
}
}
static void build_log_panel(AppState *app) {
if (!app->show_log) return;
Clay_Color lp_top = g_theme.bg_medium;
Clay_Color lp_bot = {(float)Max((int)lp_top.r-8,0), (float)Max((int)lp_top.g-8,0), (float)Max((int)lp_top.b-8,0), 255};
CustomGradientData *lp_grad = alloc_gradient(lp_top, lp_bot);
CLAY(CLAY_ID("LogPanel"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(uis(app->log_height)) },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.backgroundColor = lp_top,
.custom = { .customData = lp_grad },
) {
{
S32 sel = 0;
static const char *log_tabs[] = { "Log" };
ui_tab_bar("LogTabRow", log_tabs, 1, &sel);
}
CLAY(CLAY_ID("LogContent"),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
.padding = { uip(8), uip(8), uip(6), uip(6) },
.childGap = uip(4),
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
) {
// Top highlight (beveled edge)
CLAY(CLAY_ID("LogHighlight"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
.backgroundColor = g_theme.bg_lighter
) {}
CLAY_TEXT(CLAY_STRING("Output / Log"), &g_text_config_normal);
}
}
}
////////////////////////////////
// Corner radius presets (indexed by AppState::radius_sel)
static const F32 radius_values[] = { 0.0f, 4.0f, 6.0f, 10.0f };
// Window content callbacks
static void settings_window_content(void *user_data) {
AppState *app = (AppState *)user_data;
ui_label("SettingsLblTheme", "Theme");
static const char *theme_options[] = { "Dark", "Light" };
CLAY(CLAY_ID("SettingsThemeWrap"),
.layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(180)), .height = CLAY_SIZING_FIT() } }
) {
ui_dropdown("SettingsTheme", theme_options, 2, &app->settings_theme_sel);
}
ui_label("SettingsLblAccent", "Accent Color");
static const char *accent_options[] = { "Blue", "Turquoise", "Orange", "Purple", "Pink", "Red", "Green" };
CLAY(CLAY_ID("SettingsAccentWrap"),
.layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(180)), .height = CLAY_SIZING_FIT() } }
) {
ui_dropdown("SettingsAccent", accent_options, 7, &app->accent_sel);
}
ui_label("SettingsLblRadius", "Corner Radius");
static const char *radius_options[] = { "None", "Small", "Medium", "Large" };
// radius_values defined at file scope (used by theme change handler too)
CLAY(CLAY_ID("SettingsRadiusWrap"),
.layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(180)), .height = CLAY_SIZING_FIT() } }
) {
if (ui_dropdown("SettingsRadius", radius_options, 4, &app->radius_sel)) {
g_theme.corner_radius = radius_values[app->radius_sel];
}
}
ui_checkbox("SettingsVsync", "V-Sync", &app->settings_vsync);
ui_checkbox("SettingsAutosave", "Autosave", &app->settings_autosave);
// Separator
CLAY(CLAY_ID("SettingsSep1"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) },
.padding = { 0, 0, uip(6), uip(6) } },
.backgroundColor = g_theme.border
) {}
// Audio device dropdown
ui_label("SettingsLblAudio", "Audio Device");
static const char *audio_options[AUDIO_MAX_DEVICES + 1];
int32_t audio_count = audio_get_device_count(app->audio);
audio_options[0] = "None";
for (int32_t i = 0; i < audio_count && i < AUDIO_MAX_DEVICES; i++) {
AudioDeviceInfo *dev = audio_get_device(app->audio, i);
audio_options[i + 1] = dev ? dev->name : "???";
}
CLAY(CLAY_ID("SettingsAudioWrap"),
.layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(220)), .height = CLAY_SIZING_FIT() } }
) {
ui_dropdown("SettingsAudio", audio_options, audio_count + 1, &app->audio_device_sel);
}
// Handle device selection change
if (app->audio_device_sel != app->audio_device_prev) {
audio_close_device(app->audio);
if (app->audio_device_sel > 0) {
audio_open_device(app->audio, app->audio_device_sel - 1);
}
app->audio_device_prev = app->audio_device_sel;
}
// Test tone button + status
CLAY(CLAY_ID("SettingsAudioBtnRow"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
.childGap = uip(8),
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
) {
bool device_open = (app->audio_device_sel > 0);
bool tone_playing = audio_is_test_tone_playing(app->audio);
if (device_open && !tone_playing) {
if (ui_button("BtnTestTone", "Play Test Tone")) {
audio_play_test_tone(app->audio);
}
} else {
// Disabled button appearance
Clay_ElementId btn_eid = CLAY_ID("BtnTestToneDisabled");
CLAY(btn_eid,
.layout = {
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(uis(28)) },
.padding = { uip(12), uip(12), 0, 0 },
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
},
.backgroundColor = g_theme.disabled_bg,
.cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS)
) {
static Clay_TextElementConfig disabled_text = {};
disabled_text.textColor = g_theme.disabled_text;
disabled_text.fontSize = FONT_SIZE_NORMAL;
disabled_text.wrapMode = CLAY_TEXT_WRAP_NONE;
CLAY_TEXT(CLAY_STRING("Play Test Tone"), &disabled_text);
}
}
if (tone_playing) {
ui_label("SettingsLblPlaying", "Playing...");
}
}
}
static void about_window_content(void *user_data) {
(void)user_data;
ui_label("AboutLbl1", "autosample v0.1");
ui_label("AboutLbl2", "An audio sampling workstation.");
ui_label("AboutLbl3", "Built with Clay UI.");
}
////////////////////////////////
// Panel splitter drag logic (called each frame before build_ui)
static void update_panel_splitters(AppState *app) {
PlatformInput input = g_wstate.input;
B32 mouse_clicked = input.mouse_down && !input.was_mouse_down;
B32 mouse_released = !input.mouse_down && input.was_mouse_down;
PlatformCursor cursor = PLATFORM_CURSOR_ARROW;
// Check hover on splitter elements (uses previous-frame layout)
B32 hover_browser = Clay_PointerOver(CLAY_ID("SplitBrowser"));
B32 hover_right = Clay_PointerOver(CLAY_ID("SplitRight"));
B32 hover_log = Clay_PointerOver(CLAY_ID("SplitLog"));
// Start drag
if (mouse_clicked && app->panel_drag == 0) {
if (hover_browser) {
app->panel_drag = 1;
app->panel_drag_start_mouse = input.mouse_pos.x;
app->panel_drag_start_size = app->browser_width;
} else if (hover_right) {
app->panel_drag = 2;
app->panel_drag_start_mouse = input.mouse_pos.x;
app->panel_drag_start_size = app->right_col_width;
} else if (hover_log) {
app->panel_drag = 3;
app->panel_drag_start_mouse = input.mouse_pos.y;
app->panel_drag_start_size = app->log_height;
}
}
// During drag
if (app->panel_drag == 1) {
F32 delta = (input.mouse_pos.x - app->panel_drag_start_mouse) / g_ui_scale;
app->browser_width = Clamp(100.0f, app->panel_drag_start_size + delta, 500.0f);
cursor = PLATFORM_CURSOR_SIZE_WE;
} else if (app->panel_drag == 2) {
F32 delta = (input.mouse_pos.x - app->panel_drag_start_mouse) / g_ui_scale;
// Right panel: dragging left increases width, right decreases
app->right_col_width = Clamp(150.0f, app->panel_drag_start_size - delta, 500.0f);
cursor = PLATFORM_CURSOR_SIZE_WE;
} else if (app->panel_drag == 3) {
F32 delta = (input.mouse_pos.y - app->panel_drag_start_mouse) / g_ui_scale;
// Log panel: dragging up increases height, down decreases
app->log_height = Clamp(80.0f, app->panel_drag_start_size - delta, 500.0f);
cursor = PLATFORM_CURSOR_SIZE_NS;
}
// End drag
if (mouse_released) {
app->panel_drag = 0;
}
// Set cursor on hover (when not dragging)
if (app->panel_drag == 0) {
if (hover_browser || hover_right) cursor = PLATFORM_CURSOR_SIZE_WE;
else if (hover_log) cursor = PLATFORM_CURSOR_SIZE_NS;
}
platform_set_cursor(cursor);
}
////////////////////////////////
// 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);
// Browser splitter (vertical, 4px wide)
if (app->show_browser) {
CLAY(CLAY_ID("SplitBrowser"),
.layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(4)), .height = CLAY_SIZING_GROW() } },
.backgroundColor = g_theme.border
) {}
}
build_main_panel(app);
if (app->show_props || app->show_midi_devices) {
// Right splitter (vertical, 4px wide)
CLAY(CLAY_ID("SplitRight"),
.layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(4)), .height = CLAY_SIZING_GROW() } },
.backgroundColor = g_theme.border
) {}
CLAY(CLAY_ID("RightColumn"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIXED(uis(app->right_col_width)), .height = CLAY_SIZING_GROW() },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
) {
build_right_panel(app);
}
}
}
// Log splitter (horizontal, 4px tall)
if (app->show_log) {
CLAY(CLAY_ID("SplitLog"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(uis(4)) } },
.backgroundColor = g_theme.border
) {}
}
build_log_panel(app);
}
// Draggable windows (rendered as floating elements above normal UI)
ui_window("WinSettings", "Settings", &app->show_settings_window,
Vec2F32{100, 100}, Vec2F32{280, 0},
settings_window_content, app);
ui_window("WinAbout", "About", &app->show_about_window,
Vec2F32{200, 150}, Vec2F32{260, 0},
about_window_content, nullptr);
// Modal confirmation dialog
if (app->show_confirm_dialog) {
static const char *confirm_buttons[] = { "Cancel", "OK" };
S32 modal_result = ui_modal("ModalConfirm", "Confirm Action",
"Are you sure you want to proceed? This action cannot be undone.",
confirm_buttons, 2);
if (modal_result != -1) {
app->show_confirm_dialog = 0;
}
}
}
////////////////////////////////
// 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
#ifdef __APPLE__
uint64_t now = mach_absolute_time();
F32 dt = (F32)(now - app->last_time) * (F32)app->freq_numer / ((F32)app->freq_denom * 1e9f);
app->last_time = now;
#else
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
F32 dt = (F32)(now.QuadPart - app->last_time.QuadPart) / (F32)app->freq.QuadPart;
app->last_time = now;
#endif
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;
// Update subsystems
midi_update(app->midi, dt);
audio_update(app->audio, dt);
// Gather input
PlatformInput input = platform_get_input(app->window);
// Cmd+= / Cmd+- (or Ctrl on Windows) to zoom UI, Cmd+0 to reset
for (S32 k = 0; k < input.key_count; k++) {
if (input.ctrl_held) {
if (input.keys[k] == PKEY_EQUAL) app->ui_scale *= 1.1f;
if (input.keys[k] == PKEY_MINUS) app->ui_scale /= 1.1f;
if (input.keys[k] == PKEY_0) app->ui_scale = 1.0f;
}
}
app->ui_scale = Clamp(0.5f, app->ui_scale, 3.0f);
g_ui_scale = app->ui_scale;
renderer_set_font_scale(app->renderer, app->ui_scale);
// Handle theme change
if (app->settings_theme_sel != app->settings_theme_prev) {
ui_set_theme(app->settings_theme_sel);
ui_set_accent(app->accent_sel); // reapply accent on new base theme
g_theme.corner_radius = radius_values[app->radius_sel]; // preserve user radius
app->settings_theme_prev = app->settings_theme_sel;
// Refresh all text configs with new theme colors
g_text_config_normal.textColor = g_theme.text;
g_text_config_title.textColor = g_theme.text;
g_text_config_dim.textColor = g_theme.text_dim;
ui_widgets_theme_changed();
// Update renderer clear color to match theme
renderer_set_clear_color(app->renderer,
g_theme.bg_dark.r / 255.f, g_theme.bg_dark.g / 255.f, g_theme.bg_dark.b / 255.f);
}
// Handle accent change
if (app->accent_sel != app->accent_prev) {
ui_set_accent(app->accent_sel);
app->accent_prev = app->accent_sel;
g_text_config_normal.textColor = g_theme.text;
g_text_config_title.textColor = g_theme.text;
g_text_config_dim.textColor = g_theme.text_dim;
ui_widgets_theme_changed();
}
// Update text configs when scale changes
if (g_text_config_normal.fontSize != FONT_SIZE_NORMAL) {
g_text_config_normal.fontSize = FONT_SIZE_NORMAL;
g_text_config_title.fontSize = FONT_SIZE_NORMAL;
g_text_config_dim.fontSize = FONT_SIZE_NORMAL;
}
ui_widgets_begin_frame(input);
update_panel_splitters(app);
// Build UI with Clay
ui_begin_frame(app->ui, (F32)w, (F32)h, input.mouse_pos, input.mouse_down,
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();
AudioEngine *audio = audio_create(platform_get_native_handle(window));
// Initialize UI (Clay)
ui_set_theme(0);
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();
// Rasterize icon atlas and upload to GPU
{
S32 iw, ih;
U8 *icon_atlas = ui_icons_rasterize_atlas(&iw, &ih, 48);
if (icon_atlas) {
renderer_create_icon_atlas(renderer, icon_atlas, iw, ih);
free(icon_atlas);
}
}
AppState app = {};
app.window = window;
app.renderer = renderer;
app.midi = midi;
app.audio = audio;
app.ui = ui;
app.last_w = w;
app.last_h = h;
app.ui_scale = 1.0f;
app.show_browser = 1;
app.show_props = 1;
app.show_log = 1;
app.show_midi_devices = 1;
app.demo_knob_unsigned = 75.0f;
app.demo_knob_signed = 0.0f;
app.demo_slider_h = 50.0f;
app.demo_slider_v = 75.0f;
app.demo_fader = 0.0f;
app.demo_dropdown_sel = 1; // default to 48000 Hz
app.radius_sel = 1; // default to "Small" (4.0f)
app.browser_width = 200.0f;
app.right_col_width = 250.0f;
app.log_height = 180.0f;
app.panel_drag = 0;
snprintf(app.demo_text_a, sizeof(app.demo_text_a), "My Instrument");
#ifdef __APPLE__
snprintf(app.demo_text_b, sizeof(app.demo_text_b), "~/Samples/output");
{ mach_timebase_info_data_t tbi; mach_timebase_info(&tbi); app.freq_numer = tbi.numer; app.freq_denom = tbi.denom; }
app.last_time = mach_absolute_time();
#else
snprintf(app.demo_text_b, sizeof(app.demo_text_b), "C:\\Samples\\output");
QueryPerformanceFrequency(&app.freq);
QueryPerformanceCounter(&app.last_time);
#endif
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:
audio_destroy(audio);
midi_destroy(midi);
ui_destroy(ui);
renderer_destroy(renderer);
platform_destroy_window(window);
return 0;
}