add proper tab implementation

This commit is contained in:
2026-03-03 02:17:44 -05:00
parent 7902db6ec7
commit de6c7754cb
4 changed files with 403 additions and 201 deletions

View File

@@ -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);
}
}
}