If you want something very simple, you can just save the position of the click and the scrollbar and set a variable to a state that represents that we’re in scrollbar-dragging mode. While in this dragging mode, you update the scrollbar position on the mouse movement, and button up events by looking at the difference of the starting point and where the cursor is now. The button up event also clears the dragging state.
Here’s a very simple example with a scrollbar. I aimed to keep the code somewhat short. Getting the exact behavior of the scrollbars used in modern user interfaces is too much for just an example.
scrollbar_verysimple.c
#include <SDL.h>
static SDL_Window * window;
static SDL_Renderer * renderer;
static int window_width, window_height;
/* Padding around the rectangles. */
#define PADDING 4
#define SCROLLBAR_WIDTH 30
static struct {
/* Coordinates and size in window. */
SDL_Rect rect;
/* Value from 0 to 1. */
float v;
/* Button down position and dragging state. */
int my;
int dragging;
/* Also need to keep track of the original value when dragging started. */
float dv;
} scrollbar;
/* Need some content to show in the area that scrolls. Some random lines will do. */
#define CONTENT_HEIGHT 2000
#define CONTENT_POINTS 300
static struct {
SDL_Point points[CONTENT_POINTS];
} content;
static Uint32 xstate = 905309021;
static Uint32 xorshift()
{
xstate ^= xstate << 13;
xstate ^= xstate >> 17;
xstate ^= xstate << 5;
return xstate;
}
static int ScrollbarGetThumbSize()
{
/* Simple 10% height. */
int scrollarea = scrollbar.rect.h - PADDING * 2;
int height = scrollarea / 10;
/* To avoid some issues in the drawing functions, this just returns 1. */
if (scrollarea < 1) {
return 1;
}
if (height < 10) {
if (scrollarea < 10) {
/* This is annoying to handle. Needs up and down buttons on the bar. */
height = scrollarea;
} else {
height = 10;
}
}
return height;
}
static void ScrollbarWindowSizeUpdate()
{
scrollbar.rect.x = window_width - SCROLLBAR_WIDTH - PADDING;
scrollbar.rect.y = PADDING;
scrollbar.rect.w = SCROLLBAR_WIDTH;
scrollbar.rect.h = window_height - PADDING * 2;
}
static int ScrollbarScrollareaSize()
{
int thumb = ScrollbarGetThumbSize();
return scrollbar.rect.h - thumb - PADDING * 2;
}
static void ScrollbarUpdateValue(int my)
{
/* I chose to implement it this way because it doesn't take much code. */
int dy = my - scrollbar.my;
scrollbar.v = scrollbar.dv + 1.f / ScrollbarScrollareaSize() * dy;
if (scrollbar.v < 0) {
scrollbar.v = 0;
} else if (scrollbar.v > 1) {
scrollbar.v = 1;
}
}
static void ScrollbarButtonDown(int mx, int my)
{
SDL_Point m = {mx, my};
SDL_Rect rect = scrollbar.rect;
int thumb = ScrollbarGetThumbSize();
/* Check if a click inside the scrollbar happened. */
rect.y += PADDING;
rect.h -= PADDING * 2;
if (!SDL_PointInRect(&m, &rect)) {
return;
}
/* A click above or below the thumb just centers it on the cursor. */
/* Not the behavior of your usual scrollbar, but it works ok here. */
rect.y += (int)(ScrollbarScrollareaSize() * scrollbar.v);
rect.h = thumb;
if (!SDL_PointInRect(&m, &rect)) {
scrollbar.v = (float)(my - scrollbar.rect.y - PADDING * 2 - thumb / 2) / ScrollbarScrollareaSize();
if (scrollbar.v < 0) {
scrollbar.v = 0;
} else if (scrollbar.v > 1) {
scrollbar.v = 1;
}
}
/* Set dragging state. */
scrollbar.my = my;
scrollbar.dv = scrollbar.v;
scrollbar.dragging = 1;
}
static void ScrollbarMouseMovement(int my)
{
if (!scrollbar.dragging) {
return;
}
ScrollbarUpdateValue(my);
}
static void ScrollbarButtonUp(int my)
{
if (!scrollbar.dragging) {
return;
}
ScrollbarUpdateValue(my);
/* Clear dragging state. */
scrollbar.dragging = 0;
}
static void ScrollbarDraw()
{
int thumb = ScrollbarGetThumbSize();
SDL_Rect rect = scrollbar.rect;
SDL_SetRenderDrawColor(renderer, 0xff, 0xff, 0xff, 0xff);
SDL_RenderDrawRect(renderer, &rect);
rect.x += PADDING;
rect.y = scrollbar.rect.y + PADDING;
rect.y += (int)(ScrollbarScrollareaSize() * scrollbar.v);
rect.h = thumb;
rect.w -= PADDING * 2;
SDL_RenderFillRect(renderer, &rect);
}
static void ContentCreate()
{
size_t i;
for (i = 1; i < CONTENT_POINTS; i++) {
content.points[i].x = xorshift() % 4000;
content.points[i].y = xorshift() % CONTENT_HEIGHT;
}
/* Push some points to the corners. */
content.points[0].x = 0;
content.points[0].y = 0;
content.points[CONTENT_POINTS / 3].x = 4000;
content.points[CONTENT_POINTS / 3].y = 0;
content.points[CONTENT_POINTS / 3 * 2].x = 0;
content.points[CONTENT_POINTS / 3 * 2].y = CONTENT_HEIGHT;
content.points[CONTENT_POINTS - 1].x = 4000;
content.points[CONTENT_POINTS - 1].y = CONTENT_HEIGHT;
}
static void ContentDraw()
{
SDL_Rect rect;
SDL_Point points[CONTENT_POINTS];
int overlap, offset;
size_t i;
rect.x = PADDING;
rect.y = PADDING;
rect.w = scrollbar.rect.x - PADDING * 2;
rect.h = window_height - PADDING * 2;
/* Offset the lines depending on the padding and scrollbar value. */
overlap = CONTENT_HEIGHT - (rect.h - 2);
offset = (int)(overlap * scrollbar.v) - (PADDING + 1);
for (i = 0; i < CONTENT_POINTS; i++) {
points[i].x = content.points[i].x;
points[i].y = content.points[i].y - offset;
}
SDL_SetRenderDrawColor(renderer, 0xff, 0xff, 0xff, 0xff);
SDL_RenderDrawRect(renderer, &rect);
rect.x++;
rect.y++;
rect.w -= 2;
rect.h -= 2;
SDL_RenderSetClipRect(renderer, &rect);
SDL_SetRenderDrawColor(renderer, 0x55, 0x55, 0xff, 0xff);
SDL_RenderDrawLines(renderer, points, CONTENT_POINTS);
SDL_RenderSetClipRect(renderer, NULL);
}
static int run(int argc, char * argv[])
{
int done = 0;
SDL_Event e;
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER) < 0) {
SDL_Log("Could not initialize SDL: %s\n", SDL_GetError());
return 10;
}
window = SDL_CreateWindow("Scrollbar 1", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 450, SDL_WINDOW_RESIZABLE);
if (window == NULL) {
SDL_Log("Could not create window: %s\n", SDL_GetError());
return 20;
}
renderer = SDL_CreateRenderer(window, -1, 0);
if (renderer == NULL) {
SDL_Log("Could not create renderer: %s\n", SDL_GetError());
return 20;
}
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_GetWindowSize(window, &window_width, &window_height);
ScrollbarWindowSizeUpdate();
ContentCreate();
while (!done) {
while (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT) {
done = 1;
} else if (e.type == SDL_KEYDOWN) {
Uint32 sym = e.key.keysym.sym;
if (sym == SDLK_c) {
ContentCreate();
}
} else if (e.type == SDL_KEYUP) {
Uint32 sym = e.key.keysym.sym;
if (sym == SDLK_ESCAPE) {
done = 1;
}
} else if (e.type == SDL_MOUSEMOTION) {
ScrollbarMouseMovement(e.motion.y);
} else if (e.type == SDL_MOUSEBUTTONDOWN) {
if (e.button.button == SDL_BUTTON(SDL_BUTTON_LEFT)) {
ScrollbarButtonDown(e.button.x, e.button.y);
}
} else if (e.button.type == SDL_MOUSEBUTTONUP) {
if (e.button.button == SDL_BUTTON(SDL_BUTTON_LEFT)) {
ScrollbarButtonUp(e.button.y);
}
} else if (e.type == SDL_WINDOWEVENT) {
Uint8 ev = e.window.event;
if (ev == SDL_WINDOWEVENT_CLOSE) {
done = 1;
} else if (ev == SDL_WINDOWEVENT_RESIZED || ev == SDL_WINDOWEVENT_SIZE_CHANGED) {
window_width = e.window.data1;
window_height = e.window.data2;
ScrollbarWindowSizeUpdate();
}
}
}
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 0xff, 0xff, 0xff, 0xff);
ContentDraw();
ScrollbarDraw();
SDL_RenderPresent(renderer);
SDL_Delay(1);
}
return 0;
}
int main(int argc, char * argv[])
{
int ret = run(argc, argv);
if (renderer) {
SDL_DestroyRenderer(renderer);
}
if (window) {
SDL_DestroyWindow(window);
}
if (SDL_WasInit(SDL_INIT_EVERYTHING)) {
SDL_Quit();
}
return ret;
}
This may work fine with just one scrollbar, but it will get complicated fast once you have mutliple controls on screen. At that point it’s probably be better to create some kind of framework that organizes these controls.
A straight forward approach for a user interface is to create control objects with event handling and hit-testing. When a click happens, the program steps through the list of controls and asks them if a click at that position is something they care about. The program may stop searching if a control says that it processed the click. Same with mouse movement and mouse release.
There are a lot of UI projects (big and small) out there that can give you insight on other ways to implement it. There was so much research and work done on this area, it would be a shame not to take advantage of all these resources.