// 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" #include "ui/ui_popups.h" #include "ui/ui_piano.h" // [cpp] #include "base/base_inc.cpp" #include "ui/ui_core.cpp" #include "ui/ui_icons.cpp" #include "ui/ui_widgets.cpp" #include "ui/ui_popups.cpp" #include "ui/ui_piano.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_vulkan.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__ U64 freq_numer; U64 freq_denom; U64 last_time; #else LARGE_INTEGER freq; LARGE_INTEGER last_time; #endif // Tab state S32 right_panel_tab; // 0 = Properties, 1 = MIDI Devices S32 bottom_panel_tab; // 0 = Item Editor, 1 = Sample Mapper // Piano state UI_PianoState piano_state; // Demo widget state B32 demo_checkbox_a; B32 demo_checkbox_b; S32 demo_radio_sel; S32 demo_dropdown_sel; char demo_text_a[128]; char demo_text_b[128]; S32 demo_button_count; // Modal / window demo state B32 show_settings_window; B32 show_about_window; B32 show_confirm_dialog; // Pop-out windows B32 show_mix_popout; B32 show_patch_popout; 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; // Settings dialog tab S32 settings_tab; // 0=Audio, 1=Midi, 2=Keyboard Shortcuts, 3=Appearance // 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 // Master layout: 0 = Edit, 1 = Mix, 2 = Patch S32 master_layout; // Mix view fader state (8 channels + master) F32 mix_faders[8]; F32 mix_pans[8]; F32 mix_master_pan; // Patch view state S32 patch_tab; // 0 = Matrix, 1 = Graph B32 patch_matrix[32][33]; // internal routing: 32 inputs x (Master + 32 outputs) B32 hw_matrix[32][32]; // hardware routing: 32 channel outputs x 32 hw outputs }; //////////////////////////////// //////////////////////////////// // 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 = {(F32)Max((S32)bp_top.r-8,0), (F32)Max((S32)bp_top.g-8,0), (F32)Max((S32)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 = {(F32)Max((S32)mp_top.r-8,0), (F32)Max((S32)mp_top.g-8,0), (F32)Max((S32)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("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) { F32 track_h = scroll_data.scrollContainerDimensions.height; F32 content_h = scroll_data.contentDimensions.height; F32 visible_ratio = track_h / content_h; F32 thumb_h = Max(visible_ratio * track_h, uis(24)); F32 scroll_range = content_h - track_h; F32 scroll_pct = scroll_range > 0 ? -scroll_data.scrollPosition->y / scroll_range : 0; F32 thumb_y = scroll_pct * (track_h - thumb_h); F32 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; F32 click_rel = input.mouse_pos.y - track_bb.y; F32 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) { F32 dy = input.mouse_pos.y - app->scrollbar_drag_start_y; F32 scroll_per_px = scroll_range / (track_h - thumb_h); F32 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{ (F32)Min((S32)thumb_color.r + 30, 255), (F32)Min((S32)thumb_color.g + 30, 255), (F32)Min((S32)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 = {(F32)Max((S32)rp_top.r-8,0), (F32)Max((S32)rp_top.g-8,0), (F32)Max((S32)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]; S32 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 (S32 i = 0; i < device_count && i < 64; i++) { MidiDeviceInfo *dev = midi_get_device(midi, i); if (!dev->is_input) continue; has_inputs = 1; S32 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) { box_color = piano_velocity_color(dev->velocity); } 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 S32 nlen; if (dev->active) { S32 pitch = dev->note % 12; S32 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 S32 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 (S32 i = 0; i < device_count && i < 64; i++) { MidiDeviceInfo *dev = midi_get_device(midi, i); if (dev->is_input) continue; has_outputs = 1; S32 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 = {(F32)Max((S32)lp_top.r-8,0), (F32)Max((S32)lp_top.g-8,0), (F32)Max((S32)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 }, ) { static const char *bottom_tabs[] = { "Item Editor", "Sample Mapper" }; ui_tab_bar("BottomTabRow", bottom_tabs, 2, &app->bottom_panel_tab); if (app->bottom_panel_tab == 0) { // Item Editor tab CLAY(CLAY_ID("ItemEditorContent"), .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, } ) { CLAY(CLAY_ID("ItemEditorHighlight"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } }, .backgroundColor = g_theme.bg_lighter ) {} CLAY_TEXT(CLAY_STRING("Item Editor"), &g_text_config_normal); } } else { // Sample Mapper tab — 88-key piano CLAY(CLAY_ID("SampleMapperContent"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, .padding = { uip(4), uip(4), uip(4), uip(4) }, .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { F32 piano_avail_h = uis(app->log_height) - TAB_HEIGHT - uip(8); F32 piano_avail_w = (F32)app->last_w - uip(8); ui_piano(&app->piano_state, app->midi, piano_avail_w, piano_avail_h); } } } } //////////////////////////////// // 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; static const char *settings_tabs[] = { "Audio", "MIDI", "Keyboard Shortcuts", "Appearance" }; ui_tab_bar("SettingsTabs", settings_tabs, 4, &app->settings_tab); // Tab content area with some top padding CLAY(CLAY_ID("SettingsTabContent"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, .padding = { 0, 0, uip(8), 0 }, .childGap = uip(6), .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { if (app->settings_tab == 0) { // === Audio tab === ui_label("SettingsLblAudio", "Audio Device"); static const char *audio_options[AUDIO_MAX_DEVICES + 1]; S32 audio_count = audio_get_device_count(app->audio); audio_options[0] = "None"; for (S32 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(260)), .height = CLAY_SIZING_FIT() } } ) { ui_dropdown("SettingsAudio", audio_options, audio_count + 1, &app->audio_device_sel); } 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; } 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, } ) { B32 device_open = (app->audio_device_sel > 0); B32 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 { 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..."); } } ui_checkbox("SettingsVsync", "V-Sync", &app->settings_vsync); } else if (app->settings_tab == 1) { // === MIDI tab === ui_label("SettingsLblMidi", "MIDI settings will go here."); } else if (app->settings_tab == 2) { // === Keyboard Shortcuts tab === ui_label("SettingsLblKeys", "Keyboard shortcut settings will go here."); } else if (app->settings_tab == 3) { // === Appearance tab === ui_label("SettingsLblTheme", "Theme"); static const char *theme_options[] = { "Dark", "Light" }; CLAY(CLAY_ID("SettingsThemeWrap"), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(220)), .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(220)), .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" }; CLAY(CLAY_ID("SettingsRadiusWrap"), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(220)), .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("SettingsAutosave", "Autosave", &app->settings_autosave); } } } 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) { if (app->master_layout != 0) { platform_set_cursor(PLATFORM_CURSOR_ARROW); return; } 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 with expanded hit area (splitters are 1px) F32 grab_margin = uis(3); F32 mx = input.mouse_pos.x, my = input.mouse_pos.y; B32 hover_browser = false, hover_right = false, hover_log = false; { Clay_ElementData d = Clay_GetElementData(CLAY_ID("SplitBrowser")); if (d.found) { Clay_BoundingBox b = d.boundingBox; hover_browser = mx >= b.x - grab_margin && mx <= b.x + b.width + grab_margin && my >= b.y && my <= b.y + b.height; } } { Clay_ElementData d = Clay_GetElementData(CLAY_ID("SplitRight")); if (d.found) { Clay_BoundingBox b = d.boundingBox; hover_right = mx >= b.x - grab_margin && mx <= b.x + b.width + grab_margin && my >= b.y && my <= b.y + b.height; } } { Clay_ElementData d = Clay_GetElementData(CLAY_ID("SplitLog")); if (d.found) { Clay_BoundingBox b = d.boundingBox; hover_log = mx >= b.x && mx <= b.x + b.width && my >= b.y - grab_margin && my <= b.y + b.height + grab_margin; } } // 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); } //////////////////////////////// // Header bar — transport / layout toggle static void build_header_bar(AppState *app) { Clay_Color bar_bg = g_theme.header_bg; Clay_Color border_bot = {(F32)Max((S32)bar_bg.r - 12, 0), (F32)Max((S32)bar_bg.g - 12, 0), (F32)Max((S32)bar_bg.b - 12, 0), 255}; static Clay_TextElementConfig header_btn_active_text = {}; header_btn_active_text.textColor = Clay_Color{255, 255, 255, 255}; header_btn_active_text.fontSize = FONT_SIZE_NORMAL; header_btn_active_text.wrapMode = CLAY_TEXT_WRAP_NONE; static Clay_TextElementConfig header_clock_text = {}; header_clock_text.textColor = g_theme.text; header_clock_text.fontSize = uifs(22); header_clock_text.wrapMode = CLAY_TEXT_WRAP_NONE; static Clay_TextElementConfig header_indicator_text = {}; header_indicator_text.textColor = g_theme.text; header_indicator_text.fontSize = FONT_SIZE_NORMAL; header_indicator_text.wrapMode = CLAY_TEXT_WRAP_NONE; static Clay_TextElementConfig header_indicator_label = {}; header_indicator_label.textColor = g_theme.text_dim; header_indicator_label.fontSize = FONT_SIZE_SMALL; header_indicator_label.wrapMode = CLAY_TEXT_WRAP_NONE; Clay_Color inset_bg = {(F32)Max((S32)bar_bg.r - 8, 0), (F32)Max((S32)bar_bg.g - 8, 0), (F32)Max((S32)bar_bg.b - 8, 0), 255}; CLAY(CLAY_ID("HeaderBar"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(uis(64)) }, .padding = { uip(10), uip(10), uip(6), uip(6) }, .childGap = uip(8), .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .layoutDirection = CLAY_LEFT_TO_RIGHT, }, .backgroundColor = bar_bg, .border = { .color = border_bot, .width = { .bottom = 2 } }, ) { // === LEFT: toolbar buttons === CLAY(CLAY_ID("ToolbarGroup"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() }, .childGap = uip(4), .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .layoutDirection = CLAY_LEFT_TO_RIGHT, } ) { // Rewind CLAY(CLAY_ID("TbRewind"), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(32)), .height = CLAY_SIZING_FIXED(uis(32)) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = g_theme.bg_lighter, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), ) { ui_icon(UI_ICON_TRANSPORT_REWIND, uis(16), g_theme.text_dim); } // Stop CLAY(CLAY_ID("TbStop"), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(32)), .height = CLAY_SIZING_FIXED(uis(32)) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = g_theme.bg_lighter, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), ) { ui_icon(UI_ICON_TRANSPORT_STOP, uis(14), g_theme.text_dim); } // Play CLAY(CLAY_ID("TbPlay"), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(32)), .height = CLAY_SIZING_FIXED(uis(32)) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = g_theme.bg_lighter, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), ) { ui_icon(UI_ICON_TRANSPORT_PLAY, uis(16), g_theme.text_dim); } // Record CLAY(CLAY_ID("TbRecord"), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(32)), .height = CLAY_SIZING_FIXED(uis(32)) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = g_theme.bg_lighter, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), ) { ui_icon(UI_ICON_TRANSPORT_RECORD, uis(14), Clay_Color{200, 60, 60, 255}); } } // Spacer CLAY(CLAY_ID("HeaderSpacerL"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } } ) {} // === CENTER: time display (nearly full header height) === CLAY(CLAY_ID("TimeDisplay"), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(200)), .height = CLAY_SIZING_FIXED(uis(48)) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = inset_bg, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), .border = { .color = border_bot, .width = { .left = 1, .right = 1, .top = 1, .bottom = 1 } }, ) { CLAY_TEXT(CLAY_STRING("00:00:00.000"), &header_clock_text); } // === Tempo & time sig indicators === CLAY(CLAY_ID("IndicatorGroup"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() }, .childGap = uip(10), .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .layoutDirection = CLAY_LEFT_TO_RIGHT, } ) { // Tempo CLAY(CLAY_ID("TempoBox"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(uis(48)) }, .padding = { uip(10), uip(10), 0, 0 }, .childGap = uip(2), .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, .layoutDirection = CLAY_TOP_TO_BOTTOM, }, .backgroundColor = inset_bg, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), .border = { .color = border_bot, .width = { .left = 1, .right = 1, .top = 1, .bottom = 1 } }, ) { CLAY_TEXT(CLAY_STRING("TEMPO"), &header_indicator_label); CLAY_TEXT(CLAY_STRING("120.00"), &header_indicator_text); } // Time Signature CLAY(CLAY_ID("TimeSigBox"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(uis(48)) }, .padding = { uip(10), uip(10), 0, 0 }, .childGap = uip(2), .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, .layoutDirection = CLAY_TOP_TO_BOTTOM, }, .backgroundColor = inset_bg, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), .border = { .color = border_bot, .width = { .left = 1, .right = 1, .top = 1, .bottom = 1 } }, ) { CLAY_TEXT(CLAY_STRING("SIGNATURE"), &header_indicator_label); CLAY_TEXT(CLAY_STRING("4 / 4"), &header_indicator_text); } } // Spacer CLAY(CLAY_ID("HeaderSpacerR"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } } ) {} // === RIGHT: Edit / Mix buttons === // Edit button { Clay_ElementId edit_eid = CLAY_ID("BtnLayoutEdit"); B32 edit_hovered = Clay_PointerOver(edit_eid); B32 edit_active = (app->master_layout == 0); Clay_Color edit_bg = edit_active ? g_theme.accent : g_theme.bg_lighter; if (edit_hovered && !edit_active) edit_bg = g_theme.accent_hover; CLAY(edit_eid, .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(uis(28)) }, .padding = { uip(14), uip(14), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = edit_bg, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), ) { CLAY_TEXT(CLAY_STRING("Edit"), edit_active ? &header_btn_active_text : &g_text_config_normal); } if (edit_hovered && g_wstate.mouse_clicked) { app->master_layout = 0; } } // Mix button { Clay_ElementId mix_eid = CLAY_ID("BtnLayoutMix"); B32 mix_hovered = Clay_PointerOver(mix_eid); B32 mix_active = (app->master_layout == 1); Clay_Color mix_bg = mix_active ? g_theme.accent : g_theme.bg_lighter; if (mix_hovered && !mix_active) mix_bg = g_theme.accent_hover; CLAY(mix_eid, .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(uis(28)) }, .padding = { uip(14), uip(14), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = mix_bg, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), ) { CLAY_TEXT(CLAY_STRING("Mix"), mix_active ? &header_btn_active_text : &g_text_config_normal); } if (mix_hovered && g_wstate.mouse_clicked) { PopupWindow *mix_pop = popup_find_by_flag(&app->show_mix_popout); if (mix_pop) { platform_focus_window(mix_pop->platform_window); } else { app->master_layout = 1; } } } // Mix pop-out / pop-in button { B32 mix_popped = app->show_mix_popout; Clay_ElementId pop_eid = CLAY_ID("BtnMixPopOut"); B32 pop_hovered = Clay_PointerOver(pop_eid); CLAY(pop_eid, .layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(22)), .height = CLAY_SIZING_FIXED(uis(22)) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = pop_hovered ? g_theme.accent_hover : g_theme.bg_lighter, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), ) { ui_icon(mix_popped ? UI_ICON_POP_IN : UI_ICON_POP_OUT, uis(12), g_theme.text_dim); } if (pop_hovered && g_wstate.mouse_clicked) { if (mix_popped) { PopupWindow *p = popup_find_by_flag(&app->show_mix_popout); if (p) popup_close(p); app->master_layout = 1; } else { app->show_mix_popout = 1; if (app->master_layout == 1) app->master_layout = 0; } } } // Patch button { Clay_ElementId patch_eid = CLAY_ID("BtnLayoutPatch"); B32 patch_hovered = Clay_PointerOver(patch_eid); B32 patch_active = (app->master_layout == 2); Clay_Color patch_bg = patch_active ? g_theme.accent : g_theme.bg_lighter; if (patch_hovered && !patch_active) patch_bg = g_theme.accent_hover; CLAY(patch_eid, .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(uis(28)) }, .padding = { uip(14), uip(14), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = patch_bg, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), ) { CLAY_TEXT(CLAY_STRING("Patch"), patch_active ? &header_btn_active_text : &g_text_config_normal); } if (patch_hovered && g_wstate.mouse_clicked) { PopupWindow *patch_pop = popup_find_by_flag(&app->show_patch_popout); if (patch_pop) { platform_focus_window(patch_pop->platform_window); } else { app->master_layout = 2; } } } // Patch pop-out / pop-in button { B32 patch_popped = app->show_patch_popout; Clay_ElementId pop_eid = CLAY_ID("BtnPatchPopOut"); B32 pop_hovered = Clay_PointerOver(pop_eid); CLAY(pop_eid, .layout = { .sizing = { .width = CLAY_SIZING_FIXED(uis(22)), .height = CLAY_SIZING_FIXED(uis(22)) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = pop_hovered ? g_theme.accent_hover : g_theme.bg_lighter, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), ) { ui_icon(patch_popped ? UI_ICON_POP_IN : UI_ICON_POP_OUT, uis(12), g_theme.text_dim); } if (pop_hovered && g_wstate.mouse_clicked) { if (patch_popped) { PopupWindow *p = popup_find_by_flag(&app->show_patch_popout); if (p) popup_close(p); app->master_layout = 2; } else { app->show_patch_popout = 1; if (app->master_layout == 2) app->master_layout = 0; } } } } } //////////////////////////////// // Mix view — channel strip faders static void build_mix_view(AppState *app) { Clay_Color mv_top = g_theme.bg_medium; Clay_Color mv_bot = {(F32)Max((S32)mv_top.r - 8, 0), (F32)Max((S32)mv_top.g - 8, 0), (F32)Max((S32)mv_top.b - 8, 0), 255}; CustomGradientData *mv_grad = alloc_gradient(mv_top, mv_bot); CLAY(CLAY_ID("MixView"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, .padding = { uip(4), uip(4), uip(4), uip(4) }, .childGap = 0, .layoutDirection = CLAY_LEFT_TO_RIGHT, }, .backgroundColor = mv_top, .custom = { .customData = mv_grad }, ) { static char ch_label_bufs[8][8]; static char fader_id_bufs[8][16]; static char pan_id_bufs[8][16]; for (S32 i = 0; i < 8; i++) { snprintf(ch_label_bufs[i], sizeof(ch_label_bufs[i]), "Ch %d", i + 1); snprintf(fader_id_bufs[i], sizeof(fader_id_bufs[i]), "MixFader%d", i); snprintf(pan_id_bufs[i], sizeof(pan_id_bufs[i]), "MixPan%d", i); Clay_Color strip_bg = g_theme.bg_medium; CLAY(CLAY_IDI("MixStrip", i), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, .padding = { uip(8), uip(8), uip(8), uip(8) }, .childGap = uip(8), .childAlignment = { .x = CLAY_ALIGN_X_CENTER }, .layoutDirection = CLAY_TOP_TO_BOTTOM, }, .backgroundColor = strip_bg, .border = { .color = g_theme.border, .width = { .right = 1 } }, ) { // Channel label S32 llen = snprintf(ch_label_bufs[i], sizeof(ch_label_bufs[i]), "Ch %d", i + 1); Clay_String ch_str = { .isStaticallyAllocated = false, .length = llen, .chars = ch_label_bufs[i] }; CLAY_TEXT(ch_str, &g_text_config_dim); // SEND/RECV button Clay_ElementId sr_eid = CLAY_IDI("MixSendRecv", i); B32 sr_hovered = Clay_PointerOver(sr_eid); CLAY(sr_eid, .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(uis(22)) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = sr_hovered ? g_theme.accent_hover : g_theme.bg_lighter, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), ) { CLAY_TEXT(CLAY_STRING("SEND/RECV"), &g_text_config_dim); } // Spacer pushes pan + fader to bottom CLAY(CLAY_IDI("MixSpacer", i), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() } } ) {} // Pan knob ui_knob(pan_id_bufs[i], "Pan", &app->mix_pans[i], 50.0f, 1, 0.0f, 1); // Fader ui_fader(fader_id_bufs[i], ch_label_bufs[i], &app->mix_faders[i], 10.0f, 0, 0.0f, 1); } } // Master strip (slightly wider, accent-tinted) { Clay_Color master_bg = { (F32)Min((S32)g_theme.bg_medium.r + 6, 255), (F32)Min((S32)g_theme.bg_medium.g + 6, 255), (F32)Min((S32)g_theme.bg_medium.b + 6, 255), 255 }; static F32 master_fader = 0.0f; CLAY(CLAY_ID("MixMaster"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(0, uis(120)), .height = CLAY_SIZING_GROW() }, .padding = { uip(8), uip(8), uip(8), uip(8) }, .childGap = uip(8), .childAlignment = { .x = CLAY_ALIGN_X_CENTER }, .layoutDirection = CLAY_TOP_TO_BOTTOM, }, .backgroundColor = master_bg, ) { CLAY_TEXT(CLAY_STRING("Master"), &g_text_config_normal); // SEND/RECV button { Clay_ElementId msr_eid = CLAY_ID("MixMasterSendRecv"); B32 msr_hovered = Clay_PointerOver(msr_eid); CLAY(msr_eid, .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(uis(22)) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = msr_hovered ? g_theme.accent_hover : g_theme.bg_lighter, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), ) { CLAY_TEXT(CLAY_STRING("SEND/RECV"), &g_text_config_dim); } } CLAY(CLAY_ID("MixMasterSpacer"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() } } ) {} // Master pan knob ui_knob("MixMasterPan", "Pan", &app->mix_master_pan, 50.0f, 1, 0.0f, 1); ui_fader("MixMasterFader", "Master", &master_fader, 10.0f, 0, 0.0f, 1); } } } } //////////////////////////////// // Patch view — routing matrix / graph static void build_patch_view(AppState *app) { Clay_Color pv_top = g_theme.bg_medium; Clay_Color pv_bot = {(F32)Max((S32)pv_top.r - 8, 0), (F32)Max((S32)pv_top.g - 8, 0), (F32)Max((S32)pv_top.b - 8, 0), 255}; CustomGradientData *pv_grad = alloc_gradient(pv_top, pv_bot); CLAY(CLAY_ID("PatchView"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, .layoutDirection = CLAY_TOP_TO_BOTTOM, }, .backgroundColor = pv_top, .custom = { .customData = pv_grad }, ) { static const char *patch_tabs[] = { "Matrix", "Graph" }; ui_tab_bar("PatchTabs", patch_tabs, 2, &app->patch_tab); if (app->patch_tab == 0) { // === Matrix routing view === #define MATRIX_INPUTS 32 #define MATRIX_OUTPUTS 33 // Master + Out 1..32 #define HW_OUTPUTS 32 static Clay_TextElementConfig matrix_hdr_text = {}; matrix_hdr_text.textColor = g_theme.text_dim; matrix_hdr_text.fontSize = FONT_SIZE_SMALL; matrix_hdr_text.wrapMode = CLAY_TEXT_WRAP_NONE; static Clay_TextElementConfig matrix_axis_text = {}; matrix_axis_text.textColor = g_theme.text; matrix_axis_text.fontSize = FONT_SIZE_SMALL; matrix_axis_text.wrapMode = CLAY_TEXT_WRAP_NONE; static Clay_TextElementConfig matrix_title_text = {}; matrix_title_text.textColor = g_theme.text; matrix_title_text.fontSize = FONT_SIZE_NORMAL; matrix_title_text.wrapMode = CLAY_TEXT_WRAP_NONE; static Clay_TextElementConfig cell_x_text = {}; cell_x_text.textColor = Clay_Color{255, 255, 255, 255}; cell_x_text.fontSize = FONT_SIZE_SMALL; cell_x_text.wrapMode = CLAY_TEXT_WRAP_NONE; static Clay_TextElementConfig fb_text = {}; fb_text.textColor = Clay_Color{80, 80, 80, 255}; fb_text.fontSize = FONT_SIZE_SMALL; fb_text.wrapMode = CLAY_TEXT_WRAP_NONE; F32 cell_size = uis(20); F32 label_w = uis(48); // Scrollable container for both matrices side by side CLAY(CLAY_ID("MatrixScroll"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, .padding = { uip(12), uip(12), uip(8), uip(8) }, .childGap = uip(32), .layoutDirection = CLAY_LEFT_TO_RIGHT, }, .clip = { .horizontal = true, .vertical = true, .childOffset = Clay_GetScrollOffset() }, ) { // ============================================================ // INTERNAL ROUTING matrix (32 inputs x 33 outputs) // ============================================================ CLAY(CLAY_ID("InternalMatrix"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() }, .childGap = uip(4), .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { // Title CLAY_TEXT(CLAY_STRING("Internal Routing"), &matrix_title_text); // Axis label: "OUTPUT >" CLAY(CLAY_ID("IntOutputLabel"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() }, .padding = { (U16)label_w, 0, 0, 0 }, } ) { CLAY_TEXT(CLAY_STRING("OUTPUT >"), &matrix_axis_text); } // Grid CLAY(CLAY_ID("IntGrid"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() }, .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { // Column headers CLAY(CLAY_ID("IntHeaderRow"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(cell_size) }, .layoutDirection = CLAY_LEFT_TO_RIGHT, } ) { CLAY(CLAY_ID("IntCorner"), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(label_w), .height = CLAY_SIZING_FIXED(cell_size) }, .padding = { 0, uip(4), 0, 0 }, .childAlignment = { .x = CLAY_ALIGN_X_RIGHT, .y = CLAY_ALIGN_Y_CENTER }, } ) { CLAY_TEXT(CLAY_STRING("INPUT v"), &matrix_axis_text); } static char int_dst_bufs[MATRIX_OUTPUTS][8]; for (S32 d = 0; d < MATRIX_OUTPUTS; d++) { S32 dlen; if (d == 0) dlen = snprintf(int_dst_bufs[d], sizeof(int_dst_bufs[d]), "Mst"); else dlen = snprintf(int_dst_bufs[d], sizeof(int_dst_bufs[d]), "%d", d); Clay_String dst_str = { .isStaticallyAllocated = false, .length = dlen, .chars = int_dst_bufs[d] }; CLAY(CLAY_IDI("IntDstHdr", d), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(cell_size), .height = CLAY_SIZING_FIXED(cell_size) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, ) { CLAY_TEXT(dst_str, d == 0 ? &matrix_axis_text : &matrix_hdr_text); } } } // Rows static char int_src_bufs[MATRIX_INPUTS][8]; for (S32 s = 0; s < MATRIX_INPUTS; s++) { S32 slen = snprintf(int_src_bufs[s], sizeof(int_src_bufs[s]), "Ch %d", s + 1); Clay_String src_str = { .isStaticallyAllocated = false, .length = slen, .chars = int_src_bufs[s] }; CLAY(CLAY_IDI("IntRow", s), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(cell_size) }, .layoutDirection = CLAY_LEFT_TO_RIGHT, } ) { CLAY(CLAY_IDI("IntSrcLbl", s), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(label_w), .height = CLAY_SIZING_FIXED(cell_size) }, .padding = { 0, uip(4), 0, 0 }, .childAlignment = { .x = CLAY_ALIGN_X_RIGHT, .y = CLAY_ALIGN_Y_CENTER }, }, ) { CLAY_TEXT(src_str, &matrix_hdr_text); } for (S32 d = 0; d < MATRIX_OUTPUTS; d++) { S32 cell_idx = s * MATRIX_OUTPUTS + d; Clay_ElementId cell_eid = CLAY_IDI("IntCell", cell_idx); B32 cell_hovered = Clay_PointerOver(cell_eid); B32 is_feedback = (d >= 1 && d == s + 1); if (is_feedback) app->patch_matrix[s][d] = 0; B32 active = app->patch_matrix[s][d]; Clay_Color cell_bg; if (is_feedback) { cell_bg = Clay_Color{ (F32)Max((S32)g_theme.bg_dark.r - 10, 0), (F32)Max((S32)g_theme.bg_dark.g - 10, 0), (F32)Max((S32)g_theme.bg_dark.b - 10, 0), 255 }; } else if (active) { cell_bg = g_theme.accent; } else if (cell_hovered) { cell_bg = g_theme.bg_lighter; } else if ((s + d) % 2 == 0) { cell_bg = g_theme.bg_dark; } else { cell_bg = Clay_Color{ (F32)Min((S32)g_theme.bg_dark.r + 6, 255), (F32)Min((S32)g_theme.bg_dark.g + 6, 255), (F32)Min((S32)g_theme.bg_dark.b + 6, 255), 255 }; } if (d == 0 && !active && !cell_hovered && !is_feedback) { cell_bg = Clay_Color{ (F32)Min((S32)cell_bg.r + 10, 255), (F32)Min((S32)cell_bg.g + 10, 255), (F32)Min((S32)cell_bg.b + 12, 255), 255 }; } CLAY(cell_eid, .layout = { .sizing = { .width = CLAY_SIZING_FIXED(cell_size), .height = CLAY_SIZING_FIXED(cell_size) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = cell_bg, .border = { .color = g_theme.border, .width = { .right = 1, .bottom = 1 } }, ) { if (is_feedback) { CLAY_TEXT(CLAY_STRING("-"), &fb_text); } else if (active) { CLAY_TEXT(CLAY_STRING("X"), &cell_x_text); } } if (!is_feedback && cell_hovered && g_wstate.mouse_clicked) { app->patch_matrix[s][d] = !app->patch_matrix[s][d]; } } } } } } // ============================================================ // HARDWARE ROUTING matrix (32 ch outputs x 32 hw outputs) // ============================================================ CLAY(CLAY_ID("HardwareMatrix"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() }, .childGap = uip(4), .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { // Title CLAY_TEXT(CLAY_STRING("Hardware Routing"), &matrix_title_text); // Axis label: "HW OUTPUT >" CLAY(CLAY_ID("HwOutputLabel"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() }, .padding = { (U16)label_w, 0, 0, 0 }, } ) { CLAY_TEXT(CLAY_STRING("HW OUTPUT >"), &matrix_axis_text); } // Grid CLAY(CLAY_ID("HwGrid"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() }, .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { // Column headers CLAY(CLAY_ID("HwHeaderRow"), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(cell_size) }, .layoutDirection = CLAY_LEFT_TO_RIGHT, } ) { CLAY(CLAY_ID("HwCorner"), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(label_w), .height = CLAY_SIZING_FIXED(cell_size) }, .padding = { 0, uip(4), 0, 0 }, .childAlignment = { .x = CLAY_ALIGN_X_RIGHT, .y = CLAY_ALIGN_Y_CENTER }, } ) { CLAY_TEXT(CLAY_STRING("CH OUT v"), &matrix_axis_text); } static char hw_dst_bufs[HW_OUTPUTS][8]; for (S32 d = 0; d < HW_OUTPUTS; d++) { S32 dlen = snprintf(hw_dst_bufs[d], sizeof(hw_dst_bufs[d]), "%d", d + 1); Clay_String dst_str = { .isStaticallyAllocated = false, .length = dlen, .chars = hw_dst_bufs[d] }; CLAY(CLAY_IDI("HwDstHdr", d), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(cell_size), .height = CLAY_SIZING_FIXED(cell_size) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, ) { CLAY_TEXT(dst_str, &matrix_hdr_text); } } } // Rows static char hw_src_bufs[HW_OUTPUTS][8]; for (S32 s = 0; s < HW_OUTPUTS; s++) { S32 slen = snprintf(hw_src_bufs[s], sizeof(hw_src_bufs[s]), "Out %d", s + 1); Clay_String src_str = { .isStaticallyAllocated = false, .length = slen, .chars = hw_src_bufs[s] }; CLAY(CLAY_IDI("HwRow", s), .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(cell_size) }, .layoutDirection = CLAY_LEFT_TO_RIGHT, } ) { CLAY(CLAY_IDI("HwSrcLbl", s), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(label_w), .height = CLAY_SIZING_FIXED(cell_size) }, .padding = { 0, uip(4), 0, 0 }, .childAlignment = { .x = CLAY_ALIGN_X_RIGHT, .y = CLAY_ALIGN_Y_CENTER }, }, ) { CLAY_TEXT(src_str, &matrix_hdr_text); } for (S32 d = 0; d < HW_OUTPUTS; d++) { S32 cell_idx = s * HW_OUTPUTS + d; Clay_ElementId cell_eid = CLAY_IDI("HwCell", cell_idx); B32 cell_hovered = Clay_PointerOver(cell_eid); B32 active = app->hw_matrix[s][d]; Clay_Color cell_bg; if (active) { cell_bg = g_theme.accent; } else if (cell_hovered) { cell_bg = g_theme.bg_lighter; } else if ((s + d) % 2 == 0) { cell_bg = g_theme.bg_dark; } else { cell_bg = Clay_Color{ (F32)Min((S32)g_theme.bg_dark.r + 6, 255), (F32)Min((S32)g_theme.bg_dark.g + 6, 255), (F32)Min((S32)g_theme.bg_dark.b + 6, 255), 255 }; } CLAY(cell_eid, .layout = { .sizing = { .width = CLAY_SIZING_FIXED(cell_size), .height = CLAY_SIZING_FIXED(cell_size) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, }, .backgroundColor = cell_bg, .border = { .color = g_theme.border, .width = { .right = 1, .bottom = 1 } }, ) { if (active) { CLAY_TEXT(CLAY_STRING("X"), &cell_x_text); } } if (cell_hovered && g_wstate.mouse_clicked) { app->hw_matrix[s][d] = !app->hw_matrix[s][d]; } } } } } } } } else { // === Graph view (placeholder) === CLAY(CLAY_ID("GraphContent"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, .padding = { uip(16), uip(16), uip(12), uip(12) }, .childGap = 0, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, } ) { CLAY_TEXT(CLAY_STRING("Graph view coming soon"), &g_text_config_dim); } } } } //////////////////////////////// // Pop-out window content callbacks static void mix_popout_content(void *user_data) { build_mix_view((AppState *)user_data); } static void patch_popout_content(void *user_data) { build_patch_view((AppState *)user_data); } //////////////////////////////// // 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, } ) { build_header_bar(app); if (app->master_layout == 0) { // === EDIT MODE (existing layout) === 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, 1px line) if (app->show_browser) { CLAY(CLAY_ID("SplitBrowser"), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(1), .height = CLAY_SIZING_GROW() } }, .backgroundColor = g_theme.border ) {} } build_main_panel(app); if (app->show_props || app->show_midi_devices) { // Right splitter (vertical, 1px line) CLAY(CLAY_ID("SplitRight"), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(1), .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, 1px line) if (app->show_log) { CLAY(CLAY_ID("SplitLog"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } }, .backgroundColor = g_theme.border ) {} } build_log_panel(app); } else if (app->master_layout == 1) { // === MIX MODE (skip if popped out) === if (!app->show_mix_popout) { build_mix_view(app); } } else { // === PATCH MODE (skip if popped out) === if (!app->show_patch_popout) { build_patch_view(app); } } } } //////////////////////////////// // 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__ U64 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); // Sync scale from popups (they may have changed g_ui_scale) app->ui_scale = g_ui_scale; // 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; F32 dpi_scale = platform_get_dpi_scale(app->window); renderer_set_font_scale(app->renderer, app->ui_scale * dpi_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); // Build UI with Clay ui_begin_frame(app->ui, (F32)w, (F32)h, input.mouse_pos, input.mouse_down, input.scroll_delta, dt); // These query previous frame's Clay layout data (Clay_GetElementData, // Clay_PointerOver) so they must run after ui_begin_frame sets the context. if (app->master_layout == 0) ui_piano_update_input(&app->piano_state); update_panel_splitters(app); 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.bottom_panel_tab = 0; app.piano_state.mouse_note = -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; app.master_layout = 0; for (S32 i = 0; i < 8; i++) { app.mix_faders[i] = 0.0f; app.mix_pans[i] = 0.0f; } app.mix_master_pan = 0.0f; app.patch_tab = 0; memset(app.patch_matrix, 0, sizeof(app.patch_matrix)); memset(app.hw_matrix, 0, sizeof(app.hw_matrix)); 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); B32 running = 1; while (running && platform_poll_events(window)) { // Menu commands S32 menu_cmd = platform_poll_menu_command(window); switch (menu_cmd) { case MENU_FILE_EXIT: running = 0; break; 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; case MENU_PREFERENCES_EDIT: app.show_settings_window = 1; break; default: break; } // Native confirmation dialog (blocking) if (app.show_confirm_dialog) { S32 result = platform_message_box(window, "Confirm Action", "Are you sure you want to proceed? This action cannot be undone.", PLATFORM_MSGBOX_OK_CANCEL); app.show_confirm_dialog = 0; (void)result; // result: 0 = OK, 1 = Cancel } // Open popup windows when flags transition to 1 if (app.show_settings_window && !popup_find_by_flag(&app.show_settings_window)) popup_open(window, renderer, "Preferences", &app.show_settings_window, 480, 400, settings_window_content, &app); if (app.show_about_window && !popup_find_by_flag(&app.show_about_window)) popup_open(window, renderer, "About", &app.show_about_window, 260, 200, about_window_content, nullptr); if (app.show_mix_popout && !popup_find_by_flag(&app.show_mix_popout)) popup_open(window, renderer, "Mix", &app.show_mix_popout, 900, 600, mix_popout_content, &app, PLATFORM_WINDOW_STYLE_POPUP_RESIZABLE, 1); if (app.show_patch_popout && !popup_find_by_flag(&app.show_patch_popout)) popup_open(window, renderer, "Patch", &app.show_patch_popout, 900, 600, patch_popout_content, &app, PLATFORM_WINDOW_STYLE_POPUP_RESIZABLE, 1); // Check for OS close on popups popup_close_check(); if (running) do_frame(&app); // Render popup windows // Compute dt for popups (same as main frame) F32 popup_dt = 1.0f / 60.0f; // approximate; popups don't need precise timing for (S32 i = 0; i < MAX_POPUP_WINDOWS; i++) { if (g_popups[i].alive) popup_do_frame(&g_popups[i], popup_dt); } } popup_close_all(); platform_set_frame_callback(window, nullptr, nullptr); audio_destroy(audio); midi_destroy(midi); ui_destroy(ui); renderer_destroy(renderer); platform_destroy_window(window); return 0; }