diff --git a/src/main.cpp b/src/main.cpp index 47d82db..dc78684 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -142,6 +142,11 @@ static void build_browser_panel(B32 show) { .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { + // Top highlight (beveled edge) + CLAY(CLAY_ID("BrowserHighlight"), + .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } }, + .backgroundColor = g_theme.bg_lighter + ) {} CLAY_TEXT(CLAY_STRING("Instruments"), &g_text_config_normal); } } @@ -169,6 +174,11 @@ static void build_main_panel(AppState *app) { .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { + // Top highlight (beveled edge) + CLAY(CLAY_ID("MainHighlight"), + .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } }, + .backgroundColor = g_theme.bg_lighter + ) {} // Section: Buttons ui_label("LblButtons", "Buttons"); CLAY(CLAY_ID("ButtonRow"), @@ -313,6 +323,11 @@ static void build_right_panel(AppState *app) { .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { + // Top highlight (beveled edge) + CLAY(CLAY_ID("PropsHighlight"), + .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } }, + .backgroundColor = g_theme.bg_lighter + ) {} CLAY_TEXT(CLAY_STRING("Details"), &g_text_config_normal); } } else { @@ -326,6 +341,11 @@ static void build_right_panel(AppState *app) { .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { + // Top highlight (beveled edge) + CLAY(CLAY_ID("MidiHighlight"), + .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } }, + .backgroundColor = g_theme.bg_lighter + ) {} // Refresh button Clay_ElementId refresh_eid = CLAY_ID("MidiRefreshBtn"); B32 refresh_hovered = Clay_PointerOver(refresh_eid); @@ -504,6 +524,11 @@ static void build_log_panel(B32 show) { .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { + // Top highlight (beveled edge) + CLAY(CLAY_ID("LogHighlight"), + .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } }, + .backgroundColor = g_theme.bg_lighter + ) {} CLAY_TEXT(CLAY_STRING("Output / Log"), &g_text_config_normal); } } diff --git a/src/ui/ui_core.cpp b/src/ui/ui_core.cpp index 923d053..9905857 100644 --- a/src/ui/ui_core.cpp +++ b/src/ui/ui_core.cpp @@ -42,6 +42,7 @@ void ui_set_theme(S32 theme_id) { g_theme.title_bar = Clay_Color{ 26, 26, 28, 255}; g_theme.scrollbar_bg = Clay_Color{ 26, 26, 28, 255}; g_theme.scrollbar_grab = Clay_Color{ 61, 61, 66, 255}; + g_theme.shadow = Clay_Color{ 0, 0, 0, 30}; g_theme.tab_active_top = Clay_Color{ 70, 120, 160, 255}; g_theme.tab_active_bottom= Clay_Color{ 30, 55, 80, 255}; g_theme.tab_inactive = Clay_Color{ 45, 45, 48, 255}; @@ -66,6 +67,7 @@ void ui_set_theme(S32 theme_id) { g_theme.title_bar = Clay_Color{220, 220, 225, 255}; g_theme.scrollbar_bg = Clay_Color{220, 220, 225, 255}; g_theme.scrollbar_grab = Clay_Color{180, 180, 185, 255}; + g_theme.shadow = Clay_Color{ 0, 0, 0, 18}; g_theme.tab_active_top = Clay_Color{ 70, 130, 180, 255}; g_theme.tab_active_bottom= Clay_Color{ 50, 100, 150, 255}; g_theme.tab_inactive = Clay_Color{218, 218, 222, 255}; diff --git a/src/ui/ui_core.h b/src/ui/ui_core.h index f75a23e..7bcfff5 100644 --- a/src/ui/ui_core.h +++ b/src/ui/ui_core.h @@ -64,6 +64,7 @@ struct UI_Theme { Clay_Color title_bar; Clay_Color scrollbar_bg; Clay_Color scrollbar_grab; + Clay_Color shadow; // Semi-transparent black for drop shadows // Tab bar colors Clay_Color tab_active_top; diff --git a/src/ui/ui_widgets.cpp b/src/ui/ui_widgets.cpp index 8135c36..070c52f 100644 --- a/src/ui/ui_widgets.cpp +++ b/src/ui/ui_widgets.cpp @@ -15,6 +15,95 @@ UI_WidgetState g_wstate = {}; static CustomIconData g_icon_pool[UI_MAX_ICONS_PER_FRAME]; static S32 g_icon_pool_count = 0; +// Gradient per-frame pool (for button/title bar/dropdown/input gradients) +#define UI_MAX_GRADIENTS_PER_FRAME 64 +static CustomGradientData g_grad_pool[UI_MAX_GRADIENTS_PER_FRAME]; +static S32 g_grad_pool_count = 0; + +static CustomGradientData *alloc_gradient(Clay_Color top, Clay_Color bottom) { + if (g_grad_pool_count >= UI_MAX_GRADIENTS_PER_FRAME) return nullptr; + CustomGradientData *g = &g_grad_pool[g_grad_pool_count++]; + g->type = CUSTOM_RENDER_VGRADIENT; + g->top_color = top; + g->bottom_color = bottom; + return g; +} + +// Per-frame shadow layer ID counter (each shadow uses N unique IDs) +static S32 g_shadow_id_counter = 0; + +// Emit a smooth multi-layer drop shadow as floating rects. +// bb: bounding box of the element to shadow (previous frame) +// ox/oy: directional offset (light direction) +// radius: max blur spread +// peak_alpha: total alpha at center when all layers stack +// z: zIndex for shadow layers +// attach_to_root: if true, use absolute positioning; if false, attach to parent_id +// parent_id: only used when attach_to_root=false +// parent_attach: attachment point on parent (e.g. LEFT_BOTTOM for dropdowns) +#define SHADOW_LAYERS 7 + +static void emit_shadow(Clay_BoundingBox bb, F32 ox, F32 oy, F32 radius, + float peak_alpha, int16_t z, + B32 attach_to_root, uint32_t parent_id, + Clay_FloatingAttachPointType parent_attach) { + if (bb.width <= 0) return; + + float per_layer = peak_alpha / (float)SHADOW_LAYERS; + + // Draw outermost first (largest, lowest alpha contribution), innermost last + for (S32 i = SHADOW_LAYERS - 1; i >= 0; i--) { + float t = (float)(i + 1) / (float)SHADOW_LAYERS; // 1/N .. 1.0 + F32 expand = radius * t; + + S32 sid = g_shadow_id_counter++; + Clay_ElementId shadow_eid = CLAY_IDI("Shadow", sid); + + if (attach_to_root) { + CLAY(shadow_eid, + .layout = { + .sizing = { + .width = CLAY_SIZING_FIXED(bb.width + expand*2), + .height = CLAY_SIZING_FIXED(bb.height + expand*2), + }, + }, + .backgroundColor = {0, 0, 0, per_layer}, + .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS + expand), + .floating = { + .offset = { bb.x - expand + ox, bb.y - expand + oy }, + .zIndex = z, + .attachPoints = { + .element = CLAY_ATTACH_POINT_LEFT_TOP, + .parent = CLAY_ATTACH_POINT_LEFT_TOP, + }, + .attachTo = CLAY_ATTACH_TO_ROOT, + } + ) {} + } else { + CLAY(shadow_eid, + .layout = { + .sizing = { + .width = CLAY_SIZING_FIXED(bb.width + expand*2), + .height = CLAY_SIZING_FIXED(bb.height + expand*2), + }, + }, + .backgroundColor = {0, 0, 0, per_layer}, + .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS + expand), + .floating = { + .parentId = parent_id, + .offset = { -expand + ox, -expand + oy }, + .zIndex = z, + .attachPoints = { + .element = CLAY_ATTACH_POINT_LEFT_TOP, + .parent = parent_attach, + }, + .attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID, + } + ) {} + } + } +} + void ui_widgets_init() { g_wstate = {}; } @@ -23,6 +112,8 @@ void ui_widgets_begin_frame(PlatformInput input) { g_wstate.input = input; g_wstate.mouse_clicked = (input.mouse_down && !input.was_mouse_down); g_icon_pool_count = 0; + g_grad_pool_count = 0; + g_shadow_id_counter = 0; g_wstate.cursor_blink += 1.0f / 60.0f; g_wstate.text_input_count = 0; g_wstate.tab_pressed = 0; @@ -146,14 +237,20 @@ B32 ui_button(const char *id, const char *text) { Clay_ElementId eid = WID(id); B32 hovered = Clay_PointerOver(eid); + Clay_Color base = hovered ? g_theme.accent_hover : g_theme.accent; + Clay_Color top = {(float)Min((int)base.r+12,255), (float)Min((int)base.g+12,255), (float)Min((int)base.b+12,255), base.a}; + Clay_Color bot = {(float)Max((int)base.r-15,0), (float)Max((int)base.g-15,0), (float)Max((int)base.b-15,0), base.a}; + CustomGradientData *grad = alloc_gradient(top, bot); + CLAY(eid, .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(WIDGET_BUTTON_HEIGHT) }, .padding = { uip(12), uip(12), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, - .backgroundColor = hovered ? g_theme.accent_hover : g_theme.accent, - .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS) + .backgroundColor = base, + .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), + .custom = { .customData = grad }, ) { CLAY_TEXT(clay_str(text), &g_widget_text_config_btn); } @@ -598,6 +695,11 @@ B32 ui_text_input(const char *id, char *buf, S32 buf_size) { Clay_Color bg = is_focused ? g_theme.bg_dark : g_theme.bg_medium; Clay_Color border_color = is_focused ? g_theme.accent : g_theme.border; + // Inset effect: darker top, lighter bottom (recessed look) + Clay_Color inset_top = {(float)Max((int)bg.r-8,0), (float)Max((int)bg.g-8,0), (float)Max((int)bg.b-8,0), bg.a}; + Clay_Color inset_bot = {(float)Min((int)bg.r+3,255), (float)Min((int)bg.g+3,255), (float)Min((int)bg.b+3,255), bg.a}; + CustomGradientData *inset_grad = alloc_gradient(inset_top, inset_bot); + CLAY(eid, .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WIDGET_INPUT_HEIGHT) }, @@ -607,7 +709,8 @@ B32 ui_text_input(const char *id, char *buf, S32 buf_size) { }, .backgroundColor = bg, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), - .border = { .color = border_color, .width = { 1, 1, 1, 1 } } + .border = { .color = border_color, .width = { 1, 1, 1, 1 } }, + .custom = { .customData = inset_grad }, ) { if (len == 0 && !is_focused) { // Placeholder @@ -712,6 +815,9 @@ B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected) Clay_Color bg = is_open ? g_theme.bg_dark : g_theme.bg_medium; if (header_hovered && !is_open) bg = g_theme.bg_lighter; + Clay_Color dd_top = {(float)Min((int)bg.r+6,255), (float)Min((int)bg.g+6,255), (float)Min((int)bg.b+6,255), bg.a}; + CustomGradientData *dd_grad = alloc_gradient(dd_top, bg); + CLAY(eid, .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WIDGET_DROPDOWN_HEIGHT) }, @@ -721,7 +827,8 @@ B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected) }, .backgroundColor = bg, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), - .border = { .color = is_open ? g_theme.accent : g_theme.border, .width = { 1, 1, 1, 1 } } + .border = { .color = is_open ? g_theme.accent : g_theme.border, .width = { 1, 1, 1, 1 } }, + .custom = { .customData = dd_grad }, ) { CLAY(text_eid, .layout = { @@ -756,6 +863,15 @@ B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected) if (is_open) { Clay_ElementId list_id = WIDI(id, 502); float header_width = Clay_GetElementData(eid).boundingBox.width; + + // Dropdown list shadow + { + Clay_BoundingBox dd_bb = Clay_GetElementData(list_id).boundingBox; + emit_shadow(dd_bb, uis(2), uis(2), uis(5), + g_theme.shadow.a, 1999, + 0, eid.id, CLAY_ATTACH_POINT_LEFT_BOTTOM); + } + CLAY(list_id, .layout = { .sizing = { .width = CLAY_SIZING_FIT(.min = header_width), .height = CLAY_SIZING_FIT() }, @@ -875,6 +991,15 @@ S32 ui_modal(const char *id, const char *title, const char *message, } ) {} + // Modal drop shadow + { + Clay_ElementId modal_box_id = WIDI(id, 2001); + Clay_BoundingBox modal_bb = Clay_GetElementData(modal_box_id).boundingBox; + emit_shadow(modal_bb, uis(3), uis(4), uis(10), + g_theme.shadow.a, 1000, + 1, 0, CLAY_ATTACH_POINT_LEFT_TOP); + } + // Dialog box (centered) CLAY(WIDI(id, 2001), .layout = { @@ -894,18 +1019,25 @@ S32 ui_modal(const char *id, const char *title, const char *message, }, .border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } } ) { - // Title bar - CLAY(WIDI(id, 2002), - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WINDOW_TITLE_HEIGHT) }, - .padding = { uip(12), uip(12), 0, 0 }, - .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, - }, - .backgroundColor = g_theme.title_bar, - .cornerRadius = { CORNER_RADIUS, CORNER_RADIUS, 0, 0 }, - .border = { .color = g_theme.border, .width = { .bottom = 1 } } - ) { - CLAY_TEXT(clay_str(title), &g_widget_text_config); + // Title bar (gradient: lighter top) + { + Clay_Color mtb = g_theme.title_bar; + Clay_Color mtb_top = {(float)Min((int)mtb.r+12,255), (float)Min((int)mtb.g+12,255), (float)Min((int)mtb.b+12,255), mtb.a}; + CustomGradientData *mtb_grad = alloc_gradient(mtb_top, mtb); + + CLAY(WIDI(id, 2002), + .layout = { + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WINDOW_TITLE_HEIGHT) }, + .padding = { uip(12), uip(12), 0, 0 }, + .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, + }, + .backgroundColor = g_theme.title_bar, + .cornerRadius = { CORNER_RADIUS, CORNER_RADIUS, 0, 0 }, + .border = { .color = g_theme.border, .width = { .bottom = 1 } }, + .custom = { .customData = mtb_grad }, + ) { + CLAY_TEXT(clay_str(title), &g_widget_text_config); + } } // Message body @@ -936,14 +1068,20 @@ S32 ui_modal(const char *id, const char *title, const char *message, Clay_ElementId btn_id = WIDI(id, 2100 + i); B32 btn_hovered = Clay_PointerOver(btn_id); + Clay_Color mbtn_base = btn_hovered ? g_theme.accent_hover : g_theme.accent; + Clay_Color mbtn_top = {(float)Min((int)mbtn_base.r+12,255), (float)Min((int)mbtn_base.g+12,255), (float)Min((int)mbtn_base.b+12,255), mbtn_base.a}; + Clay_Color mbtn_bot = {(float)Max((int)mbtn_base.r-15,0), (float)Max((int)mbtn_base.g-15,0), (float)Max((int)mbtn_base.b-15,0), mbtn_base.a}; + CustomGradientData *mbtn_grad = alloc_gradient(mbtn_top, mbtn_bot); + CLAY(btn_id, .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(WIDGET_BUTTON_HEIGHT) }, .padding = { uip(16), uip(16), 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, - .backgroundColor = btn_hovered ? g_theme.accent_hover : g_theme.accent, - .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS) + .backgroundColor = mbtn_base, + .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), + .custom = { .customData = mbtn_grad }, ) { CLAY_TEXT(clay_str(buttons[i]), &g_widget_text_config_btn); } @@ -1059,6 +1197,16 @@ B32 ui_window(const char *id, const char *title, B32 *open, Clay_ElementId close_id = WIDI(id, 3001); B32 close_hovered = Clay_PointerOver(close_id); + // Drop shadow + { + Clay_BoundingBox win_bb = Clay_GetElementData(eid).boundingBox; + // Use absolute position since window uses offset-based floating + Clay_BoundingBox shadow_bb = { slot->position.x, slot->position.y, win_bb.width, win_bb.height }; + emit_shadow(shadow_bb, uis(3), uis(3), uis(8), + g_theme.shadow.a, (int16_t)(100 + slot->z_order - 1), + 1, 0, CLAY_ATTACH_POINT_LEFT_TOP); + } + // Window floating element CLAY(eid, .layout = { @@ -1079,7 +1227,11 @@ B32 ui_window(const char *id, const char *title, B32 *open, }, .border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } } ) { - // Title bar + // Title bar (gradient: lighter top) + Clay_Color tb = g_theme.title_bar; + Clay_Color tb_top = {(float)Min((int)tb.r+12,255), (float)Min((int)tb.g+12,255), (float)Min((int)tb.b+12,255), tb.a}; + CustomGradientData *tb_grad = alloc_gradient(tb_top, tb); + CLAY(title_bar_id, .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WINDOW_TITLE_HEIGHT) }, @@ -1089,7 +1241,8 @@ B32 ui_window(const char *id, const char *title, B32 *open, }, .backgroundColor = g_theme.title_bar, .cornerRadius = { CORNER_RADIUS, CORNER_RADIUS, 0, 0 }, - .border = { .color = g_theme.border, .width = { .bottom = 1 } } + .border = { .color = g_theme.border, .width = { .bottom = 1 } }, + .custom = { .customData = tb_grad }, ) { // Title text (grows to push close button right) CLAY(WIDI(id, 3002),