diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9892a04..6cdb9c9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -47,7 +47,7 @@ }, "group": "build", "problemMatcher": { - "owner": "cpp", + "owner": "c", "fileLocation": ["relative", "${workspaceFolder}"], "pattern": { "regexp": "^(.+?):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$", diff --git a/src/daw/daw_main.c b/src/daw/daw_main.c index 680241f..fda79af 100644 --- a/src/daw/daw_main.c +++ b/src/daw/daw_main.c @@ -130,10 +130,8 @@ typedef 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; + // Main content scroll state + UI_ScrollState main_scroll; // Audio device selection S32 audio_device_sel; // dropdown index: 0 = None, 1+ = device @@ -228,21 +226,8 @@ 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() }, - ) { + ui_scroll_begin("MainScroll", &app->main_scroll); + { // Top highlight (beveled edge) CLAY(CLAY_ID("MainHighlight"), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } }, @@ -410,97 +395,7 @@ 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) { - F32 track_h = scroll_data.scrollContainerDimensions.height; - F32 content_h = scroll_data.contentDimensions.height; - F32 visible_ratio = track_h / content_h; - F32 thumb_h = Max(visible_ratio * track_h, uis(24)); - F32 scroll_range = content_h - track_h; - F32 scroll_pct = scroll_range > 0 ? -scroll_data.scrollPosition->y / scroll_range : 0; - F32 thumb_y = scroll_pct * (track_h - thumb_h); - F32 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; - F32 click_rel = input.mouse_pos.y - track_bb.y; - F32 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) { - F32 dy = input.mouse_pos.y - app->scrollbar_drag_start_y; - F32 scroll_per_px = scroll_range / (track_h - thumb_h); - F32 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){ - (F32)Min((S32)thumb_color.r + 30, 255), - (F32)Min((S32)thumb_color.g + 30, 255), - (F32)Min((S32)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 + ui_scroll_end("MainScroll", &app->main_scroll); } } diff --git a/src/ui/ui_widgets.c b/src/ui/ui_widgets.c index d96ca51..7e96396 100644 --- a/src/ui/ui_widgets.c +++ b/src/ui/ui_widgets.c @@ -218,6 +218,14 @@ static Clay_String clay_str(const char *s) { #define WID(s) CLAY_SID(clay_str(s)) #define WIDI(s, i) CLAY_SIDI(clay_str(s), i) +// Sub-element index constants for ui_scroll +enum { + SCROLL_IDX_AREA = 0, + SCROLL_IDX_CONTENT = 1, + SCROLL_IDX_TRACK = 2, + SCROLL_IDX_THUMB = 3, +}; + //////////////////////////////// // Icon @@ -2052,3 +2060,135 @@ B32 ui_fader(const char *id, const char *label, F32 *value, F32 max_val, B32 is_ return changed; } + +//////////////////////////////// +// Scrollable area with custom scrollbar +// +// CLAY() is a for-loop macro, so we can't split begin/end across two function +// calls while keeping the content in the caller. Instead, ui_scroll_begin opens +// the outer wrapper + scrollable content element using Clay__OpenElement / +// Clay__ConfigureOpenElement, and ui_scroll_end closes them and renders the +// scrollbar. + +void ui_scroll_begin(const char *id, UI_ScrollState *state) { + (void)state; + Clay_String cs = clay_str(id); + + // Open outer row (content + scrollbar side by side) + Clay__OpenElementWithId(CLAY_SIDI(cs, SCROLL_IDX_AREA)); + Clay__ConfigureOpenElement((Clay_ElementDeclaration) { + .layout = { + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, + .layoutDirection = CLAY_LEFT_TO_RIGHT, + }, + }); + + // Open scrollable content area + Clay__OpenElementWithId(CLAY_SIDI(cs, SCROLL_IDX_CONTENT)); + Clay__ConfigureOpenElement((Clay_ElementDeclaration) { + .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() }, + }); + // Caller inserts content here, then calls ui_scroll_end() +} + +void ui_scroll_end(const char *id, UI_ScrollState *state) { + Clay_String cs = clay_str(id); + + // Close scrollable content area + Clay__CloseElement(); + + // Draw scrollbar + Clay_ElementId content_eid = CLAY_SIDI(cs, SCROLL_IDX_CONTENT); + Clay_ScrollContainerData scroll_data = Clay_GetScrollContainerData(content_eid); + + if (scroll_data.found && scroll_data.contentDimensions.height > scroll_data.scrollContainerDimensions.height) { + F32 track_h = scroll_data.scrollContainerDimensions.height; + F32 content_h = scroll_data.contentDimensions.height; + F32 visible_ratio = track_h / content_h; + F32 thumb_h = Max(visible_ratio * track_h, uis(24)); + F32 scroll_range = content_h - track_h; + F32 scroll_pct = scroll_range > 0 ? -scroll_data.scrollPosition->y / scroll_range : 0; + F32 thumb_y = scroll_pct * (track_h - thumb_h); + F32 bar_w = uis(8); + + Clay_ElementId thumb_eid = CLAY_SIDI(cs, SCROLL_IDX_THUMB); + Clay_ElementId track_eid = CLAY_SIDI(cs, SCROLL_IDX_TRACK); + B32 thumb_hovered = Clay_PointerOver(thumb_eid); + B32 track_hovered = Clay_PointerOver(track_eid); + PlatformInput input = g_wstate.input; + B32 mouse_clicked = input.mouse_down && !input.was_mouse_down; + + if (mouse_clicked && thumb_hovered) { + state->dragging = true; + state->drag_start_y = input.mouse_pos.y; + state->drag_start_scroll = scroll_data.scrollPosition->y; + } else if (mouse_clicked && track_hovered && !thumb_hovered) { + Clay_BoundingBox track_bb = Clay_GetElementData(track_eid).boundingBox; + F32 click_rel = input.mouse_pos.y - track_bb.y; + F32 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; + state->dragging = true; + state->drag_start_y = input.mouse_pos.y; + state->drag_start_scroll = scroll_data.scrollPosition->y; + } + + if (!input.mouse_down) { + state->dragging = false; + } + + if (state->dragging) { + F32 dy = input.mouse_pos.y - state->drag_start_y; + F32 scroll_per_px = scroll_range / (track_h - thumb_h); + F32 new_scroll = state->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; + } + + Clay_Color thumb_color = g_theme.scrollbar_grab; + if (state->dragging || thumb_hovered) { + thumb_color = (Clay_Color){ + (F32)Min((S32)thumb_color.r + 30, 255), + (F32)Min((S32)thumb_color.g + 30, 255), + (F32)Min((S32)thumb_color.b + 30, 255), + thumb_color.a + }; + } + + CLAY(track_eid, + .layout = { + .sizing = { .width = CLAY_SIZING_FIXED(bar_w), .height = CLAY_SIZING_GROW() }, + }, + .backgroundColor = g_theme.scrollbar_bg + ) { + CLAY(thumb_eid, + .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 { + state->dragging = false; + } + + // Close outer row + Clay__CloseElement(); +} diff --git a/src/ui/ui_widgets.h b/src/ui/ui_widgets.h index d3e2248..9fa278f 100644 --- a/src/ui/ui_widgets.h +++ b/src/ui/ui_widgets.h @@ -128,3 +128,25 @@ B32 ui_slider_v(const char *id, const char *label, F32 *value, F32 max_val, B32 // DAW-style fader (vertical slider with fader cap icon). B32 ui_fader(const char *id, const char *label, F32 *value, F32 max_val, B32 is_signed, F32 default_val, B32 editable); + +//////////////////////////////// +// Scrollable area with custom scrollbar +// +// Usage: +// UI_ScrollState scroll = {0}; // persist across frames +// ui_scroll_begin("MyScroll", &scroll); +// /* ... scrollable content ... */ +// ui_scroll_end("MyScroll", &scroll); + +typedef struct UI_ScrollState { + B32 dragging; + F32 drag_start_y; + F32 drag_start_scroll; +} UI_ScrollState; + +// Opens a horizontal row containing a vertical-scroll content area. +// The content area clips vertically and uses Clay_GetScrollOffset(). +void ui_scroll_begin(const char *id, UI_ScrollState *state); + +// Closes the content area and draws the scrollbar thumb/track. +void ui_scroll_end(const char *id, UI_ScrollState *state);