diff --git a/assets/fader.svg b/assets/fader.svg new file mode 100644 index 0000000..0a92a22 --- /dev/null +++ b/assets/fader.svg @@ -0,0 +1,399 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.c b/build.c index 8a6fc02..95e5755 100644 --- a/build.c +++ b/build.c @@ -88,7 +88,7 @@ static bool build_lunasvg_lib(const char *build_dir, bool debug) { Nob_Cmd cmd = {0}; nob_cmd_append(&cmd, "clang"); nob_cmd_append(&cmd, "-std=c11", "-c"); - nob_cmd_append(&cmd, "-DPLUTVOG_BUILD", "-DPLUTVOG_BUILD_STATIC"); + nob_cmd_append(&cmd, "-DPLUTOVG_BUILD", "-DPLUTOVG_BUILD_STATIC"); nob_cmd_append(&cmd, "-Ivendor/lunasvg/plutovg/include"); nob_cmd_append(&cmd, "-Ivendor/lunasvg/plutovg/source"); nob_cmd_append(&cmd, "-Wall", "-Wno-unused-function", "-Wno-unused-parameter"); @@ -118,7 +118,7 @@ static bool build_lunasvg_lib(const char *build_dir, bool debug) { nob_cmd_append(&cmd, "clang++"); nob_cmd_append(&cmd, "-std=c++17", "-c"); nob_cmd_append(&cmd, "-fno-exceptions", "-fno-rtti"); - nob_cmd_append(&cmd, "-DLUNASVG_BUILD", "-DLUNASVG_BUILD_STATIC"); + nob_cmd_append(&cmd, "-DLUNASVG_BUILD", "-DLUNASVG_BUILD_STATIC", "-DPLUTOVG_BUILD_STATIC"); nob_cmd_append(&cmd, "-Ivendor/lunasvg/include"); nob_cmd_append(&cmd, "-Ivendor/lunasvg/source"); nob_cmd_append(&cmd, "-Ivendor/lunasvg/plutovg/include"); @@ -346,7 +346,7 @@ static bool build_lunasvg_lib(const char *build_dir, bool debug) { Nob_Cmd cmd = {0}; nob_cmd_append(&cmd, "cl.exe", "/nologo", "/c"); nob_cmd_append(&cmd, "/std:c11"); - nob_cmd_append(&cmd, "/DPLUTVOG_BUILD", "/DPLUTVOG_BUILD_STATIC"); + nob_cmd_append(&cmd, "/DPLUTOVG_BUILD", "/DPLUTOVG_BUILD_STATIC"); nob_cmd_append(&cmd, "/Ivendor/lunasvg/plutovg/include"); nob_cmd_append(&cmd, "/Ivendor/lunasvg/plutovg/source"); nob_cmd_append(&cmd, "/W3"); @@ -367,7 +367,7 @@ static bool build_lunasvg_lib(const char *build_dir, bool debug) { Nob_Cmd cmd = {0}; nob_cmd_append(&cmd, "cl.exe", "/nologo", "/c"); nob_cmd_append(&cmd, "/std:c++17", "/EHsc"); - nob_cmd_append(&cmd, "/DLUNASVG_BUILD", "/DLUNASVG_BUILD_STATIC"); + nob_cmd_append(&cmd, "/DLUNASVG_BUILD", "/DLUNASVG_BUILD_STATIC", "/DPLUTOVG_BUILD_STATIC"); nob_cmd_append(&cmd, "/Ivendor/lunasvg/include"); nob_cmd_append(&cmd, "/Ivendor/lunasvg/source"); nob_cmd_append(&cmd, "/Ivendor/lunasvg/plutovg/include"); diff --git a/src/main.cpp b/src/main.cpp index 27cb6fe..7617f6d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -115,6 +115,11 @@ struct AppState { F32 demo_slider_v; F32 demo_fader; + // Scrollbar drag state + B32 scrollbar_dragging; + F32 scrollbar_drag_start_y; + F32 scrollbar_drag_start_scroll; + // Audio device selection S32 audio_device_sel; // dropdown index: 0 = None, 1+ = device S32 audio_device_prev; // previous selection for change detection @@ -175,13 +180,20 @@ static void build_main_panel(AppState *app) { ui_tab_bar("MainTabRow", main_tabs, 1, &sel); } + CLAY(CLAY_ID("MainScrollArea"), + .layout = { + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, + .layoutDirection = CLAY_LEFT_TO_RIGHT, + } + ) { CLAY(CLAY_ID("MainContent"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, .padding = { uip(16), uip(16), uip(12), uip(12) }, .childGap = uip(12), .layoutDirection = CLAY_TOP_TO_BOTTOM, - } + }, + .clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() }, ) { // Top highlight (beveled edge) CLAY(CLAY_ID("MainHighlight"), @@ -353,6 +365,97 @@ static void build_main_panel(AppState *app) { } } } + + // Scrollbar + { + Clay_ScrollContainerData scroll_data = Clay_GetScrollContainerData(CLAY_ID("MainContent")); + if (scroll_data.found && scroll_data.contentDimensions.height > scroll_data.scrollContainerDimensions.height) { + float track_h = scroll_data.scrollContainerDimensions.height; + float content_h = scroll_data.contentDimensions.height; + float visible_ratio = track_h / content_h; + float thumb_h = Max(visible_ratio * track_h, uis(24)); + float scroll_range = content_h - track_h; + float scroll_pct = scroll_range > 0 ? -scroll_data.scrollPosition->y / scroll_range : 0; + float thumb_y = scroll_pct * (track_h - thumb_h); + float bar_w = uis(8); + + // Handle scrollbar drag + Clay_ElementId thumb_id = CLAY_ID("MainScrollThumb"); + Clay_ElementId track_id = CLAY_ID("MainScrollTrack"); + B32 thumb_hovered = Clay_PointerOver(thumb_id); + B32 track_hovered = Clay_PointerOver(track_id); + PlatformInput input = g_wstate.input; + B32 mouse_clicked = input.mouse_down && !input.was_mouse_down; + + if (mouse_clicked && thumb_hovered) { + app->scrollbar_dragging = true; + app->scrollbar_drag_start_y = input.mouse_pos.y; + app->scrollbar_drag_start_scroll = scroll_data.scrollPosition->y; + } else if (mouse_clicked && track_hovered && !thumb_hovered) { + // Click on track: jump scroll position so thumb centers on click + Clay_BoundingBox track_bb = Clay_GetElementData(track_id).boundingBox; + float click_rel = input.mouse_pos.y - track_bb.y; + float target_pct = (click_rel - thumb_h / 2) / (track_h - thumb_h); + if (target_pct < 0) target_pct = 0; + if (target_pct > 1) target_pct = 1; + scroll_data.scrollPosition->y = -target_pct * scroll_range; + // Start dragging from new position + app->scrollbar_dragging = true; + app->scrollbar_drag_start_y = input.mouse_pos.y; + app->scrollbar_drag_start_scroll = scroll_data.scrollPosition->y; + } + + if (!input.mouse_down) { + app->scrollbar_dragging = false; + } + + if (app->scrollbar_dragging) { + float dy = input.mouse_pos.y - app->scrollbar_drag_start_y; + float scroll_per_px = scroll_range / (track_h - thumb_h); + float new_scroll = app->scrollbar_drag_start_scroll - dy * scroll_per_px; + if (new_scroll > 0) new_scroll = 0; + if (new_scroll < -scroll_range) new_scroll = -scroll_range; + scroll_data.scrollPosition->y = new_scroll; + } + + // Thumb color: highlight on hover or drag + Clay_Color thumb_color = g_theme.scrollbar_grab; + if (app->scrollbar_dragging || thumb_hovered) { + thumb_color = Clay_Color{ + (float)Min((int)thumb_color.r + 30, 255), + (float)Min((int)thumb_color.g + 30, 255), + (float)Min((int)thumb_color.b + 30, 255), + thumb_color.a + }; + } + + CLAY(track_id, + .layout = { + .sizing = { .width = CLAY_SIZING_FIXED(bar_w), .height = CLAY_SIZING_GROW() }, + }, + .backgroundColor = g_theme.scrollbar_bg + ) { + CLAY(thumb_id, + .layout = { + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(thumb_h) }, + }, + .backgroundColor = thumb_color, + .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), + .floating = { + .offset = { 0, thumb_y }, + .attachPoints = { + .element = CLAY_ATTACH_POINT_LEFT_TOP, + .parent = CLAY_ATTACH_POINT_LEFT_TOP, + }, + .attachTo = CLAY_ATTACH_TO_PARENT, + }, + ) {} + } + } else { + app->scrollbar_dragging = false; + } + } + } // MainScrollArea } } diff --git a/src/platform/platform_win32.cpp b/src/platform/platform_win32.cpp index c5f83ee..4471af2 100644 --- a/src/platform/platform_win32.cpp +++ b/src/platform/platform_win32.cpp @@ -53,7 +53,7 @@ static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM case WM_MOUSEWHEEL: if (g_current_window) { int16_t wheel_delta = (int16_t)HIWORD(wparam); - g_current_window->input.scroll_delta.y += (F32)wheel_delta / (F32)WHEEL_DELTA; + g_current_window->input.scroll_delta.y += (F32)wheel_delta / (F32)WHEEL_DELTA * 6.0f; } return 0; case WM_COMMAND: diff --git a/src/renderer/renderer.h b/src/renderer/renderer.h index 2650b9b..50bfc52 100644 --- a/src/renderer/renderer.h +++ b/src/renderer/renderer.h @@ -27,5 +27,5 @@ void renderer_set_clear_color(Renderer *renderer, float r, float g, float b struct Vec2F32; Vec2F32 renderer_measure_text(const char *text, int32_t length, float font_size, void *user_data); -// Upload an R8 icon atlas texture for icon rendering +// Upload an RGBA8 icon atlas texture for icon rendering (4 bytes per pixel) void renderer_create_icon_atlas(Renderer *renderer, const uint8_t *data, int32_t w, int32_t h); diff --git a/src/renderer/renderer_dx12.cpp b/src/renderer/renderer_dx12.cpp index 3bacbe6..550ccc9 100644 --- a/src/renderer/renderer_dx12.cpp +++ b/src/renderer/renderer_dx12.cpp @@ -118,8 +118,12 @@ float rounded_rect_sdf(float2 sample_pos, float2 rect_center, float2 rect_half_s float4 PSMain(PSInput input) : SV_TARGET { float4 col = input.col; - if (input.mode > 0.5) { - // Textured glyph mode: sample font atlas alpha + if (input.mode > 1.5) { + // RGBA textured mode: sample all channels, multiply by vertex color + float4 tex = font_tex.Sample(font_smp, input.uv); + col *= tex; + } else if (input.mode > 0.5) { + // Alpha-only textured mode: sample R channel as alpha (font atlas) float alpha = font_tex.Sample(font_smp, input.uv).r; col.a *= alpha; } else { @@ -210,6 +214,11 @@ struct Renderer { HDC measure_dc; HFONT measure_font; F32 measure_font_size; + + // Clear color + float clear_r = 0.12f; + float clear_g = 0.12f; + float clear_b = 0.13f; }; //////////////////////////////// @@ -914,7 +923,7 @@ static void emit_quad_rotated(DrawBatch *batch, v[i].corner_radii[2] = 0; v[i].corner_radii[3] = 0; v[i].border_thickness = 0; v[i].softness = 0; - v[i].mode = 1.0f; + v[i].mode = 2.0f; } U32 *idx = &batch->indices[batch->index_count]; @@ -1175,7 +1184,7 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) { barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; r->command_list->ResourceBarrier(1, &barrier); - const float clear_color[4] = { 0.12f, 0.12f, 0.13f, 1.0f }; + const float clear_color[4] = { r->clear_r, r->clear_g, r->clear_b, 1.0f }; r->command_list->ClearRenderTargetView(r->rtv_descriptors[back_buffer_idx], clear_color, 0, nullptr); r->command_list->OMSetRenderTargets(1, &r->rtv_descriptors[back_buffer_idx], FALSE, nullptr); @@ -1202,14 +1211,15 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) { auto bind_font = [&]() { if (bound_texture != 0) { - flush_batch(r, &batch, buf_idx, &flush_index_start, r->srv_heap); + ID3D12DescriptorHeap *heap = bound_texture == 1 ? r->icon_srv_heap : r->srv_heap; + flush_batch(r, &batch, buf_idx, &flush_index_start, heap); bound_texture = 0; } }; auto bind_icon = [&]() { if (bound_texture != 1 && r->icon_srv_heap) { - flush_batch(r, &batch, buf_idx, &flush_index_start); + flush_batch(r, &batch, buf_idx, &flush_index_start, r->srv_heap); bound_texture = 1; } }; @@ -1330,7 +1340,7 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) { cr, cg, cb, ca, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 1.0f); + 0, 0, 2.0f); } else if (type == CUSTOM_RENDER_ROTATED_ICON) { bind_icon(); CustomRotatedIconData *ri = (CustomRotatedIconData *)custom->customData; @@ -1383,7 +1393,7 @@ void renderer_create_icon_atlas(Renderer *r, const uint8_t *data, int32_t w, int tex_desc.Height = h; tex_desc.DepthOrArraySize = 1; tex_desc.MipLevels = 1; - tex_desc.Format = DXGI_FORMAT_R8_UNORM; + tex_desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; tex_desc.SampleDesc.Count = 1; tex_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; @@ -1418,7 +1428,7 @@ void renderer_create_icon_atlas(Renderer *r, const uint8_t *data, int32_t w, int upload_buf->Map(0, &read_range, &mapped); U8 *dst = (U8 *)mapped; for (int y = 0; y < h; y++) { - memcpy(dst + y * footprint.Footprint.RowPitch, data + y * w, w); + memcpy(dst + y * footprint.Footprint.RowPitch, data + y * w * 4, w * 4); } upload_buf->Unmap(0, nullptr); @@ -1458,7 +1468,7 @@ void renderer_create_icon_atlas(Renderer *r, const uint8_t *data, int32_t w, int r->device->CreateDescriptorHeap(&srv_desc, IID_PPV_ARGS(&r->icon_srv_heap)); D3D12_SHADER_RESOURCE_VIEW_DESC srv_view = {}; - srv_view.Format = DXGI_FORMAT_R8_UNORM; + srv_view.Format = DXGI_FORMAT_R8G8B8A8_UNORM; srv_view.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; srv_view.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; srv_view.Texture2D.MipLevels = 1; @@ -1482,3 +1492,17 @@ void renderer_resize(Renderer *r, int32_t width, int32_t height) { r->width = width; r->height = height; } + +void renderer_set_clear_color(Renderer *r, float cr, float cg, float cb) { + r->clear_r = cr; + r->clear_g = cg; + r->clear_b = cb; +} + +void renderer_set_font_scale(Renderer *r, float scale) { + float target_size = 15.0f * scale; + if (fabsf(target_size - r->font_atlas_size) < 0.1f) return; + wait_for_pending(r); + if (r->font_texture) { r->font_texture->Release(); r->font_texture = nullptr; } + create_font_atlas(r, target_size); +} diff --git a/src/renderer/renderer_metal.mm b/src/renderer/renderer_metal.mm index 43b9d02..c8b866e 100644 --- a/src/renderer/renderer_metal.mm +++ b/src/renderer/renderer_metal.mm @@ -107,7 +107,10 @@ fragment float4 fragment_main(Fragment in [[stage_in]], sampler font_smp [[sampler(0)]]) { float4 col = in.col; - if (in.mode > 0.5) { + if (in.mode > 1.5) { + float4 tex = font_tex.sample(font_smp, in.uv); + col *= tex; + } else if (in.mode > 0.5) { float alpha = font_tex.sample(font_smp, in.uv).r; col.a *= alpha; } else { @@ -447,7 +450,7 @@ static void emit_quad_rotated(DrawBatch *batch, v[i].corner_radii[2] = 0; v[i].corner_radii[3] = 0; v[i].border_thickness = 0; v[i].softness = 0; - v[i].mode = 1.0f; // textured + v[i].mode = 2.0f; // RGBA textured } U32 *idx = &batch->indices[batch->index_count]; @@ -901,7 +904,7 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) { cr, cg, cb, ca, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 1.0f); + 0, 0, 2.0f); } else if (type == CUSTOM_RENDER_ROTATED_ICON) { bind_icon_texture(); CustomRotatedIconData *ri = (CustomRotatedIconData *)custom->customData; @@ -943,7 +946,7 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) { void renderer_create_icon_atlas(Renderer *r, const uint8_t *data, int32_t w, int32_t h) { MTLTextureDescriptor *tex_desc = [[MTLTextureDescriptor alloc] init]; - tex_desc.pixelFormat = MTLPixelFormatR8Unorm; + tex_desc.pixelFormat = MTLPixelFormatRGBA8Unorm; tex_desc.width = w; tex_desc.height = h; tex_desc.usage = MTLTextureUsageShaderRead; @@ -952,7 +955,7 @@ void renderer_create_icon_atlas(Renderer *r, const uint8_t *data, int32_t w, int [r->icon_texture replaceRegion:MTLRegionMake2D(0, 0, w, h) mipmapLevel:0 withBytes:data - bytesPerRow:w]; + bytesPerRow:w * 4]; } void renderer_set_clear_color(Renderer *r, float cr, float cg, float cb) { diff --git a/src/ui/ui_core.cpp b/src/ui/ui_core.cpp index 9905857..b77480e 100644 --- a/src/ui/ui_core.cpp +++ b/src/ui/ui_core.cpp @@ -204,7 +204,7 @@ void ui_begin_frame(UI_Context *ctx, F32 viewport_w, F32 viewport_h, Clay_SetCurrentContext(ctx->clay_ctx); Clay_SetLayoutDimensions(Clay_Dimensions{viewport_w, viewport_h}); Clay_SetPointerState(Clay_Vector2{mouse_pos.x, mouse_pos.y}, mouse_down != 0); - Clay_UpdateScrollContainers(true, Clay_Vector2{scroll_delta.x, scroll_delta.y}, dt); + Clay_UpdateScrollContainers(false, Clay_Vector2{scroll_delta.x, scroll_delta.y}, dt); Clay_BeginLayout(); } diff --git a/src/ui/ui_core.h b/src/ui/ui_core.h index a742e9a..52ffdff 100644 --- a/src/ui/ui_core.h +++ b/src/ui/ui_core.h @@ -170,8 +170,11 @@ struct CustomRotatedIconData { #define WIDGET_SLIDER_V_THUMB_H uis(14) #define WIDGET_FADER_HEIGHT uis(160) #define WIDGET_FADER_TRACK_W uis(4) -#define WIDGET_FADER_CAP_W uis(38) +#define WIDGET_FADER_CAP_W uis(25) #define WIDGET_FADER_CAP_H uis(52) +#define WIDGET_FADER_TICK_MAJOR_W uis(8) +#define WIDGET_FADER_TICK_MINOR_W uis(4) +#define WIDGET_FADER_TICK_H uis(1) //////////////////////////////// // Corner radius (from theme) diff --git a/src/ui/ui_icons.cpp b/src/ui/ui_icons.cpp index a1789d1..024957c 100644 --- a/src/ui/ui_icons.cpp +++ b/src/ui/ui_icons.cpp @@ -30,30 +30,80 @@ static const char *g_icon_svgs[UI_ICON_COUNT] = { )", - // UI_ICON_SLIDER_THUMB - layered body with grip ridges + // UI_ICON_SLIDER_THUMB - solid body with grip ridges R"( - - - - - - - + + + + + )", - // UI_ICON_FADER - Pro Tools-style fader cap: solid body, bright center indicator, beveled caps - R"( - - - - - - - - - - - )", + // UI_ICON_FADER - exact asset fader cap from assets/fader.svg + R"SVG( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)SVG", }; U8 *ui_icons_rasterize_atlas(S32 *out_w, S32 *out_h, S32 icon_size) { @@ -65,7 +115,7 @@ U8 *ui_icons_rasterize_atlas(S32 *out_w, S32 *out_h, S32 icon_size) { if (atlas_w < 64) atlas_w = 64; if (atlas_h < 64) atlas_h = 64; - U8 *atlas = (U8 *)calloc(atlas_w * atlas_h, 1); + U8 *atlas = (U8 *)calloc(atlas_w * atlas_h * 4, 1); if (!atlas) return nullptr; S32 pen_x = 0; @@ -76,7 +126,7 @@ U8 *ui_icons_rasterize_atlas(S32 *out_w, S32 *out_h, S32 icon_size) { lunasvg::Bitmap bmp = doc->renderToBitmap(icon_size, icon_size); if (bmp.isNull()) continue; - // Extract alpha channel from ARGB32 premultiplied into R8 + // Copy BGRA premultiplied → RGBA straight (un-premultiply) U8 *src = bmp.data(); S32 bmp_w = bmp.width(); S32 bmp_h = bmp.height(); @@ -84,9 +134,18 @@ U8 *ui_icons_rasterize_atlas(S32 *out_w, S32 *out_h, S32 icon_size) { for (S32 y = 0; y < bmp_h && y < atlas_h; y++) { for (S32 x = 0; x < bmp_w && (pen_x + x) < atlas_w; x++) { - // ARGB32 premultiplied: bytes are B, G, R, A (little-endian) - U8 a = src[y * stride + x * 4 + 3]; - atlas[y * atlas_w + pen_x + x] = a; + U8 *s = &src[y * stride + x * 4]; + S32 dst_idx = (y * atlas_w + pen_x + x) * 4; + U8 b = s[0], g = s[1], r = s[2], a = s[3]; + if (a > 0 && a < 255) { + r = (U8)((r * 255) / a); + g = (U8)((g * 255) / a); + b = (U8)((b * 255) / a); + } + atlas[dst_idx + 0] = r; + atlas[dst_idx + 1] = g; + atlas[dst_idx + 2] = b; + atlas[dst_idx + 3] = a; } } diff --git a/src/ui/ui_icons.h b/src/ui/ui_icons.h index 6a57e3d..21d3e34 100644 --- a/src/ui/ui_icons.h +++ b/src/ui/ui_icons.h @@ -20,7 +20,7 @@ struct UI_IconInfo { extern UI_IconInfo g_icons[UI_ICON_COUNT]; -// Rasterizes all icons into an R8 atlas bitmap. +// Rasterizes all icons into an RGBA8 atlas bitmap (4 bytes per pixel). // Returns malloc'd data (caller frees). Sets *out_w, *out_h to atlas dimensions. // icon_size is the pixel height to rasterize each icon at. U8 *ui_icons_rasterize_atlas(S32 *out_w, S32 *out_h, S32 icon_size); diff --git a/src/ui/ui_widgets.cpp b/src/ui/ui_widgets.cpp index 67aa4b6..40803d7 100644 --- a/src/ui/ui_widgets.cpp +++ b/src/ui/ui_widgets.cpp @@ -105,8 +105,8 @@ static void emit_shadow(Clay_BoundingBox bb, F32 ox, F32 oy, F32 radius, .backgroundColor = {0, 0, 0, per_layer}, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS + expand), .floating = { - .parentId = parent_id, .offset = { -expand + ox, -expand + oy }, + .parentId = parent_id, .zIndex = z, .attachPoints = { .element = CLAY_ATTACH_POINT_LEFT_TOP, @@ -732,8 +732,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 } }, .custom = { .customData = inset_grad }, + .border = { .color = border_color, .width = { 1, 1, 1, 1 } }, ) { if (len == 0 && !is_focused) { // Placeholder @@ -850,8 +850,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 } }, .custom = { .customData = dd_grad }, + .border = { .color = is_open ? g_theme.accent : g_theme.border, .width = { 1, 1, 1, 1 } }, ) { CLAY(text_eid, .layout = { @@ -902,7 +902,6 @@ B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected) }, .backgroundColor = g_theme.bg_dark, .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), - .border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } }, .floating = { .parentId = eid.id, .zIndex = 2000, @@ -911,7 +910,8 @@ B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected) .parent = CLAY_ATTACH_POINT_LEFT_BOTTOM, }, .attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID, - } + }, + .border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } }, ) { for (S32 i = 0; i < count; i++) { B32 is_item_selected = (*selected == i); @@ -1056,8 +1056,8 @@ S32 ui_modal(const char *id, const char *title, const char *message, }, .backgroundColor = g_theme.title_bar, .cornerRadius = { CORNER_RADIUS, CORNER_RADIUS, 0, 0 }, - .border = { .color = g_theme.border, .width = { .bottom = 1 } }, .custom = { .customData = mtb_grad }, + .border = { .color = g_theme.border, .width = { .bottom = 1 } }, ) { CLAY_TEXT(clay_str(title), &g_widget_text_config); } @@ -1264,8 +1264,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 } }, .custom = { .customData = tb_grad }, + .border = { .color = g_theme.border, .width = { .bottom = 1 } }, ) { // Title text (grows to push close button right) CLAY(WIDI(id, 3002), @@ -1916,7 +1916,6 @@ B32 ui_slider_h(const char *id, const char *label, F32 *value, F32 max_val, B32 .layout = { .sizing = { .width = CLAY_SIZING_FIXED(thumb_w), .height = CLAY_SIZING_FIXED(thumb_h) }, }, - .custom = { .customData = idata }, .floating = { .offset = { .x = normalized * (track_w - thumb_w), @@ -1928,7 +1927,9 @@ B32 ui_slider_h(const char *id, const char *label, F32 *value, F32 max_val, B32 }, .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH, .attachTo = CLAY_ATTACH_TO_PARENT, - } + .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT, + }, + .custom = { .customData = idata }, ) {} } } @@ -2092,7 +2093,6 @@ B32 ui_slider_v(const char *id, const char *label, F32 *value, F32 max_val, B32 .layout = { .sizing = { .width = CLAY_SIZING_FIXED(thumb_w), .height = CLAY_SIZING_FIXED(thumb_h) }, }, - .custom = { .customData = idata }, .floating = { .offset = { .x = 0, @@ -2104,7 +2104,9 @@ B32 ui_slider_v(const char *id, const char *label, F32 *value, F32 max_val, B32 }, .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH, .attachTo = CLAY_ATTACH_TO_PARENT, - } + .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT, + }, + .custom = { .customData = idata }, ) {} } } @@ -2210,9 +2212,12 @@ B32 ui_fader(const char *id, const char *label, F32 *value, F32 max_val, B32 is_ S32 val_len = 0; char *val_text = is_editing ? nullptr : value_format_text(*value, is_signed, &val_len); - // Dimmed accent for fill bar - Clay_Color fill_color = g_theme.accent; - fill_color.a = 160; + // Tick mark dimensions + F32 tick_major_w = WIDGET_FADER_TICK_MAJOR_W; + F32 tick_minor_w = WIDGET_FADER_TICK_MINOR_W; + F32 tick_h = WIDGET_FADER_TICK_H; + S32 num_ticks = 10; + F32 track_left = (cap_w - track_w) / 2.0f; // Layout: vertical column (label → hit area with track → value/edit) CLAY(WID(id), @@ -2236,43 +2241,31 @@ B32 ui_fader(const char *id, const char *label, F32 *value, F32 max_val, B32 is_ .childAlignment = { .x = CLAY_ALIGN_X_CENTER }, } ) { - // Visible track (centered inside hit area) + // Visible track (centered inside hit area, empty) CLAY(CLAY_IDI("FdrTrack", (int)hash), .layout = { .sizing = { .width = CLAY_SIZING_FIXED(track_w), .height = CLAY_SIZING_FIXED(track_h) }, - .childAlignment = { .y = CLAY_ALIGN_Y_BOTTOM }, }, .backgroundColor = g_theme.bg_dark, .cornerRadius = CLAY_CORNER_RADIUS(track_w / 2.0f) - ) { - // Fill bar (from bottom) - F32 fill_h = normalized * track_h; - if (fill_h < 1.0f) fill_h = 1.0f; - CLAY(CLAY_IDI("FdrFill", (int)hash), - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(fill_h) }, - }, - .backgroundColor = fill_color, - .cornerRadius = CLAY_CORNER_RADIUS(track_w / 2.0f) - ) {} - } + ) {} - // Floating fader cap (icon, silver colored) - if (g_icon_pool_count < UI_MAX_ICONS_PER_FRAME) { - CustomIconData *idata = &g_icon_pool[g_icon_pool_count++]; - idata->type = CUSTOM_RENDER_ICON; - idata->icon_id = (S32)UI_ICON_FADER; - idata->color = Clay_Color{200, 200, 210, 255}; + // Tick marks on both sides of the track + for (S32 i = 0; i <= num_ticks; i++) { + F32 norm = (F32)i / (F32)num_ticks; + F32 tick_y = (1.0f - norm) * (track_h - tick_h); + F32 tw = (i % 5 == 0) ? tick_major_w : tick_minor_w; - CLAY(CLAY_IDI("FdrCap", (int)hash), + // Left tick + CLAY(CLAY_IDI("FdrTkL", (int)(hash * 100 + i)), .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(cap_w), .height = CLAY_SIZING_FIXED(cap_h) }, + .sizing = { .width = CLAY_SIZING_FIXED(tw), .height = CLAY_SIZING_FIXED(tick_h) }, }, - .custom = { .customData = idata }, + .backgroundColor = g_theme.text_dim, .floating = { .offset = { - .x = 0, - .y = (1.0f - normalized) * (track_h - cap_h), + .x = track_left - tw, + .y = tick_y, }, .attachPoints = { .element = CLAY_ATTACH_POINT_LEFT_TOP, @@ -2280,7 +2273,78 @@ B32 ui_fader(const char *id, const char *label, F32 *value, F32 max_val, B32 is_ }, .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH, .attachTo = CLAY_ATTACH_TO_PARENT, - } + .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT, + }, + ) {} + + // Right tick + CLAY(CLAY_IDI("FdrTkR", (int)(hash * 100 + i)), + .layout = { + .sizing = { .width = CLAY_SIZING_FIXED(tw), .height = CLAY_SIZING_FIXED(tick_h) }, + }, + .backgroundColor = g_theme.text_dim, + .floating = { + .offset = { + .x = track_left + track_w, + .y = tick_y, + }, + .attachPoints = { + .element = CLAY_ATTACH_POINT_LEFT_TOP, + .parent = CLAY_ATTACH_POINT_LEFT_TOP, + }, + .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH, + .attachTo = CLAY_ATTACH_TO_PARENT, + .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT, + }, + ) {} + } + + // Floating fader cap (RGBA icon from asset SVG) + if (g_icon_pool_count < UI_MAX_ICONS_PER_FRAME) { + CustomIconData *idata = &g_icon_pool[g_icon_pool_count++]; + idata->type = CUSTOM_RENDER_ICON; + idata->icon_id = (S32)UI_ICON_FADER; + idata->color = Clay_Color{255, 255, 255, 255}; + + F32 cap_y = (1.0f - normalized) * (track_h - cap_h); + + CLAY(CLAY_IDI("FdrCap", (int)hash), + .layout = { + .sizing = { .width = CLAY_SIZING_FIXED(cap_w), .height = CLAY_SIZING_FIXED(cap_h) }, + }, + .floating = { + .offset = { .x = 0, .y = cap_y }, + .attachPoints = { + .element = CLAY_ATTACH_POINT_LEFT_TOP, + .parent = CLAY_ATTACH_POINT_LEFT_TOP, + }, + .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH, + .attachTo = CLAY_ATTACH_TO_PARENT, + .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT, + }, + .custom = { .customData = idata }, + ) {} + + // Color tint overlay on top of fader cap + Clay_Color tint = g_theme.accent; + tint.a = 80; + F32 cap_corner = cap_w * (3.0f / 62.2f); + CLAY(CLAY_IDI("FdrTint", (int)hash), + .layout = { + .sizing = { .width = CLAY_SIZING_FIXED(cap_w), .height = CLAY_SIZING_FIXED(cap_h) }, + }, + .backgroundColor = tint, + .cornerRadius = CLAY_CORNER_RADIUS(cap_corner), + .floating = { + .offset = { .x = 0, .y = cap_y }, + .attachPoints = { + .element = CLAY_ATTACH_POINT_LEFT_TOP, + .parent = CLAY_ATTACH_POINT_LEFT_TOP, + }, + .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH, + .attachTo = CLAY_ATTACH_TO_PARENT, + .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT, + }, ) {} } }