factor out scroll into ui

This commit is contained in:
2026-03-13 10:17:13 -04:00
parent 4e0d9fdcca
commit 68fc916bc1
4 changed files with 168 additions and 111 deletions

2
.vscode/tasks.json vendored
View File

@@ -47,7 +47,7 @@
}, },
"group": "build", "group": "build",
"problemMatcher": { "problemMatcher": {
"owner": "cpp", "owner": "c",
"fileLocation": ["relative", "${workspaceFolder}"], "fileLocation": ["relative", "${workspaceFolder}"],
"pattern": { "pattern": {
"regexp": "^(.+?):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$", "regexp": "^(.+?):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",

View File

@@ -130,10 +130,8 @@ typedef struct AppState {
F32 demo_slider_v; F32 demo_slider_v;
F32 demo_fader; F32 demo_fader;
// Scrollbar drag state // Main content scroll state
B32 scrollbar_dragging; UI_ScrollState main_scroll;
F32 scrollbar_drag_start_y;
F32 scrollbar_drag_start_scroll;
// Audio device selection // Audio device selection
S32 audio_device_sel; // dropdown index: 0 = None, 1+ = device 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); ui_tab_bar("MainTabRow", main_tabs, 1, &sel);
} }
CLAY(CLAY_ID("MainScrollArea"), ui_scroll_begin("MainScroll", &app->main_scroll);
.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) // Top highlight (beveled edge)
CLAY(CLAY_ID("MainHighlight"), CLAY(CLAY_ID("MainHighlight"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } }, .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
@@ -410,97 +395,7 @@ static void build_main_panel(AppState *app) {
} }
} }
} }
ui_scroll_end("MainScroll", &app->main_scroll);
// 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
} }
} }

View File

@@ -218,6 +218,14 @@ static Clay_String clay_str(const char *s) {
#define WID(s) CLAY_SID(clay_str(s)) #define WID(s) CLAY_SID(clay_str(s))
#define WIDI(s, i) CLAY_SIDI(clay_str(s), i) #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 // Icon
@@ -2052,3 +2060,135 @@ B32 ui_fader(const char *id, const char *label, F32 *value, F32 max_val, B32 is_
return changed; 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();
}

View File

@@ -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). // 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); 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);