Dragging a sprite

Hello! I am trying to figure out how to drag a sprite. Currently, I am able to figure out if the left mouse button was pressed on the sprite, but I am not sure what to do from there. In the picture below, I am trying to make it so the scrollbar on the right moves up and down like a normal scrollbar would.

Capture

My code looks something like this so far (pseudo-code):

if (mousePressed(SDL_BUTTON_LEFT)) {
   currentMousePosition = getMousePosition();       // This part works
}

if (mouseReleased(SDL_BUTTON_LEFT)) {
     // What do I do in here?
}

Any help would be appreciated! Thank you!

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.

1 Like

Thank you so much! I was able to make the scroll bar work thanks to your explanation and code. Also, I have read a lot on interface design and I do have control objects with individual event, update, and render handling. I made it pretty similar to how Unity does it (GameObjects and Components). So the above picture has 20 list item objects (8 shown, rest are clipped), 1 list object (white rectangle around everything), and 1 scrollbar object.

With that said, one thing I don’t understand is how you are making the area on the left move with it. I see that you are doing it in ContentDraw():

/* 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;
  }

However, when I try to do that in the picture above, it only moves once when I move the scrollbar all the way down. The left side jumps down instead of moving smoothly like yours. To move the left side I pass in the offset as listHeight * scrollbar.v then set the y of each list item by listItem.y - offset. In my List::draw() function, I loop through each ListItem like you did and pass in the same offset for each one. Is there something I am missing?

Thanks for the help, really appreciate it!

Never mind! It was something stupid that I did (was converting scrollbar value to an int at some point, so it was always zero unless the value was one D:). Thank you for all the help! Everything works great and I learned a lot from this!