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

@@ -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)"
]
}
}

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

View File

@@ -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
View 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