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",
|
"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+(.*)$",
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user