// Unity build - include all src files here // -mta #ifdef __APPLE__ #include #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; }