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,
+ },
) {}
}
}