2093 lines
90 KiB
C++
2093 lines
90 KiB
C++
// Unity build - include all src files here
|
|
// -mta
|
|
#ifdef __APPLE__
|
|
#include <mach/mach_time.h>
|
|
#endif
|
|
// [h]
|
|
#include "base/base_inc.h"
|
|
#include "platform/platform.h"
|
|
#include "renderer/renderer.h"
|
|
#include "midi/midi.h"
|
|
#include "audio/audio.h"
|
|
#include "ui/ui_core.h"
|
|
#include "ui/ui_icons.h"
|
|
#include "ui/ui_widgets.h"
|
|
#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;
|
|
}
|