factor out scroll into ui
This commit is contained in:
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"group": "build",
|
||||
"problemMatcher": {
|
||||
"owner": "cpp",
|
||||
"owner": "c",
|
||||
"fileLocation": ["relative", "${workspaceFolder}"],
|
||||
"pattern": {
|
||||
"regexp": "^(.+?):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user