Blink the caret while editing text

In a browser and most text editors there’s a caret that shows where you’re inserting text. I was thinking, every 500 milliseconds it’ll either become visible or invisible (at the moment it’s always visible because I don’t like the blink). When it’s becoming visible I could draw one texture but SDL_RenderPresent (quoted below) suggest I must clear and redraw everything. Is there a case where I won’t need to?

My code is simple enough that I can cache a lot of geometry between frames but it’s already so fast I might not bother (for now, it’s 2ms of work). It feels a little awkward to redraw just to blink a single texture

The backbuffer should be considered invalidated after each present; do not assume that previous contents will exist between frames. You are strongly encouraged to call SDL_RenderClear() to initialize the backbuffer before starting each new frame’s drawing, even if you plan to overwrite every pixel.

I haven’t tested either of these, but I think one of them might do the trick:
SDL_SetRenderClipRect
SDL_SetRenderViewport

I’ll try to set up some kind of test, but I’m a bit distracted right now. You’ll probably beat me to a result.

You may have misread my post :rofl:

Yeah.
Neither of those functions do what I thought they might do, and I’m just left unclear in what their effects/purposes actually are. Sorry.

Back with SDL1, you could call SDL_UpdateRect to update only part of a window, but indeed it has since been replaced by SDL_RenderPresent, which always updates the entire window.

If you want to keep CPU/GPU usage caused by a blinking cursor down, one option is to render the expensive part to a texture using SDL_SetRenderTarget. Then, when it’s time to redraw the window, you’d:

  • Update the texture with your geometry only if needed.
  • Render the texture to the window.
  • Render the cursor to the window, in case it’s visible.
  • Call SDL_RenderPresent.

This optimizes for blinking cursor case, while taking a bit more memory (additional texture) and drawing time (painting said texture). Also there will be a bit more overhead for resizing the window, since it means reallocating the texture at the correct size.

1 Like

In my project I do something similar — the window uses an additional texture, for the purpose of rendering additional effects and this texture is no smaller than the window.

In order not to do a lot of relocations of this texture, I pre-allocate it in the native size of the main screen. However, if the window resizes to a larger size than this texture, I reallocate this texture, always with an additional spare margin (currently 128 pixels). This way its reallocation frequency decreases drastically.

There are many ways to solve this, depending on whether we care more about saving VRAM or increasing performance. I myself chose a compromise, i.e. pre-using more VRAM, but still with the possibility of dynamically expanding the texture size, with spare space to do it rarely.

OK, So after looking into it further, SDL_SetRenderClipRect looks like the function that you want. It allows you to specify a small rectangle in which drawing occurs. All draw calls landing outside the rectangle are ignored by the GPU, and it can reduce GPU processing a lot.

But SDL_Clear breaks SDL_SetRenderClipRect. ← According to this link, it fails to take the clipping rectangle into account when clearing the screen. It sounds like this has been an ongoing issue. @icculus I can confirm that it has carried over into SDL3 (on XFCE4 Ubuntu).

The reason that SDL_Clear is so strongly recommended to begin with has to do with MacOS/IOS GPUs running in a slower “Preserve Framebuffer” mode when you fail to call it. If there are no moving objects on the screen that need to be overdrawn, it is safe to skip SDL_Clear(), just be aware that doing so may be less optimal on some devices (Mainly Mac).

Since SDL_Clear breaks things, I wrote a small (142 lines) example in which SDL_WaitEventTimeout handles drawing a blinking cursor. It simply avoids SDL_Clear while in a blinking state. Using SDL_SetRenderClipRect is not required here. You’ll need to change the font “roboto.ttf” to some font path that you have on your computer:

#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <string>

SDL_Color darkGray = {50, 50, 60, 255};

