add proper tab implementation
This commit is contained in:
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@@ -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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
414
src/main.cpp
414
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "renderer/renderer.h"
|
||||
#include "ui/ui_core.h"
|
||||
#include "ui/ui_theme.h"
|
||||
|
||||
#include <d3d12.h>
|
||||
#include <dxgi1_5.h>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
71
src/ui/ui_theme.h
Normal file
71
src/ui/ui_theme.h
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user