diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..ae5a06d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(cd /c/Users/mta/projects/autosample && ./nob.exe debug 2>&1)", + "Bash(cd /c/Users/mta/projects/autosample && rm -f build/*.pdb build/*.obj build/*.ilk && ./nob.exe debug 2>&1)" + ] + } +} diff --git a/src/main.cpp b/src/main.cpp index e30ca21..b814527 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include "renderer/renderer.h" #include "midi/midi.h" #include "ui/ui_core.h" +#include "ui/ui_theme.h" #include "ui/ui_widgets.h" // [cpp] @@ -57,6 +58,9 @@ struct AppState { LARGE_INTEGER freq; LARGE_INTEGER last_time; + // Tab state + S32 right_panel_tab; // 0 = Properties, 1 = MIDI Devices + // Demo widget state B32 demo_checkbox_a; B32 demo_checkbox_b; @@ -78,18 +82,65 @@ struct AppState { //////////////////////////////// // Panel builders -static void build_panel_title_bar(Clay_ElementId id, Clay_String title) { - CLAY(id, +static CustomGradientData g_tab_gradient = { + CUSTOM_RENDER_VGRADIENT, TAB_ACTIVE_TOP, TAB_ACTIVE_BOTTOM +}; + +static S32 build_tab_bar(const char *id, const char **labels, S32 count, S32 *selected) { + S32 id_len = (S32)strlen(id); + Clay_String id_str = { .isStaticallyAllocated = false, .length = id_len, .chars = id }; + Clay_ElementId row_eid = Clay__HashString(id_str, 0); + + CLAY(row_eid, .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(24) }, - .padding = { 8, 8, 0, 0 }, - .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, + .padding = { 0, 0, 4, 0 }, + .layoutDirection = CLAY_LEFT_TO_RIGHT, }, - .backgroundColor = g_theme.title_bar, - .border = { .color = g_theme.border, .width = { .bottom = 1 } } + .backgroundColor = g_theme.bg_medium, + .border = { .color = g_theme.border, .width = { .bottom = 1 } }, ) { - CLAY_TEXT(title, &g_text_config_title); + for (S32 i = 0; i < count; i++) { + Clay_ElementId tab_eid = Clay__HashStringWithOffset(id_str, (uint32_t)i, 0); + B32 is_active = (i == *selected); + B32 hovered = Clay_PointerOver(tab_eid); + + S32 lbl_len = (S32)strlen(labels[i]); + Clay_String lbl_str = { .isStaticallyAllocated = false, .length = lbl_len, .chars = labels[i] }; + + if (is_active) { + CLAY(tab_eid, + .layout = { + .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(TAB_HEIGHT) }, + .padding = { TAB_PADDING_H, TAB_PADDING_H, 0, 6 }, + .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, + }, + .cornerRadius = { .topLeft = TAB_CORNER_RADIUS, .topRight = TAB_CORNER_RADIUS, .bottomLeft = 0, .bottomRight = 0 }, + .custom = { .customData = &g_tab_gradient }, + ) { + CLAY_TEXT(lbl_str, &g_text_config_title); + } + } else { + Clay_Color bg = hovered ? (Clay_Color)TAB_INACTIVE_HOVER : (Clay_Color)TAB_INACTIVE_BG; + CLAY(tab_eid, + .layout = { + .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(TAB_HEIGHT) }, + .padding = { TAB_PADDING_H, TAB_PADDING_H, 0, 6 }, + .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, + }, + .backgroundColor = bg, + .cornerRadius = { .topLeft = TAB_CORNER_RADIUS, .topRight = TAB_CORNER_RADIUS, .bottomLeft = 0, .bottomRight = 0 }, + ) { + CLAY_TEXT(lbl_str, &g_text_config_title); + } + } + + if (hovered && g_wstate.mouse_clicked) { + *selected = i; + } + } } + return *selected; } static void build_browser_panel(B32 show) { @@ -103,7 +154,11 @@ static void build_browser_panel(B32 show) { .backgroundColor = g_theme.bg_medium, .border = { .color = g_theme.border, .width = { .right = 1 } } ) { - build_panel_title_bar(CLAY_ID("BrowserTitleBar"), CLAY_STRING("Browser")); + { + S32 sel = 0; + static const char *browser_tabs[] = { "Browser" }; + build_tab_bar("BrowserTabRow", browser_tabs, 1, &sel); + } CLAY(CLAY_ID("BrowserContent"), .layout = { @@ -126,7 +181,11 @@ static void build_main_panel(AppState *app) { }, .backgroundColor = g_theme.bg_light ) { - build_panel_title_bar(CLAY_ID("MainTitleBar"), CLAY_STRING("Main")); + { + S32 sel = 0; + static const char *main_tabs[] = { "Main" }; + build_tab_bar("MainTabRow", main_tabs, 1, &sel); + } CLAY(CLAY_ID("MainContent"), .layout = { @@ -258,199 +317,189 @@ static void build_main_panel(AppState *app) { } } -static void build_properties_panel(B32 show) { - if (!show) return; +static void build_right_panel(AppState *app) { + static const char *right_tabs[] = { "Properties", "MIDI Devices" }; - CLAY(CLAY_ID("PropertiesPanel"), - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, - .layoutDirection = CLAY_TOP_TO_BOTTOM, - }, - .backgroundColor = g_theme.bg_medium, - .border = { .color = g_theme.border, .width = { .bottom = 1 } } - ) { - build_panel_title_bar(CLAY_ID("PropertiesTitleBar"), CLAY_STRING("Properties")); - - CLAY(CLAY_ID("PropertiesContent"), - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, - .padding = { 8, 8, 6, 6 }, - .childGap = 4, - .layoutDirection = CLAY_TOP_TO_BOTTOM, - } - ) { - CLAY_TEXT(CLAY_STRING("Details"), &g_text_config_normal); - } - } -} - -static void build_midi_panel(B32 show, MidiEngine *midi) { - if (!show) return; - - CLAY(CLAY_ID("MidiPanel"), + CLAY(CLAY_ID("RightPanel"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, .layoutDirection = CLAY_TOP_TO_BOTTOM, }, .backgroundColor = g_theme.bg_medium ) { - build_panel_title_bar(CLAY_ID("MidiTitleBar"), CLAY_STRING("MIDI Devices")); + build_tab_bar("RightTabs", right_tabs, 2, &app->right_panel_tab); - CLAY(CLAY_ID("MidiContent"), - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, - .padding = { 8, 8, 6, 6 }, - .childGap = 6, - .layoutDirection = CLAY_TOP_TO_BOTTOM, - } - ) { - // Refresh button - Clay_ElementId refresh_eid = CLAY_ID("MidiRefreshBtn"); - B32 refresh_hovered = Clay_PointerOver(refresh_eid); - CLAY(refresh_eid, + if (app->right_panel_tab == 0) { + // Properties content + CLAY(CLAY_ID("PropertiesContent"), .layout = { - .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(28) }, - .padding = { 12, 12, 0, 0 }, - .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, - }, - .backgroundColor = refresh_hovered ? g_theme.accent_hover : g_theme.bg_lighter, - .cornerRadius = CLAY_CORNER_RADIUS(3) + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, + .padding = { 8, 8, 6, 6 }, + .childGap = 4, + .layoutDirection = CLAY_TOP_TO_BOTTOM, + } ) { - CLAY_TEXT(CLAY_STRING("Refresh"), &g_text_config_normal); + CLAY_TEXT(CLAY_STRING("Details"), &g_text_config_normal); } - if (refresh_hovered && g_wstate.mouse_clicked) { - midi_refresh_devices(midi); - } - - static char device_bufs[64][128]; - int32_t device_count = midi_get_device_count(midi); - - // --- Inputs section --- - CLAY_TEXT(CLAY_STRING("Inputs"), &g_text_config_dim); - - static const char *note_names[] = {"C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"}; - static char note_bufs[64][8]; - static char vel_bufs[64][8]; - static Clay_TextElementConfig box_text_config; - box_text_config = {}; - box_text_config.textColor = Clay_Color{255, 255, 255, 255}; - box_text_config.fontSize = 12; - box_text_config.wrapMode = CLAY_TEXT_WRAP_NONE; - - B32 has_inputs = 0; - for (int32_t i = 0; i < device_count && i < 64; i++) { - MidiDeviceInfo *dev = midi_get_device(midi, i); - if (!dev->is_input) continue; - has_inputs = 1; - - int len = snprintf(device_bufs[i], sizeof(device_bufs[i]), "%s", dev->name); - Clay_String device_str = { .isStaticallyAllocated = false, .length = len, .chars = device_bufs[i] }; - - // Velocity-based color: blue (vel 0) → green (mid) → red (vel 127) - // Idle = dark gray - Clay_Color box_color; - if (dev->active) { - float t = (float)dev->velocity / 127.0f; - float r, g, b; - if (t < 0.5f) { - float s = t * 2.0f; - r = 40.0f + s * (76.0f - 40.0f); - g = 120.0f + s * (175.0f - 120.0f); - b = 220.0f + s * (80.0f - 220.0f); - } else { - float s = (t - 0.5f) * 2.0f; - r = 76.0f + s * (220.0f - 76.0f); - g = 175.0f + s * (50.0f - 175.0f); - b = 80.0f + s * (40.0f - 80.0f); - } - box_color = Clay_Color{r, g, b, 255}; - } else if (dev->releasing) { - box_color = Clay_Color{255, 255, 255, 255}; - } else { - box_color = Clay_Color{60, 60, 60, 255}; + } else { + // MIDI Devices content + MidiEngine *midi = app->midi; + CLAY(CLAY_ID("MidiContent"), + .layout = { + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, + .padding = { 8, 8, 6, 6 }, + .childGap = 6, + .layoutDirection = CLAY_TOP_TO_BOTTOM, } - - // Box text: note name when held, "OFF" when releasing, "---" when idle - int nlen; - if (dev->active) { - int pitch = dev->note % 12; - int octave = (dev->note / 12) - 1; - nlen = snprintf(note_bufs[i], sizeof(note_bufs[i]), "%s%d", note_names[pitch], octave); - } else if (dev->releasing) { - nlen = snprintf(note_bufs[i], sizeof(note_bufs[i]), "OFF"); - } else { - nlen = snprintf(note_bufs[i], sizeof(note_bufs[i]), "---"); - } - Clay_String note_str = { .isStaticallyAllocated = false, .length = nlen, .chars = note_bufs[i] }; - - // Box text color: dark for white bg (releasing), white otherwise - Clay_TextElementConfig *box_txt = &box_text_config; - static Clay_TextElementConfig box_text_dark; - box_text_dark = box_text_config; - box_text_dark.textColor = Clay_Color{30, 30, 30, 255}; - if (dev->releasing) box_txt = &box_text_dark; - - // Velocity text - int vlen; - if (dev->active) - vlen = snprintf(vel_bufs[i], sizeof(vel_bufs[i]), "%d", dev->velocity); - else - vlen = snprintf(vel_bufs[i], sizeof(vel_bufs[i]), ""); - Clay_String vel_str = { .isStaticallyAllocated = false, .length = vlen, .chars = vel_bufs[i] }; - - CLAY(CLAY_IDI("MidiIn", i), + ) { + // Refresh button + Clay_ElementId refresh_eid = CLAY_ID("MidiRefreshBtn"); + B32 refresh_hovered = Clay_PointerOver(refresh_eid); + CLAY(refresh_eid, .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .padding = { 4, 4, 2, 2 }, - .childGap = 6, + .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(28) }, + .padding = { 12, 12, 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, - .layoutDirection = CLAY_LEFT_TO_RIGHT, - } + }, + .backgroundColor = refresh_hovered ? g_theme.accent_hover : g_theme.bg_lighter, + .cornerRadius = CLAY_CORNER_RADIUS(3) ) { - // Note name box (colored by velocity) - CLAY(CLAY_IDI("MidiInNote", i), + CLAY_TEXT(CLAY_STRING("Refresh"), &g_text_config_normal); + } + if (refresh_hovered && g_wstate.mouse_clicked) { + midi_refresh_devices(midi); + } + + static char device_bufs[64][128]; + int32_t device_count = midi_get_device_count(midi); + + // --- Inputs section --- + CLAY_TEXT(CLAY_STRING("Inputs"), &g_text_config_dim); + + static const char *note_names[] = {"C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"}; + static char note_bufs[64][8]; + static char vel_bufs[64][8]; + static Clay_TextElementConfig box_text_config; + box_text_config = {}; + box_text_config.textColor = Clay_Color{255, 255, 255, 255}; + box_text_config.fontSize = 12; + box_text_config.wrapMode = CLAY_TEXT_WRAP_NONE; + + B32 has_inputs = 0; + for (int32_t i = 0; i < device_count && i < 64; i++) { + MidiDeviceInfo *dev = midi_get_device(midi, i); + if (!dev->is_input) continue; + has_inputs = 1; + + int len = snprintf(device_bufs[i], sizeof(device_bufs[i]), "%s", dev->name); + Clay_String device_str = { .isStaticallyAllocated = false, .length = len, .chars = device_bufs[i] }; + + // Velocity-based color: blue (vel 0) → green (mid) → red (vel 127) + // Idle = dark gray + Clay_Color box_color; + if (dev->active) { + float t = (float)dev->velocity / 127.0f; + float r, g, b; + if (t < 0.5f) { + float s = t * 2.0f; + r = 40.0f + s * (76.0f - 40.0f); + g = 120.0f + s * (175.0f - 120.0f); + b = 220.0f + s * (80.0f - 220.0f); + } else { + float s = (t - 0.5f) * 2.0f; + r = 76.0f + s * (220.0f - 76.0f); + g = 175.0f + s * (50.0f - 175.0f); + b = 80.0f + s * (40.0f - 80.0f); + } + box_color = Clay_Color{r, g, b, 255}; + } else if (dev->releasing) { + box_color = Clay_Color{255, 255, 255, 255}; + } else { + box_color = Clay_Color{60, 60, 60, 255}; + } + + // Box text: note name when held, "OFF" when releasing, "---" when idle + int nlen; + if (dev->active) { + int pitch = dev->note % 12; + int octave = (dev->note / 12) - 1; + nlen = snprintf(note_bufs[i], sizeof(note_bufs[i]), "%s%d", note_names[pitch], octave); + } else if (dev->releasing) { + nlen = snprintf(note_bufs[i], sizeof(note_bufs[i]), "OFF"); + } else { + nlen = snprintf(note_bufs[i], sizeof(note_bufs[i]), "---"); + } + Clay_String note_str = { .isStaticallyAllocated = false, .length = nlen, .chars = note_bufs[i] }; + + // Box text color: dark for white bg (releasing), white otherwise + Clay_TextElementConfig *box_txt = &box_text_config; + static Clay_TextElementConfig box_text_dark; + box_text_dark = box_text_config; + box_text_dark.textColor = Clay_Color{30, 30, 30, 255}; + if (dev->releasing) box_txt = &box_text_dark; + + // Velocity text + int vlen; + if (dev->active) + vlen = snprintf(vel_bufs[i], sizeof(vel_bufs[i]), "%d", dev->velocity); + else + vlen = snprintf(vel_bufs[i], sizeof(vel_bufs[i]), ""); + Clay_String vel_str = { .isStaticallyAllocated = false, .length = vlen, .chars = vel_bufs[i] }; + + CLAY(CLAY_IDI("MidiIn", i), .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(36), .height = CLAY_SIZING_FIXED(18) }, - .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, - }, - .backgroundColor = box_color, - .cornerRadius = CLAY_CORNER_RADIUS(3) + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, + .padding = { 4, 4, 2, 2 }, + .childGap = 6, + .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, + .layoutDirection = CLAY_LEFT_TO_RIGHT, + } ) { - CLAY_TEXT(note_str, box_txt); + // Note name box (colored by velocity) + CLAY(CLAY_IDI("MidiInNote", i), + .layout = { + .sizing = { .width = CLAY_SIZING_FIXED(36), .height = CLAY_SIZING_FIXED(18) }, + .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, + }, + .backgroundColor = box_color, + .cornerRadius = CLAY_CORNER_RADIUS(3) + ) { + 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); } - // 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); - } + 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); + // --- Outputs section --- + CLAY_TEXT(CLAY_STRING("Outputs"), &g_text_config_dim); - B32 has_outputs = 0; - for (int32_t i = 0; i < device_count && i < 64; i++) { - MidiDeviceInfo *dev = midi_get_device(midi, i); - if (dev->is_input) continue; - has_outputs = 1; + B32 has_outputs = 0; + for (int32_t i = 0; i < device_count && i < 64; i++) { + MidiDeviceInfo *dev = midi_get_device(midi, i); + if (dev->is_input) continue; + has_outputs = 1; - int len = snprintf(device_bufs[i], sizeof(device_bufs[i]), "%s", dev->name); - Clay_String device_str = { .isStaticallyAllocated = false, .length = len, .chars = device_bufs[i] }; + int len = snprintf(device_bufs[i], sizeof(device_bufs[i]), "%s", dev->name); + Clay_String device_str = { .isStaticallyAllocated = false, .length = len, .chars = device_bufs[i] }; - CLAY(CLAY_IDI("MidiOut", i), - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .padding = { 4, 4, 2, 2 }, + CLAY(CLAY_IDI("MidiOut", i), + .layout = { + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, + .padding = { 4, 4, 2, 2 }, + } + ) { + CLAY_TEXT(device_str, &g_text_config_normal); } - ) { - CLAY_TEXT(device_str, &g_text_config_normal); } - } - if (!has_outputs) { - CLAY_TEXT(CLAY_STRING(" No MIDI outputs"), &g_text_config_dim); + if (!has_outputs) { + CLAY_TEXT(CLAY_STRING(" No MIDI outputs"), &g_text_config_dim); + } } } } @@ -467,7 +516,11 @@ static void build_log_panel(B32 show) { .backgroundColor = g_theme.bg_medium, .border = { .color = g_theme.border, .width = { .top = 1 } } ) { - build_panel_title_bar(CLAY_ID("LogTitleBar"), CLAY_STRING("Log")); + { + S32 sel = 0; + static const char *log_tabs[] = { "Log" }; + build_tab_bar("LogTabRow", log_tabs, 1, &sel); + } CLAY(CLAY_ID("LogContent"), .layout = { @@ -535,8 +588,7 @@ static void build_ui(AppState *app) { }, .border = { .color = g_theme.border, .width = { .left = 1 } } ) { - build_properties_panel(app->show_props); - build_midi_panel(app->show_midi_devices, app->midi); + build_right_panel(app); } } } diff --git a/src/renderer/renderer_dx12.cpp b/src/renderer/renderer_dx12.cpp index 55a9b73..a9a7e44 100644 --- a/src/renderer/renderer_dx12.cpp +++ b/src/renderer/renderer_dx12.cpp @@ -1,5 +1,6 @@ #include "renderer/renderer.h" #include "ui/ui_core.h" +#include "ui/ui_theme.h" #include #include @@ -41,7 +42,7 @@ struct UIVertex { float col[4]; float rect_min[2]; float rect_max[2]; - float corner_radius; + float corner_radii[4]; // TL, TR, BR, BL float border_thickness; float softness; float mode; // 0 = rect SDF, 1 = textured @@ -66,7 +67,7 @@ struct VSInput { float4 col : COLOR0; float2 rect_min : TEXCOORD1; float2 rect_max : TEXCOORD2; - float corner_radius : TEXCOORD3; + float4 corner_radii : TEXCOORD3; float border_thickness : TEXCOORD4; float softness : TEXCOORD5; float mode : TEXCOORD6; @@ -78,7 +79,7 @@ struct PSInput { float4 col : COLOR0; float2 rect_min : TEXCOORD1; float2 rect_max : TEXCOORD2; - float corner_radius : TEXCOORD3; + float4 corner_radii : TEXCOORD3; float border_thickness : TEXCOORD4; float softness : TEXCOORD5; float mode : TEXCOORD6; @@ -102,7 +103,7 @@ PSInput VSMain(VSInput input) { output.col = input.col; output.rect_min = input.rect_min; output.rect_max = input.rect_max; - output.corner_radius = input.corner_radius; + output.corner_radii = input.corner_radii; output.border_thickness = input.border_thickness; output.softness = input.softness; output.mode = input.mode; @@ -126,7 +127,10 @@ float4 PSMain(PSInput input) : SV_TARGET { float2 pixel_pos = input.pos.xy; float2 rect_center = (input.rect_min + input.rect_max) * 0.5; float2 rect_half_size = (input.rect_max - input.rect_min) * 0.5; - float radius = input.corner_radius; + // corner_radii = (TL, TR, BR, BL) — select radius by quadrant + float radius = (pixel_pos.x < rect_center.x) + ? ((pixel_pos.y < rect_center.y) ? input.corner_radii.x : input.corner_radii.w) + : ((pixel_pos.y < rect_center.y) ? input.corner_radii.y : input.corner_radii.z); float softness = max(input.softness, 0.5); float dist = rounded_rect_sdf(pixel_pos, rect_center, rect_half_size, radius); @@ -692,7 +696,7 @@ static bool create_ui_pipeline(Renderer *r) { { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, offsetof(UIVertex, col), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, { "TEXCOORD", 1, DXGI_FORMAT_R32G32_FLOAT, 0, offsetof(UIVertex, rect_min), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, { "TEXCOORD", 2, DXGI_FORMAT_R32G32_FLOAT, 0, offsetof(UIVertex, rect_max), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, - { "TEXCOORD", 3, DXGI_FORMAT_R32_FLOAT, 0, offsetof(UIVertex, corner_radius), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, + { "TEXCOORD", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, offsetof(UIVertex, corner_radii), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, { "TEXCOORD", 4, DXGI_FORMAT_R32_FLOAT, 0, offsetof(UIVertex, border_thickness), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, { "TEXCOORD", 5, DXGI_FORMAT_R32_FLOAT, 0, offsetof(UIVertex, softness), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, { "TEXCOORD", 6, DXGI_FORMAT_R32_FLOAT, 0, offsetof(UIVertex, mode), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, @@ -809,7 +813,8 @@ static void emit_quad(DrawBatch *batch, float u0, float v0, float u1, float v1, float cr, float cg, float cb, float ca, float rmin_x, float rmin_y, float rmax_x, float rmax_y, - float corner_radius, float border_thickness, float softness, float mode) + float cr_tl, float cr_tr, float cr_br, float cr_bl, + float border_thickness, float softness, float mode) { if (batch->vertex_count + 4 > MAX_VERTICES || batch->index_count + 6 > MAX_INDICES) return; @@ -833,7 +838,8 @@ static void emit_quad(DrawBatch *batch, v[i].col[0] = cr; v[i].col[1] = cg; v[i].col[2] = cb; v[i].col[3] = ca; v[i].rect_min[0] = rmin_x; v[i].rect_min[1] = rmin_y; v[i].rect_max[0] = rmax_x; v[i].rect_max[1] = rmax_y; - v[i].corner_radius = corner_radius; + v[i].corner_radii[0] = cr_tl; v[i].corner_radii[1] = cr_tr; + v[i].corner_radii[2] = cr_br; v[i].corner_radii[3] = cr_bl; v[i].border_thickness = border_thickness; v[i].softness = softness; v[i].mode = mode; @@ -850,13 +856,60 @@ static void emit_quad(DrawBatch *batch, static void emit_rect(DrawBatch *batch, float x0, float y0, float x1, float y1, float cr, float cg, float cb, float ca, - float corner_radius, float border_thickness, float softness) + float cr_tl, float cr_tr, float cr_br, float cr_bl, + float border_thickness, float softness) { emit_quad(batch, x0, y0, x1, y1, 0, 0, 0, 0, cr, cg, cb, ca, x0, y0, x1, y1, - corner_radius, border_thickness, softness, 0.0f); + cr_tl, cr_tr, cr_br, cr_bl, + border_thickness, softness, 0.0f); +} + +static void emit_rect_vgradient(DrawBatch *batch, + float x0, float y0, float x1, float y1, + float tr, float tg, float tb, float ta, + float br, float bg, float bb_, float ba, + float cr_tl, float cr_tr, float cr_br, float cr_bl, + float softness) +{ + if (batch->vertex_count + 4 > MAX_VERTICES || batch->index_count + 6 > MAX_INDICES) + return; + + U32 base = batch->vertex_count; + UIVertex *v = &batch->vertices[base]; + + float pad = softness + 1.0f; + float px0 = x0 - pad, py0 = y0 - pad, px1 = x1 + pad, py1 = y1 + pad; + + v[0].pos[0] = px0; v[0].pos[1] = py0; v[0].uv[0] = 0; v[0].uv[1] = 0; + v[1].pos[0] = px1; v[1].pos[1] = py0; v[1].uv[0] = 0; v[1].uv[1] = 0; + v[2].pos[0] = px1; v[2].pos[1] = py1; v[2].uv[0] = 0; v[2].uv[1] = 0; + v[3].pos[0] = px0; v[3].pos[1] = py1; v[3].uv[0] = 0; v[3].uv[1] = 0; + + // Top vertices get top color, bottom vertices get bottom color + v[0].col[0] = tr; v[0].col[1] = tg; v[0].col[2] = tb; v[0].col[3] = ta; + v[1].col[0] = tr; v[1].col[1] = tg; v[1].col[2] = tb; v[1].col[3] = ta; + v[2].col[0] = br; v[2].col[1] = bg; v[2].col[2] = bb_; v[2].col[3] = ba; + v[3].col[0] = br; v[3].col[1] = bg; v[3].col[2] = bb_; v[3].col[3] = ba; + + for (int i = 0; i < 4; i++) { + v[i].rect_min[0] = x0; v[i].rect_min[1] = y0; + v[i].rect_max[0] = x1; v[i].rect_max[1] = y1; + v[i].corner_radii[0] = cr_tl; v[i].corner_radii[1] = cr_tr; + v[i].corner_radii[2] = cr_br; v[i].corner_radii[3] = cr_bl; + v[i].border_thickness = 0; + v[i].softness = softness; + v[i].mode = 0; + } + + U32 *idx = &batch->indices[batch->index_count]; + idx[0] = base; idx[1] = base + 1; idx[2] = base + 2; + idx[3] = base; idx[4] = base + 2; idx[5] = base + 3; + + batch->vertex_count += 4; + batch->index_count += 6; } static void emit_text_glyphs(DrawBatch *batch, Renderer *r, @@ -901,7 +954,8 @@ static void emit_text_glyphs(DrawBatch *batch, Renderer *r, g->u0, g->v0, g->u1, g->v1, cr, cg, cb, ca, 0, 0, 0, 0, - 0, 0, 0, 1.0f); + 0, 0, 0, 0, + 0, 0, 1.0f); x += g->x_advance * scale; } @@ -1075,13 +1129,12 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) { case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: { Clay_RectangleRenderData *rect = &cmd->renderData.rectangle; Clay_Color c = rect->backgroundColor; - // Use average corner radius for SDF - float cr = (rect->cornerRadius.topLeft + rect->cornerRadius.topRight + - rect->cornerRadius.bottomLeft + rect->cornerRadius.bottomRight) * 0.25f; emit_rect(&batch, bb.x, bb.y, bb.x + bb.width, bb.y + bb.height, c.r / 255.f, c.g / 255.f, c.b / 255.f, c.a / 255.f, - cr, 0, 1.0f); + rect->cornerRadius.topLeft, rect->cornerRadius.topRight, + rect->cornerRadius.bottomRight, rect->cornerRadius.bottomLeft, + 0, 1.0f); } break; case CLAY_RENDER_COMMAND_TYPE_BORDER: { @@ -1095,19 +1148,19 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) { // Draw individual border sides as thin rects if (border->width.top > 0) { emit_rect(&batch, bb.x, bb.y, bb.x + bb.width, bb.y + border->width.top, - cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 1.0f); + cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f); } if (border->width.bottom > 0) { emit_rect(&batch, bb.x, bb.y + bb.height - border->width.bottom, bb.x + bb.width, bb.y + bb.height, - cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 1.0f); + cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f); } if (border->width.left > 0) { emit_rect(&batch, bb.x, bb.y, bb.x + border->width.left, bb.y + bb.height, - cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 1.0f); + cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f); } if (border->width.right > 0) { emit_rect(&batch, bb.x + bb.width - border->width.right, bb.y, bb.x + bb.width, bb.y + bb.height, - cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 1.0f); + cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f); } } break; @@ -1137,8 +1190,26 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) { r->command_list->RSSetScissorRects(1, &full_scissor); } break; + case CLAY_RENDER_COMMAND_TYPE_CUSTOM: { + Clay_CustomRenderData *custom = &cmd->renderData.custom; + if (custom->customData) { + CustomRenderType type = *(CustomRenderType *)custom->customData; + if (type == CUSTOM_RENDER_VGRADIENT) { + CustomGradientData *grad = (CustomGradientData *)custom->customData; + Clay_Color tc = grad->top_color; + Clay_Color bc = grad->bottom_color; + emit_rect_vgradient(&batch, + bb.x, bb.y, bb.x + bb.width, bb.y + bb.height, + tc.r / 255.f, tc.g / 255.f, tc.b / 255.f, tc.a / 255.f, + bc.r / 255.f, bc.g / 255.f, bc.b / 255.f, bc.a / 255.f, + custom->cornerRadius.topLeft, custom->cornerRadius.topRight, + custom->cornerRadius.bottomRight, custom->cornerRadius.bottomLeft, + 1.0f); + } + } + } break; + case CLAY_RENDER_COMMAND_TYPE_IMAGE: - case CLAY_RENDER_COMMAND_TYPE_CUSTOM: default: break; } diff --git a/src/ui/ui_theme.h b/src/ui/ui_theme.h new file mode 100644 index 0000000..0313e94 --- /dev/null +++ b/src/ui/ui_theme.h @@ -0,0 +1,71 @@ +#pragma once +// ui_theme.h - Theme constants and defines for the UI layer. +// Centralizes colors, sizing, padding, and radius values used across the app. + +#include "clay.h" + +//////////////////////////////// +// Tab styling + +#define TAB_ACTIVE_TOP Clay_Color{ 70, 120, 160, 255} +#define TAB_ACTIVE_BOTTOM Clay_Color{ 30, 55, 80, 255} +#define TAB_INACTIVE_BG Clay_Color{ 45, 45, 48, 255} +#define TAB_INACTIVE_HOVER Clay_Color{ 55, 55, 60, 255} +#define TAB_HEIGHT 26 +#define TAB_CORNER_RADIUS 5 +#define TAB_PADDING_H 10 + +//////////////////////////////// +// Custom render types (for gradient rects via CLAY_RENDER_COMMAND_TYPE_CUSTOM) + +enum CustomRenderType { + CUSTOM_RENDER_VGRADIENT = 1, +}; + +struct CustomGradientData { + CustomRenderType type; + Clay_Color top_color; + Clay_Color bottom_color; +}; + +//////////////////////////////// +// Font sizes + +#define FONT_SIZE_NORMAL 15 +#define FONT_SIZE_SMALL 12 + +//////////////////////////////// +// Widget sizing + +#define WIDGET_BUTTON_HEIGHT 30 +#define WIDGET_CHECKBOX_HEIGHT 28 +#define WIDGET_CHECKBOX_SIZE 18 +#define WIDGET_RADIO_OUTER 16 +#define WIDGET_RADIO_INNER 8 +#define WIDGET_INPUT_HEIGHT 30 +#define WIDGET_DROPDOWN_HEIGHT 30 +#define WIDGET_DROPDOWN_ITEM_H 28 + +//////////////////////////////// +// Corner radii + +#define CORNER_RADIUS_SM 3 +#define CORNER_RADIUS_MD 6 +#define CORNER_RADIUS_ROUND 8 + +//////////////////////////////// +// Modal / window styling + +#define MODAL_OVERLAY_COLOR Clay_Color{ 0, 0, 0, 120} +#define MODAL_WIDTH 400 +#define MODAL_CORNER_RADIUS CORNER_RADIUS_MD + +#define WINDOW_CORNER_RADIUS CORNER_RADIUS_MD +#define WINDOW_TITLE_HEIGHT 32 + +//////////////////////////////// +// Panel sizing + +#define PANEL_BROWSER_WIDTH 200 +#define PANEL_RIGHT_COL_WIDTH 250 +#define PANEL_LOG_HEIGHT 180