SDL_Texture * updateTextbox(SDL_Renderer * renderer, SDL_FRect & box, TTF_Font * font, const std::string text)
{
	SDL_Texture * dest = NULL;
	if(font)
	{
		if(text.length() > 0)
		{
			SDL_Surface * temp = TTF_RenderText_Blended(font, text.c_str(), text.length(), darkGray);
			if(temp)
			{
				dest = SDL_CreateTextureFromSurface(renderer, temp);
				box.w = temp->w;
				box.h = temp->h;
				SDL_DestroySurface(temp);
			}
			else
			{
				SDL_Log("Failed to render text to a surface");
			}
		}	
	}
	return dest;
}
int main()
{
	SDL_Init(SDL_INIT_VIDEO);
	TTF_Init();
	SDL_Window * window = SDL_CreateWindow("Txt", 800, 800, SDL_WINDOW_RESIZABLE);
	SDL_Renderer * renderer = SDL_CreateRenderer(window, 0);
	SDL_SetRenderVSync(renderer, 1);

	TTF_Font * font = TTF_OpenFont("roboto.ttf", 50);
	if(!font)
	{
		SDL_Log("Failed to load font");
	}
	SDL_FPoint cursorPos = {30, 30};
	SDL_FRect textBox = {30, 30, 740, 50};
	std::string text = "";
	SDL_Texture * textOutput = NULL;
	textOutput = updateTextbox(renderer, textBox, font, "Type Something");

	SDL_StartTextInput(window);
	bool blinkOn = true;
	bool run = true;
	int cursor = 0;
	while(run)
	{
		while(SDL_WaitEventTimeout(NULL, 500) == 0)
		{
			SDL_Rect cursorView = {(int)cursorPos.x, (int)cursorPos.y, 3, (int) textBox.h + 1};
			SDL_SetRenderClipRect(renderer, &cursorView);
			// no user input, blink text cursor
			blinkOn = !blinkOn;
			if(blinkOn)
			{
				SDL_SetRenderDrawColor(renderer, 215, 225, 255, 255);
				SDL_RenderLine(renderer, cursorPos.x, cursorPos.y, cursorPos.x, cursorPos.y + textBox.h);
			}
			else
			{
				SDL_SetRenderDrawColor(renderer, 10, 10, 10, 255);
				SDL_RenderLine(renderer, cursorPos.x, cursorPos.y, cursorPos.x, cursorPos.y + textBox.h);
			}
			SDL_RenderPresent(renderer);
			SDL_SetRenderClipRect(renderer, NULL);
		}
		SDL_Event ev;
		while(SDL_PollEvent(&ev))
		{
			switch(ev.type)
			{
				case SDL_EVENT_TEXT_INPUT:
					text.insert(cursor, ev.text.text);
					cursor ++;
					if(textOutput)
					{
						SDL_DestroyTexture(textOutput);
					}
					textOutput = updateTextbox(renderer, textBox, font, text);
					break;
				case SDL_EVENT_KEY_DOWN:
					switch(ev.key.key)
					{
						case SDLK_BACKSPACE:
							if(cursor > 0)
							{
								text.erase(cursor - 1, 1);
								cursor --;
								if(textOutput)
								{
									SDL_DestroyTexture(textOutput);
								}
								textOutput = updateTextbox(renderer, textBox, font, text);
							}
							break;
						case SDLK_LEFT:
							cursor --;
							if(cursor < 0)
							{
								cursor = 0;
							}
							break;
						case SDLK_RIGHT:
							cursor ++;
							if(cursor > text.length())
							{
								cursor = text.length();
							}
							break;
						case SDLK_RETURN:
							SDL_Log("User has input this text: %s", text.c_str());
							break;
						case SDLK_ESCAPE:
							run = false;
							break;
					}
					break;
				case SDL_EVENT_QUIT:
					run = false;
					break;
			}
		}
		int width = 0;
		if(cursor > 0)
		{
			TTF_GetStringSize(font, text.substr(0, cursor).c_str(), cursor, &width, NULL);
		}
		cursorPos.x = textBox.x + width;

		SDL_SetRenderDrawColor(renderer, 255, 245, 255, 255);
		SDL_RenderClear(renderer);
		SDL_SetRenderDrawColor(renderer, 5, 25, 55, 255);
	//	box outline:
	//	SDL_RenderRect(renderer, &textBox);
		SDL_RenderLine(renderer, cursorPos.x, cursorPos.y, cursorPos.x, cursorPos.y + textBox.h);
		SDL_RenderTexture(renderer, textOutput, NULL, &textBox);
		SDL_RenderPresent(renderer);
	}
	TTF_CloseFont(font);
	TTF_Quit();
	SDL_Quit();
}

For coders looking for help with textbox: This is not how text input code should be set up, this is rough test code meant to have reduced line count and minimal functionality. Feel free to grab the parts that make sense, but please use structs/classes to build a much more useful text I/O API.

You didn’t have to do all that :sweat_smile:
I mentioned it takes 2ms to render because it really isn’t a big deal. I’m thinking I should cache geomentry because it’d be so simple and I wouldn’t have to worry about how many platforms work.

It’s interesting that mac and ios will “Preserve Framebuffer”
I’m still interested in minimizing frame latency and after changing my linux distro (and going back) I seem to not be able to reproduce drawing a frame 30ms after a key press

I’m reconsidering sdl_gpu but my code is so simple it only take a few hours and it might make more sense to do it later when there are more official examples. Or maybe I’ll just try on the weekend After a quick look I can’t see any differences in the latency test and after recording the screen there appears to be no difference. I’ll have to try on an install where I can measure results