Browse Source

Add SDL 3 support to all SDL backends

Improvements to both SDL 2 and SDL 3:
- Keyboard is activated and deactivated when focusing text input fields.
- Text input events are only submitted when text input fields are focused.

SDL 3-specific improvements:
- Enables positioning of the input method editor (IME) to the text cursor.

SDL_GL2-specific improvements:
- Use OpenGL directly instead of the SDL renderer, just like the SDL_GL3 renderer.
- GLEW is no longer required, stop linking to it.

By default, CMake will now first look for SDL 3, and only if that is not found, it will look for SDL 2. To override the automatic selection, set `RMLUI_SDL_VERSION_MAJOR` to the desired version (2 or 3). Here, an empty value means default (automatic) behavior.
Michael Ragazzon 11 months ago
parent
commit
01fc3c40aa

+ 3 - 3
.github/workflows/build.yml

@@ -34,7 +34,7 @@ jobs:
     - name: Install Dependencies
       run: |-
         sudo apt-get update
-        sudo apt-get install cmake ninja-build libsdl2-dev libsdl2-image-dev libfreetype6-dev libharfbuzz-dev libglew-dev liblua5.2-dev libsfml-dev librlottie-dev libglfw3-dev
+        sudo apt-get install cmake ninja-build libsdl2-dev libsdl2-image-dev libfreetype6-dev libharfbuzz-dev liblua5.2-dev libsfml-dev librlottie-dev libglfw3-dev
 
     - name: Configure CMake
       run: >-
@@ -77,7 +77,7 @@ jobs:
       - name: Install Dependencies
         run: |-
           sudo apt-get update
-          sudo apt-get install cmake ninja-build libsdl2-dev libsdl2-image-dev libfreetype6-dev libharfbuzz-dev libglew-dev liblua5.2-dev libglfw3-dev
+          sudo apt-get install cmake ninja-build libsdl2-dev libsdl2-image-dev libfreetype6-dev libharfbuzz-dev liblua5.2-dev libglfw3-dev
 
       - name: Configure CMake
         run: >-
@@ -163,7 +163,7 @@ jobs:
         run: |-
           C:\msys64\usr\bin\bash -lc ("pacman --needed --noconfirm --sync " +
             "mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-freetype mingw-w64-x86_64-lua " +
-            "mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_image mingw-w64-x86_64-glew")
+            "mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_image")
 
       - name: Configure CMake
         run: |-

+ 4 - 4
Backends/CMakeLists.txt

@@ -59,7 +59,7 @@ target_sources(rmlui_backend_SDL_GL2 INTERFACE
 	"${CMAKE_CURRENT_LIST_DIR}/RmlUi_Platform_SDL.h"
 	"${CMAKE_CURRENT_LIST_DIR}/RmlUi_Renderer_GL2.h"
 )
-target_link_libraries(rmlui_backend_SDL_GL2 INTERFACE rmlui_backend_common_headers OpenGL::GL SDL2::SDL2 GLEW::GLEW SDL2_image::SDL2_image)
+target_link_libraries(rmlui_backend_SDL_GL2 INTERFACE rmlui_backend_common_headers OpenGL::GL SDL::SDL SDL_image::SDL_image)
 
 add_library(rmlui_backend_SDL_GL3 INTERFACE)
 target_sources(rmlui_backend_SDL_GL3 INTERFACE
@@ -70,7 +70,7 @@ target_sources(rmlui_backend_SDL_GL3 INTERFACE
 	"${CMAKE_CURRENT_LIST_DIR}/RmlUi_Renderer_GL3.h"
 	"${CMAKE_CURRENT_LIST_DIR}/RmlUi_Include_GL3.h"
 )
-target_link_libraries(rmlui_backend_SDL_GL3 INTERFACE rmlui_backend_common_headers SDL2::SDL2 SDL2_image::SDL2_image)
+target_link_libraries(rmlui_backend_SDL_GL3 INTERFACE rmlui_backend_common_headers SDL::SDL SDL_image::SDL_image)
 if(UNIX)
 	# The OpenGL 3 renderer implementation uses dlopen/dlclose
 	# This is required in some UNIX and UNIX-like operating systems to load shared object files at runtime
@@ -91,7 +91,7 @@ target_sources(rmlui_backend_SDL_VK INTERFACE
 	"${CMAKE_CURRENT_LIST_DIR}/RmlUi_Include_Vulkan.h"
 	"${CMAKE_CURRENT_LIST_DIR}/RmlUi_Vulkan/ShadersCompiledSPV.h"
 )
-target_link_libraries(rmlui_backend_SDL_VK INTERFACE rmlui_backend_common_headers SDL2::SDL2)
+target_link_libraries(rmlui_backend_SDL_VK INTERFACE rmlui_backend_common_headers SDL::SDL)
 if(UNIX)
 	# The Vulkan renderer implementation uses dlopen/dlclose
 	# This is required in some UNIX and UNIX-like operating systems to load shared object files at runtime
@@ -106,7 +106,7 @@ target_sources(rmlui_backend_SDL_SDLrenderer INTERFACE
 	"${CMAKE_CURRENT_LIST_DIR}/RmlUi_Platform_SDL.h"
 	"${CMAKE_CURRENT_LIST_DIR}/RmlUi_Renderer_SDL.h"
 )
-target_link_libraries(rmlui_backend_SDL_SDLrenderer INTERFACE rmlui_backend_common_headers SDL2::SDL2 SDL2_image::SDL2_image)
+target_link_libraries(rmlui_backend_SDL_SDLrenderer INTERFACE rmlui_backend_common_headers SDL::SDL SDL_image::SDL_image)
 
 add_library(rmlui_backend_SFML_GL2 INTERFACE)
 target_sources(rmlui_backend_SFML_GL2 INTERFACE

+ 120 - 126
Backends/RmlUi_Backend_SDL_GL2.cpp

@@ -33,11 +33,14 @@
 #include <RmlUi/Core/Core.h>
 #include <RmlUi/Core/FileInterface.h>
 #include <RmlUi/Core/Log.h>
-#include <GL/glew.h>
-#include <SDL.h>
-#include <SDL_image.h>
 
-#if !(SDL_VIDEO_RENDER_OGL)
+#if SDL_MAJOR_VERSION >= 3
+	#include <SDL3_image/SDL_image.h>
+#else
+	#include <SDL_image.h>
+#endif
+
+#if SDL_MAJOR_VERSION == 2 && !(SDL_VIDEO_RENDER_OGL)
 	#error "Only the OpenGL SDL backend is supported."
 #endif
 
@@ -47,27 +50,7 @@
     Overloads the OpenGL2 render interface to load textures through SDL_image's built-in texture loading functionality.
  */
 class RenderInterface_GL2_SDL : public RenderInterface_GL2 {
-private:
-	SDL_Renderer* renderer;
-
 public:
-	RenderInterface_GL2_SDL(SDL_Renderer* renderer) : renderer(renderer) {}
-
-	void RenderGeometry(Rml::CompiledGeometryHandle handle, Rml::Vector2f translation, Rml::TextureHandle texture) override
-	{
-		SDL_Texture* sdl_texture = (SDL_Texture*)texture;
-		if (sdl_texture)
-		{
-			SDL_GL_BindTexture(sdl_texture, nullptr, nullptr);
-			texture = RenderInterface_GL2::TextureEnableWithoutBinding;
-		}
-
-		RenderInterface_GL2::RenderGeometry(handle, translation, texture);
-
-		if (sdl_texture)
-			SDL_GL_UnbindTexture(sdl_texture);
-	}
-
 	Rml::TextureHandle LoadTexture(Rml::Vector2i& texture_dimensions, const Rml::String& source) override
 	{
 		Rml::FileInterface* file_interface = Rml::GetFileInterface();
@@ -87,19 +70,29 @@ public:
 		const size_t i_ext = source.rfind('.');
 		Rml::String extension = (i_ext == Rml::String::npos ? Rml::String() : source.substr(i_ext + 1));
 
-		SDL_Surface* surface = IMG_LoadTyped_RW(SDL_RWFromMem(buffer.get(), int(buffer_size)), 1, extension.c_str());
+#if SDL_MAJOR_VERSION >= 3
+		auto CreateSurface = [&]() { return IMG_LoadTyped_IO(SDL_IOFromMem(buffer.get(), int(buffer_size)), 1, extension.c_str()); };
+		auto GetSurfaceFormat = [](SDL_Surface* surface) { return surface->format; };
+		auto ConvertSurface = [](SDL_Surface* surface, SDL_PixelFormat format) { return SDL_ConvertSurface(surface, format); };
+		auto DestroySurface = [](SDL_Surface* surface) { SDL_DestroySurface(surface); };
+#else
+		auto CreateSurface = [&]() { return IMG_LoadTyped_RW(SDL_RWFromMem(buffer.get(), int(buffer_size)), 1, extension.c_str()); };
+		auto GetSurfaceFormat = [](SDL_Surface* surface) { return surface->format->format; };
+		auto ConvertSurface = [](SDL_Surface* surface, Uint32 format) { return SDL_ConvertSurfaceFormat(surface, format, 0); };
+		auto DestroySurface = [](SDL_Surface* surface) { SDL_FreeSurface(surface); };
+#endif
+
+		SDL_Surface* surface = CreateSurface();
 		if (!surface)
 			return {};
 
 		texture_dimensions = {surface->w, surface->h};
 
-		if (surface->format->format != SDL_PIXELFORMAT_RGBA32 && surface->format->format != SDL_PIXELFORMAT_BGRA32)
+		if (GetSurfaceFormat(surface) != SDL_PIXELFORMAT_RGBA32)
 		{
-			// Ensure correct format for premultiplied alpha conversion below. Additionally, fix rendering images with
-			// no alpha channel, see https://github.com/mikke89/RmlUi/issues/239
-			SDL_Surface* converted_surface = SDL_ConvertSurfaceFormat(surface, SDL_PixelFormatEnum::SDL_PIXELFORMAT_RGBA32, 0);
-			SDL_FreeSurface(surface);
-
+			// Ensure correct format for premultiplied alpha conversion and GenerateTexture below.
+			SDL_Surface* converted_surface = ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
+			DestroySurface(surface);
 			if (!converted_surface)
 				return {};
 
@@ -107,43 +100,21 @@ public:
 		}
 
 		// Convert colors to premultiplied alpha, which is necessary for correct alpha compositing.
+		const size_t pixels_byte_size = surface->w * surface->h * 4;
 		byte* pixels = static_cast<byte*>(surface->pixels);
-		for (int i = 0; i < surface->w * surface->h * 4; i += 4)
+		for (size_t i = 0; i < pixels_byte_size; i += 4)
 		{
 			const byte alpha = pixels[i + 3];
-			for (int j = 0; j < 3; ++j)
+			for (size_t j = 0; j < 3; ++j)
 				pixels[i + j] = byte(int(pixels[i + j]) * int(alpha) / 255);
 		}
 
-		SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
-		SDL_FreeSurface(surface);
-
-		return (Rml::TextureHandle)texture;
-	}
+		Rml::TextureHandle texture_handle = RenderInterface_GL2::GenerateTexture({pixels, pixels_byte_size}, texture_dimensions);
 
-	Rml::TextureHandle GenerateTexture(Rml::Span<const Rml::byte> source, Rml::Vector2i source_dimensions) override
-	{
-#if SDL_BYTEORDER == SDL_BIG_ENDIAN
-		Uint32 rmask = 0xff000000;
-		Uint32 gmask = 0x00ff0000;
-		Uint32 bmask = 0x0000ff00;
-		Uint32 amask = 0x000000ff;
-#else
-		Uint32 rmask = 0x000000ff;
-		Uint32 gmask = 0x0000ff00;
-		Uint32 bmask = 0x00ff0000;
-		Uint32 amask = 0xff000000;
-#endif
+		DestroySurface(surface);
 
-		SDL_Surface* surface = SDL_CreateRGBSurfaceFrom((void*)source.data(), source_dimensions.x, source_dimensions.y, 32, source_dimensions.x * 4,
-			rmask, gmask, bmask, amask);
-		SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
-		SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
-		SDL_FreeSurface(surface);
-		return (Rml::TextureHandle)texture;
+		return texture_handle;
 	}
-
-	void ReleaseTexture(Rml::TextureHandle texture_handle) override { SDL_DestroyTexture((SDL_Texture*)texture_handle); }
 };
 
 /**
@@ -152,13 +123,10 @@ public:
     Lifetime governed by the calls to Backend::Initialize() and Backend::Shutdown().
  */
 struct BackendData {
-	BackendData(SDL_Renderer* renderer) : render_interface(renderer) {}
-
 	SystemInterface_SDL system_interface;
 	RenderInterface_GL2_SDL render_interface;
 
 	SDL_Window* window = nullptr;
-	SDL_Renderer* renderer = nullptr;
 	SDL_GLContext glcontext = nullptr;
 
 	bool running = true;
@@ -169,8 +137,13 @@ bool Backend::Initialize(const char* window_name, int width, int height, bool al
 {
 	RMLUI_ASSERT(!data);
 
+#if SDL_MAJOR_VERSION >= 3
+	if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS))
+		return false;
+#else
 	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER) != 0)
 		return false;
+#endif
 
 	// Submit click events when focusing the window.
 	SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
@@ -179,21 +152,44 @@ bool Backend::Initialize(const char* window_name, int width, int height, bool al
 	SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
 	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
 
-	// Enable linear filtering and MSAA for better-looking visuals, especially when transforms are applied.
-	SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
+	// Enable MSAA for better-looking visuals, especially when transforms are applied.
 	SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
 	SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 2);
 
-	const Uint32 window_flags = (SDL_WINDOW_OPENGL | (allow_resize ? SDL_WINDOW_RESIZABLE : 0));
+#if SDL_MAJOR_VERSION >= 3
+	auto CreateWindow = [&]() {
+		SDL_PropertiesID props = SDL_CreateProperties();
+		SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, window_name);
+		SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, SDL_WINDOWPOS_CENTERED);
+		SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, SDL_WINDOWPOS_CENTERED);
+		SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, width);
+		SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, height);
+		SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);
+		SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, allow_resize);
+		SDL_Window* window = SDL_CreateWindowWithProperties(props);
+		SDL_DestroyProperties(props);
+		return window;
+	};
+#else
+	auto CreateWindow = [&]() {
+		const Uint32 window_flags = (SDL_WINDOW_OPENGL | (allow_resize ? SDL_WINDOW_RESIZABLE : 0));
+		SDL_Window* window = SDL_CreateWindow(window_name, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, window_flags);
+		// SDL2 implicitly activates text input on window creation. Turn it off for now, it will be activated again e.g. when focusing a text input
+		// field.
+		SDL_StopTextInput();
+		return window;
+	};
+	// Enable linear filtering, SDL 3 already defaults to it.
+	SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
+#endif
 
-	SDL_Window* window = SDL_CreateWindow(window_name, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, window_flags);
+	SDL_Window* window = CreateWindow();
 	if (!window)
 	{
 		// Try again on low-quality settings.
-		SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest");
 		SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 0);
 		SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 0);
-		window = SDL_CreateWindow(window_name, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, window_flags);
+		window = CreateWindow();
 		if (!window)
 		{
 			Rml::Log::Message(Rml::Log::LT_ERROR, "SDL error on create window: %s", SDL_GetError());
@@ -202,34 +198,13 @@ bool Backend::Initialize(const char* window_name, int width, int height, bool al
 	}
 
 	SDL_GLContext glcontext = SDL_GL_CreateContext(window);
-	int opengl_renderer_index = -1;
-	int num_render_drivers = SDL_GetNumRenderDrivers();
-	for (int i = 0; i < num_render_drivers; i++)
-	{
-		SDL_RendererInfo info;
-		if (SDL_GetRenderDriverInfo(i, &info) == 0)
-		{
-			if (strcmp(info.name, "opengl") == 0)
-				opengl_renderer_index = i;
-		}
-	}
+	SDL_GL_MakeCurrent(window, glcontext);
+	SDL_GL_SetSwapInterval(1);
 
-	SDL_Renderer* renderer = SDL_CreateRenderer(window, opengl_renderer_index, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
-	if (!renderer)
-		return false;
-
-	GLenum err = glewInit();
-	if (err != GLEW_OK)
-	{
-		Rml::Log::Message(Rml::Log::LT_ERROR, "GLEW error: %s", glewGetErrorString(err));
-		return false;
-	}
-
-	data = Rml::MakeUnique<BackendData>(renderer);
+	data = Rml::MakeUnique<BackendData>();
 
 	data->window = window;
 	data->glcontext = glcontext;
-	data->renderer = renderer;
 
 	data->system_interface.SetWindow(window);
 	data->render_interface.SetViewport(width, height);
@@ -241,8 +216,12 @@ void Backend::Shutdown()
 {
 	RMLUI_ASSERT(data);
 
-	SDL_DestroyRenderer(data->renderer);
+#if SDL_MAJOR_VERSION >= 3
+	SDL_GL_DestroyContext(data->glcontext);
+#else
 	SDL_GL_DeleteContext(data->glcontext);
+#endif
+
 	SDL_DestroyWindow(data->window);
 
 	data.reset();
@@ -266,27 +245,55 @@ bool Backend::ProcessEvents(Rml::Context* context, KeyDownCallback key_down_call
 {
 	RMLUI_ASSERT(data && context);
 
+#if SDL_MAJOR_VERSION >= 3
+	#define RMLSDL_WINDOW_EVENTS_BEGIN
+	#define RMLSDL_WINDOW_EVENTS_END
+	auto GetKey = [](const SDL_Event& event) { return event.key.key; };
+	constexpr auto event_quit = SDL_EVENT_QUIT;
+	constexpr auto event_key_down = SDL_EVENT_KEY_DOWN;
+	constexpr auto event_window_size_changed = SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED;
+	bool has_event = false;
+#else
+	#define RMLSDL_WINDOW_EVENTS_BEGIN \
+	case SDL_WINDOWEVENT:              \
+	{                                  \
+		switch (ev.window.event)       \
+		{
+	#define RMLSDL_WINDOW_EVENTS_END \
+		}                            \
+		}                            \
+		break;
+	auto GetKey = [](const SDL_Event& event) { return event.key.keysym.sym; };
+	constexpr auto event_quit = SDL_QUIT;
+	constexpr auto event_key_down = SDL_KEYDOWN;
+	constexpr auto event_window_size_changed = SDL_WINDOWEVENT_SIZE_CHANGED;
+	int has_event = 0;
+#endif
+
 	bool result = data->running;
 	data->running = true;
 
 	SDL_Event ev;
-	int has_event = 0;
 	if (power_save)
 		has_event = SDL_WaitEventTimeout(&ev, static_cast<int>(Rml::Math::Min(context->GetNextUpdateDelay(), 10.0) * 1000));
 	else
 		has_event = SDL_PollEvent(&ev);
+
 	while (has_event)
 	{
+		bool propagate_event = true;
 		switch (ev.type)
 		{
-		case SDL_QUIT:
+		case event_quit:
 		{
+			propagate_event = false;
 			result = false;
 		}
 		break;
-		case SDL_KEYDOWN:
+		case event_key_down:
 		{
-			const Rml::Input::KeyIdentifier key = RmlSDL::ConvertKey(ev.key.keysym.sym);
+			propagate_event = false;
+			const Rml::Input::KeyIdentifier key = RmlSDL::ConvertKey(GetKey(ev));
 			const int key_modifier = RmlSDL::GetKeyModifierState();
 			const float native_dp_ratio = 1.f;
 
@@ -301,26 +308,24 @@ bool Backend::ProcessEvents(Rml::Context* context, KeyDownCallback key_down_call
 				break;
 		}
 		break;
-		case SDL_WINDOWEVENT:
-		{
-			switch (ev.window.event)
-			{
-			case SDL_WINDOWEVENT_SIZE_CHANGED:
-			{
-				Rml::Vector2i dimensions(ev.window.data1, ev.window.data2);
-				data->render_interface.SetViewport(dimensions.x, dimensions.y);
-			}
-			break;
-			}
-			RmlSDL::InputEventHandler(context, ev);
-		}
-		break;
-		default:
+
+			RMLSDL_WINDOW_EVENTS_BEGIN
+
+		case event_window_size_changed:
 		{
-			RmlSDL::InputEventHandler(context, ev);
+			Rml::Vector2i dimensions = {ev.window.data1, ev.window.data2};
+			data->render_interface.SetViewport(dimensions.x, dimensions.y);
 		}
 		break;
+
+			RMLSDL_WINDOW_EVENTS_END
+
+		default: break;
 		}
+
+		if (propagate_event)
+			RmlSDL::InputEventHandler(context, ev);
+
 		has_event = SDL_PollEvent(&ev);
 	}
 
@@ -338,13 +343,7 @@ void Backend::BeginFrame()
 {
 	RMLUI_ASSERT(data);
 
-	SDL_SetRenderDrawColor(data->renderer, 0, 0, 0, 0);
-	SDL_RenderClear(data->renderer);
-
-	// SDL uses shaders that we need to disable here.
-	glUseProgramObjectARB(0);
-	glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
-
+	data->render_interface.Clear();
 	data->render_interface.BeginFrame();
 }
 
@@ -353,10 +352,5 @@ void Backend::PresentFrame()
 	RMLUI_ASSERT(data);
 
 	data->render_interface.EndFrame();
-
-	// Draw a fake point just outside the screen to let SDL know that it needs to reset its state in case it wants to render a texture next frame.
-	SDL_SetRenderDrawBlendMode(data->renderer, SDL_BLENDMODE_NONE);
-	SDL_RenderDrawPoint(data->renderer, -1, -1);
-
-	SDL_RenderPresent(data->renderer);
+	SDL_GL_SwapWindow(data->window);
 }

+ 97 - 33
Backends/RmlUi_Backend_SDL_GL3.cpp

@@ -34,15 +34,17 @@
 #include <RmlUi/Core/FileInterface.h>
 #include <RmlUi/Core/Log.h>
 #include <RmlUi/Core/Profiling.h>
-#include <SDL.h>
-#include <SDL_image.h>
+
+#if SDL_MAJOR_VERSION >= 3
+	#include <SDL3_image/SDL_image.h>
+#else
+	#include <SDL_image.h>
+#endif
 
 #if defined RMLUI_PLATFORM_EMSCRIPTEN
 	#include <emscripten.h>
-#else
-	#if !(SDL_VIDEO_RENDER_OGL)
-		#error "Only the OpenGL SDL backend is supported."
-	#endif
+#elif SDL_MAJOR_VERSION == 2 && !(SDL_VIDEO_RENDER_OGL)
+	#error "Only the OpenGL SDL backend is supported."
 #endif
 
 /**
@@ -73,18 +75,29 @@ public:
 		const size_t i_ext = source.rfind('.');
 		Rml::String extension = (i_ext == Rml::String::npos ? Rml::String() : source.substr(i_ext + 1));
 
-		SDL_Surface* surface = IMG_LoadTyped_RW(SDL_RWFromMem(buffer.get(), int(buffer_size)), 1, extension.c_str());
+#if SDL_MAJOR_VERSION >= 3
+		auto CreateSurface = [&]() { return IMG_LoadTyped_IO(SDL_IOFromMem(buffer.get(), int(buffer_size)), 1, extension.c_str()); };
+		auto GetSurfaceFormat = [](SDL_Surface* surface) { return surface->format; };
+		auto ConvertSurface = [](SDL_Surface* surface, SDL_PixelFormat format) { return SDL_ConvertSurface(surface, format); };
+		auto DestroySurface = [](SDL_Surface* surface) { SDL_DestroySurface(surface); };
+#else
+		auto CreateSurface = [&]() { return IMG_LoadTyped_RW(SDL_RWFromMem(buffer.get(), int(buffer_size)), 1, extension.c_str()); };
+		auto GetSurfaceFormat = [](SDL_Surface* surface) { return surface->format->format; };
+		auto ConvertSurface = [](SDL_Surface* surface, Uint32 format) { return SDL_ConvertSurfaceFormat(surface, format, 0); };
+		auto DestroySurface = [](SDL_Surface* surface) { SDL_FreeSurface(surface); };
+#endif
+
+		SDL_Surface* surface = CreateSurface();
 		if (!surface)
 			return {};
 
 		texture_dimensions = {surface->w, surface->h};
 
-		if (surface->format->format != SDL_PIXELFORMAT_RGBA32)
+		if (GetSurfaceFormat(surface) != SDL_PIXELFORMAT_RGBA32)
 		{
 			// Ensure correct format for premultiplied alpha conversion and GenerateTexture below.
-			SDL_Surface* converted_surface = SDL_ConvertSurfaceFormat(surface, SDL_PixelFormatEnum::SDL_PIXELFORMAT_RGBA32, 0);
-			SDL_FreeSurface(surface);
-
+			SDL_Surface* converted_surface = ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
+			DestroySurface(surface);
 			if (!converted_surface)
 				return {};
 
@@ -103,7 +116,7 @@ public:
 
 		Rml::TextureHandle texture_handle = RenderInterface_GL3::GenerateTexture({pixels, pixels_byte_size}, texture_dimensions);
 
-		SDL_FreeSurface(surface);
+		DestroySurface(surface);
 
 		return texture_handle;
 	}
@@ -129,8 +142,13 @@ bool Backend::Initialize(const char* window_name, int width, int height, bool al
 {
 	RMLUI_ASSERT(!data);
 
+#if SDL_MAJOR_VERSION >= 3
+	if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS))
+		return false;
+#else
 	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER) != 0)
 		return false;
+#endif
 
 	// Submit click events when focusing the window.
 	SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
@@ -151,9 +169,24 @@ bool Backend::Initialize(const char* window_name, int width, int height, bool al
 
 	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
 
+#if SDL_MAJOR_VERSION >= 3
+	SDL_PropertiesID props = SDL_CreateProperties();
+	SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, window_name);
+	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, SDL_WINDOWPOS_CENTERED);
+	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, SDL_WINDOWPOS_CENTERED);
+	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, width);
+	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, height);
+	SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);
+	SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, allow_resize);
+	SDL_Window* window = SDL_CreateWindowWithProperties(props);
+	SDL_DestroyProperties(props);
+#else
 	const Uint32 window_flags = (SDL_WINDOW_OPENGL | (allow_resize ? SDL_WINDOW_RESIZABLE : 0));
-
 	SDL_Window* window = SDL_CreateWindow(window_name, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, window_flags);
+	// SDL2 implicitly activates text input on window creation. Turn it off for now, it will be activated again e.g. when focusing a text input field.
+	SDL_StopTextInput();
+#endif
+
 	if (!window)
 	{
 		Rml::Log::Message(Rml::Log::LT_ERROR, "SDL error on create window: %s", SDL_GetError());
@@ -192,7 +225,12 @@ void Backend::Shutdown()
 {
 	RMLUI_ASSERT(data);
 
+#if SDL_MAJOR_VERSION >= 3
+	SDL_GL_DestroyContext(data->glcontext);
+#else
 	SDL_GL_DeleteContext(data->glcontext);
+#endif
+
 	SDL_DestroyWindow(data->window);
 
 	data.reset();
@@ -230,27 +268,55 @@ bool Backend::ProcessEvents(Rml::Context* context, KeyDownCallback key_down_call
 
 #endif
 
+#if SDL_MAJOR_VERSION >= 3
+	#define RMLSDL_WINDOW_EVENTS_BEGIN
+	#define RMLSDL_WINDOW_EVENTS_END
+	auto GetKey = [](const SDL_Event& event) { return event.key.key; };
+	constexpr auto event_quit = SDL_EVENT_QUIT;
+	constexpr auto event_key_down = SDL_EVENT_KEY_DOWN;
+	constexpr auto event_window_size_changed = SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED;
+	bool has_event = false;
+#else
+	#define RMLSDL_WINDOW_EVENTS_BEGIN \
+	case SDL_WINDOWEVENT:              \
+	{                                  \
+		switch (ev.window.event)       \
+		{
+	#define RMLSDL_WINDOW_EVENTS_END \
+		}                            \
+		}                            \
+		break;
+	auto GetKey = [](const SDL_Event& event) { return event.key.keysym.sym; };
+	constexpr auto event_quit = SDL_QUIT;
+	constexpr auto event_key_down = SDL_KEYDOWN;
+	constexpr auto event_window_size_changed = SDL_WINDOWEVENT_SIZE_CHANGED;
+	int has_event = 0;
+#endif
+
 	bool result = data->running;
 	data->running = true;
 
 	SDL_Event ev;
-	int has_event = 0;
 	if (power_save)
 		has_event = SDL_WaitEventTimeout(&ev, static_cast<int>(Rml::Math::Min(context->GetNextUpdateDelay(), 10.0) * 1000));
 	else
 		has_event = SDL_PollEvent(&ev);
+
 	while (has_event)
 	{
+		bool propagate_event = true;
 		switch (ev.type)
 		{
-		case SDL_QUIT:
+		case event_quit:
 		{
+			propagate_event = false;
 			result = false;
 		}
 		break;
-		case SDL_KEYDOWN:
+		case event_key_down:
 		{
-			const Rml::Input::KeyIdentifier key = RmlSDL::ConvertKey(ev.key.keysym.sym);
+			propagate_event = false;
+			const Rml::Input::KeyIdentifier key = RmlSDL::ConvertKey(GetKey(ev));
 			const int key_modifier = RmlSDL::GetKeyModifierState();
 			const float native_dp_ratio = 1.f;
 
@@ -265,26 +331,24 @@ bool Backend::ProcessEvents(Rml::Context* context, KeyDownCallback key_down_call
 				break;
 		}
 		break;
-		case SDL_WINDOWEVENT:
-		{
-			switch (ev.window.event)
-			{
-			case SDL_WINDOWEVENT_SIZE_CHANGED:
-			{
-				Rml::Vector2i dimensions(ev.window.data1, ev.window.data2);
-				data->render_interface.SetViewport(dimensions.x, dimensions.y);
-			}
-			break;
-			}
-			RmlSDL::InputEventHandler(context, ev);
-		}
-		break;
-		default:
+
+			RMLSDL_WINDOW_EVENTS_BEGIN
+
+		case event_window_size_changed:
 		{
-			RmlSDL::InputEventHandler(context, ev);
+			Rml::Vector2i dimensions = {ev.window.data1, ev.window.data2};
+			data->render_interface.SetViewport(dimensions.x, dimensions.y);
 		}
 		break;
+
+			RMLSDL_WINDOW_EVENTS_END
+
+		default: break;
 		}
+
+		if (propagate_event)
+			RmlSDL::InputEventHandler(context, ev);
+
 		has_event = SDL_PollEvent(&ev);
 	}
 

+ 79 - 31
Backends/RmlUi_Backend_SDL_SDLrenderer.cpp

@@ -32,7 +32,6 @@
 #include <RmlUi/Core/Context.h>
 #include <RmlUi/Core/Core.h>
 #include <RmlUi/Core/Log.h>
-#include <SDL.h>
 
 /**
     Global data used by this backend.
@@ -56,45 +55,84 @@ bool Backend::Initialize(const char* window_name, int width, int height, bool al
 {
 	RMLUI_ASSERT(!data);
 
+#if SDL_MAJOR_VERSION >= 3
+	if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS))
+		return false;
+#else
 	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER) != 0)
 		return false;
+#endif
 
 	// Submit click events when focusing the window.
 	SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
 
+#if SDL_MAJOR_VERSION >= 3
+	SDL_PropertiesID props = SDL_CreateProperties();
+	SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, window_name);
+	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, SDL_WINDOWPOS_CENTERED);
+	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, SDL_WINDOWPOS_CENTERED);
+	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, width);
+	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, height);
+	SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, allow_resize);
+	SDL_Window* window = SDL_CreateWindowWithProperties(props);
+	SDL_DestroyProperties(props);
+#else
 	const Uint32 window_flags = (allow_resize ? SDL_WINDOW_RESIZABLE : 0);
 	SDL_Window* window = SDL_CreateWindow(window_name, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, window_flags);
+	// SDL2 implicitly activates text input on window creation. Turn it off for now, it will be activated again e.g. when focusing a text input field.
+	SDL_StopTextInput();
+#endif
+
 	if (!window)
 	{
 		Rml::Log::Message(Rml::Log::LT_ERROR, "SDL error on create window: %s\n", SDL_GetError());
 		return false;
 	}
 
-	/*
-	 * Force a specific back-end
-	SDL_SetHint(SDL_HINT_RENDER_BATCHING, "1");
-	SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
-	SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengles2");
-	SDL_SetHint(SDL_HINT_RENDER_DRIVER, "direct3d");
-	SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl");
-	*/
-
+	// Force a specific SDL renderer
+	// SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
+	// SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengles2");
+	// SDL_SetHint(SDL_HINT_RENDER_DRIVER, "direct3d");
+	// SDL_SetHint(SDL_HINT_RENDER_DRIVER, "direct3d12");
+	// SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl");
+	// SDL_SetHint(SDL_HINT_RENDER_DRIVER, "vulkan");
+
+#if SDL_MAJOR_VERSION >= 3
+	SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
+#else
 	SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
+#endif
+
 	if (!renderer)
 		return false;
 
 	data = Rml::MakeUnique<BackendData>(renderer);
-
 	data->window = window;
 	data->renderer = renderer;
-
 	data->system_interface.SetWindow(window);
 
-	SDL_RendererInfo renderer_info;
-	if (SDL_GetRendererInfo(renderer, &renderer_info) == 0)
+	const char* renderer_name = nullptr;
+	Rml::String available_renderers;
+
+#if SDL_MAJOR_VERSION >= 3
+	SDL_SetRenderVSync(renderer, 1);
+	renderer_name = SDL_GetRendererName(renderer);
+	for (int i = 0; i < SDL_GetNumRenderDrivers(); i++)
 	{
-		data->system_interface.LogMessage(Rml::Log::LT_INFO, Rml::CreateString("Using SDL renderer: %s\n", renderer_info.name));
+		if (i > 0)
+			available_renderers += ", ";
+		available_renderers += SDL_GetRenderDriver(i);
 	}
+#else
+	SDL_RendererInfo renderer_info;
+	if (SDL_GetRendererInfo(renderer, &renderer_info) == 0)
+		renderer_name = renderer_info.name;
+#endif
+
+	if (!available_renderers.empty())
+		data->system_interface.LogMessage(Rml::Log::LT_INFO, Rml::CreateString("Available SDL renderers: %s", available_renderers.c_str()));
+	if (renderer_name)
+		data->system_interface.LogMessage(Rml::Log::LT_INFO, Rml::CreateString("Using SDL renderer: %s", renderer_name));
 
 	return true;
 }
@@ -127,26 +165,42 @@ bool Backend::ProcessEvents(Rml::Context* context, KeyDownCallback key_down_call
 {
 	RMLUI_ASSERT(data && context);
 
+#if SDL_MAJOR_VERSION >= 3
+	auto GetKey = [](const SDL_Event& event) { return event.key.key; };
+	constexpr auto event_quit = SDL_EVENT_QUIT;
+	constexpr auto event_key_down = SDL_EVENT_KEY_DOWN;
+	bool has_event = false;
+#else
+	auto GetKey = [](const SDL_Event& event) { return event.key.keysym.sym; };
+	constexpr auto event_quit = SDL_QUIT;
+	constexpr auto event_key_down = SDL_KEYDOWN;
+	int has_event = 0;
+#endif
+
 	bool result = data->running;
-	SDL_Event ev;
+	data->running = true;
 
-	int has_event = 0;
+	SDL_Event ev;
 	if (power_save)
 		has_event = SDL_WaitEventTimeout(&ev, static_cast<int>(Rml::Math::Min(context->GetNextUpdateDelay(), 10.0) * 1000));
 	else
 		has_event = SDL_PollEvent(&ev);
+
 	while (has_event)
 	{
+		bool propagate_event = true;
 		switch (ev.type)
 		{
-		case SDL_QUIT:
+		case event_quit:
 		{
+			propagate_event = false;
 			result = false;
 		}
 		break;
-		case SDL_KEYDOWN:
+		case event_key_down:
 		{
-			const Rml::Input::KeyIdentifier key = RmlSDL::ConvertKey(ev.key.keysym.sym);
+			propagate_event = false;
+			const Rml::Input::KeyIdentifier key = RmlSDL::ConvertKey(GetKey(ev));
 			const int key_modifier = RmlSDL::GetKeyModifierState();
 			const float native_dp_ratio = 1.f;
 
@@ -161,12 +215,12 @@ bool Backend::ProcessEvents(Rml::Context* context, KeyDownCallback key_down_call
 				break;
 		}
 		break;
-		default:
-		{
-			RmlSDL::InputEventHandler(context, ev);
-		}
-		break;
+		default: break;
 		}
+
+		if (propagate_event)
+			RmlSDL::InputEventHandler(context, ev);
+
 		has_event = SDL_PollEvent(&ev);
 	}
 
@@ -176,24 +230,18 @@ bool Backend::ProcessEvents(Rml::Context* context, KeyDownCallback key_down_call
 void Backend::RequestExit()
 {
 	RMLUI_ASSERT(data);
-
 	data->running = false;
 }
 
 void Backend::BeginFrame()
 {
 	RMLUI_ASSERT(data);
-
-	SDL_SetRenderDrawColor(data->renderer, 0, 0, 0, 0);
-	SDL_RenderClear(data->renderer);
-
 	data->render_interface.BeginFrame();
 }
 
 void Backend::PresentFrame()
 {
 	RMLUI_ASSERT(data);
-
 	data->render_interface.EndFrame();
 	SDL_RenderPresent(data->renderer);
 }

+ 104 - 25
Backends/RmlUi_Backend_SDL_VK.cpp

@@ -33,13 +33,17 @@
 #include <RmlUi/Core/Core.h>
 #include <RmlUi/Core/FileInterface.h>
 #include <RmlUi/Core/Log.h>
-#include <SDL2/SDL.h>
-#include <SDL2/SDL_vulkan.h>
 
-#if !SDL_VIDEO_VULKAN
+#if SDL_MAJOR_VERSION == 2 && !(SDL_VIDEO_VULKAN)
 	#error "Only the Vulkan SDL backend is supported."
 #endif
 
+#if SDL_MAJOR_VERSION >= 3
+	#include <SDL3/SDL_vulkan.h>
+#else
+	#include <SDL2/SDL_vulkan.h>
+#endif
+
 /**
     Global data used by this backend.
 
@@ -59,15 +63,41 @@ bool Backend::Initialize(const char* window_name, int width, int height, bool al
 {
 	RMLUI_ASSERT(!data);
 
+#if SDL_MAJOR_VERSION >= 3
+	if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS))
+		return false;
+#else
 	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER) != 0)
 		return false;
+#endif
 
 	// Submit click events when focusing the window.
 	SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
 
+#if SDL_MAJOR_VERSION >= 3
+	SDL_PropertiesID props = SDL_CreateProperties();
+	SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, window_name);
+	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, SDL_WINDOWPOS_CENTERED);
+	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, SDL_WINDOWPOS_CENTERED);
+	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, width);
+	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, height);
+	SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_VULKAN_BOOLEAN, true);
+	SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, allow_resize);
+	SDL_Window* window = SDL_CreateWindowWithProperties(props);
+	SDL_DestroyProperties(props);
+	auto CreateSurface = [](VkInstance instance, VkSurfaceKHR* out_surface) {
+		return SDL_Vulkan_CreateSurface(data->window, instance, nullptr, out_surface);
+	};
+#else
 	const Uint32 window_flags = (SDL_WINDOW_VULKAN | (allow_resize ? SDL_WINDOW_RESIZABLE : 0));
-
 	SDL_Window* window = SDL_CreateWindow(window_name, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, window_flags);
+	// SDL2 implicitly activates text input on window creation. Turn it off for now, it will be activated again e.g. when focusing a text input field.
+	SDL_StopTextInput();
+	auto CreateSurface = [](VkInstance instance, VkSurfaceKHR* out_surface) {
+		return (bool)SDL_Vulkan_CreateSurface(data->window, instance, out_surface);
+	};
+#endif
+
 	if (!window)
 	{
 		Rml::Log::Message(Rml::Log::LT_ERROR, "SDL error on create window: %s", SDL_GetError());
@@ -80,6 +110,19 @@ bool Backend::Initialize(const char* window_name, int width, int height, bool al
 	Rml::Vector<const char*> extensions;
 	{
 		unsigned int count;
+#if SDL_MAJOR_VERSION >= 3
+		const char* const* extensions_list = SDL_Vulkan_GetInstanceExtensions(&count);
+		if (!extensions_list)
+		{
+			data.reset();
+			Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to get required vulkan extensions");
+			return false;
+		}
+		extensions.resize(count);
+		for (unsigned int i = 0; i < count; i++)
+			extensions[i] = extensions_list[i];
+#else
+
 		if (!SDL_Vulkan_GetInstanceExtensions(window, &count, nullptr))
 		{
 			data.reset();
@@ -93,10 +136,10 @@ bool Backend::Initialize(const char* window_name, int width, int height, bool al
 			Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to get required vulkan extensions");
 			return false;
 		}
+#endif
 	}
 
-	if (!data->render_interface.Initialize(std::move(extensions),
-			[](VkInstance instance, VkSurfaceKHR* out_surface) { return (bool)SDL_Vulkan_CreateSurface(data->window, instance, out_surface); }))
+	if (!data->render_interface.Initialize(std::move(extensions), CreateSurface))
 	{
 		data.reset();
 		Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to initialize Vulkan render interface");
@@ -136,6 +179,12 @@ Rml::RenderInterface* Backend::GetRenderInterface()
 
 static bool WaitForValidSwapchain()
 {
+#if SDL_MAJOR_VERSION >= 3
+	constexpr auto event_quit = SDL_EVENT_QUIT;
+#else
+	constexpr auto event_quit = SDL_QUIT;
+#endif
+
 	bool result = true;
 
 	// In some situations the swapchain may become invalid, such as when the window is minimized. In this state the renderer cannot accept any render
@@ -146,7 +195,7 @@ static bool WaitForValidSwapchain()
 		SDL_Event ev;
 		while (SDL_PollEvent(&ev))
 		{
-			if (ev.type == SDL_QUIT)
+			if (ev.type == event_quit)
 			{
 				// Restore the window so that we can recreate the swapchain, and then properly release all resource and shutdown cleanly.
 				SDL_RestoreWindow(data->window);
@@ -164,26 +213,55 @@ bool Backend::ProcessEvents(Rml::Context* context, KeyDownCallback key_down_call
 {
 	RMLUI_ASSERT(data && context);
 
+#if SDL_MAJOR_VERSION >= 3
+	#define RMLSDL_WINDOW_EVENTS_BEGIN
+	#define RMLSDL_WINDOW_EVENTS_END
+	auto GetKey = [](const SDL_Event& event) { return event.key.key; };
+	constexpr auto event_quit = SDL_EVENT_QUIT;
+	constexpr auto event_key_down = SDL_EVENT_KEY_DOWN;
+	constexpr auto event_window_size_changed = SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED;
+	bool has_event = false;
+#else
+	#define RMLSDL_WINDOW_EVENTS_BEGIN \
+	case SDL_WINDOWEVENT:              \
+	{                                  \
+		switch (ev.window.event)       \
+		{
+	#define RMLSDL_WINDOW_EVENTS_END \
+		}                            \
+		}                            \
+		break;
+	auto GetKey = [](const SDL_Event& event) { return event.key.keysym.sym; };
+	constexpr auto event_quit = SDL_QUIT;
+	constexpr auto event_key_down = SDL_KEYDOWN;
+	constexpr auto event_window_size_changed = SDL_WINDOWEVENT_SIZE_CHANGED;
+	int has_event = 0;
+#endif
+
 	bool result = data->running;
-	SDL_Event ev;
+	data->running = true;
 
-	int has_event = 0;
+	SDL_Event ev;
 	if (power_save)
 		has_event = SDL_WaitEventTimeout(&ev, static_cast<int>(Rml::Math::Min(context->GetNextUpdateDelay(), 10.0) * 1000));
 	else
 		has_event = SDL_PollEvent(&ev);
+
 	while (has_event)
 	{
+		bool propagate_event = true;
 		switch (ev.type)
 		{
-		case SDL_QUIT:
+		case event_quit:
 		{
+			propagate_event = false;
 			result = false;
 		}
 		break;
-		case SDL_KEYDOWN:
+		case event_key_down:
 		{
-			const Rml::Input::KeyIdentifier key = RmlSDL::ConvertKey(ev.key.keysym.sym);
+			propagate_event = false;
+			const Rml::Input::KeyIdentifier key = RmlSDL::ConvertKey(GetKey(ev));
 			const int key_modifier = RmlSDL::GetKeyModifierState();
 			const float native_dp_ratio = 1.f;
 
@@ -198,23 +276,24 @@ bool Backend::ProcessEvents(Rml::Context* context, KeyDownCallback key_down_call
 				break;
 		}
 		break;
-		case SDL_WINDOWEVENT:
-		{
-			if (ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
-			{
-				Rml::Vector2i dimensions(ev.window.data1, ev.window.data2);
-				context->SetDimensions(dimensions);
-				data->render_interface.SetViewport(dimensions.x, dimensions.y);
-				break;
-			}
-		}
-		break;
-		default:
+
+			RMLSDL_WINDOW_EVENTS_BEGIN
+
+		case event_window_size_changed:
 		{
-			RmlSDL::InputEventHandler(context, ev);
+			Rml::Vector2i dimensions = {ev.window.data1, ev.window.data2};
+			data->render_interface.SetViewport(dimensions.x, dimensions.y);
 		}
 		break;
+
+			RMLSDL_WINDOW_EVENTS_END
+
+		default: break;
 		}
+
+		if (propagate_event)
+			RmlSDL::InputEventHandler(context, ev);
+
 		has_event = SDL_PollEvent(&ev);
 	}
 

+ 249 - 68
Backends/RmlUi_Platform_SDL.cpp

@@ -31,10 +31,18 @@
 #include <RmlUi/Core/Input.h>
 #include <RmlUi/Core/StringUtilities.h>
 #include <RmlUi/Core/SystemInterface.h>
-#include <SDL.h>
 
 SystemInterface_SDL::SystemInterface_SDL()
 {
+#if SDL_MAJOR_VERSION >= 3
+	cursor_default = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT);
+	cursor_move = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_MOVE);
+	cursor_pointer = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_POINTER);
+	cursor_resize = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NWSE_RESIZE);
+	cursor_cross = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_CROSSHAIR);
+	cursor_text = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_TEXT);
+	cursor_unavailable = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NOT_ALLOWED);
+#else
 	cursor_default = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW);
 	cursor_move = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZEALL);
 	cursor_pointer = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND);
@@ -42,17 +50,24 @@ SystemInterface_SDL::SystemInterface_SDL()
 	cursor_cross = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_CROSSHAIR);
 	cursor_text = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM);
 	cursor_unavailable = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NO);
+#endif
 }
 
 SystemInterface_SDL::~SystemInterface_SDL()
 {
-	SDL_FreeCursor(cursor_default);
-	SDL_FreeCursor(cursor_move);
-	SDL_FreeCursor(cursor_pointer);
-	SDL_FreeCursor(cursor_resize);
-	SDL_FreeCursor(cursor_cross);
-	SDL_FreeCursor(cursor_text);
-	SDL_FreeCursor(cursor_unavailable);
+#if SDL_MAJOR_VERSION >= 3
+	auto DestroyCursor = [](SDL_Cursor* cursor) { SDL_DestroyCursor(cursor); };
+#else
+	auto DestroyCursor = [](SDL_Cursor* cursor) { SDL_FreeCursor(cursor); };
+#endif
+
+	DestroyCursor(cursor_default);
+	DestroyCursor(cursor_move);
+	DestroyCursor(cursor_pointer);
+	DestroyCursor(cursor_resize);
+	DestroyCursor(cursor_cross);
+	DestroyCursor(cursor_text);
+	DestroyCursor(cursor_unavailable);
 }
 
 void SystemInterface_SDL::SetWindow(SDL_Window* in_window)
@@ -92,9 +107,9 @@ void SystemInterface_SDL::SetMouseCursor(const Rml::String& cursor_name)
 		SDL_SetCursor(cursor);
 }
 
-void SystemInterface_SDL::SetClipboardText(const Rml::String& text_utf8)
+void SystemInterface_SDL::SetClipboardText(const Rml::String& text)
 {
-	SDL_SetClipboardText(text_utf8.c_str());
+	SDL_SetClipboardText(text.c_str());
 }
 
 void SystemInterface_SDL::GetClipboardText(Rml::String& text)
@@ -104,43 +119,135 @@ void SystemInterface_SDL::GetClipboardText(Rml::String& text)
 	SDL_free(raw_text);
 }
 
+void SystemInterface_SDL::ActivateKeyboard(Rml::Vector2f caret_position, float line_height)
+{
+	if (window)
+	{
+#if SDL_MAJOR_VERSION >= 3
+		const SDL_Rect rect = {int(caret_position.x), int(caret_position.y), 1, int(line_height)};
+		SDL_SetTextInputArea(window, &rect, 0);
+		SDL_StartTextInput(window);
+#else
+		(void)caret_position;
+		(void)line_height;
+		SDL_StartTextInput();
+#endif
+	}
+}
+
+void SystemInterface_SDL::DeactivateKeyboard()
+{
+	if (window)
+	{
+#if SDL_MAJOR_VERSION >= 3
+		SDL_StopTextInput(window);
+#else
+		SDL_StopTextInput();
+#endif
+	}
+}
+
 bool RmlSDL::InputEventHandler(Rml::Context* context, SDL_Event& ev)
 {
+#if SDL_MAJOR_VERSION >= 3
+	#define RMLSDL_WINDOW_EVENTS_BEGIN
+	#define RMLSDL_WINDOW_EVENTS_END
+	auto GetKey = [](const SDL_Event& event) { return event.key.key; };
+	constexpr auto event_mouse_motion = SDL_EVENT_MOUSE_MOTION;
+	constexpr auto event_mouse_down = SDL_EVENT_MOUSE_BUTTON_DOWN;
+	constexpr auto event_mouse_up = SDL_EVENT_MOUSE_BUTTON_UP;
+	constexpr auto event_mouse_wheel = SDL_EVENT_MOUSE_WHEEL;
+	constexpr auto event_key_down = SDL_EVENT_KEY_DOWN;
+	constexpr auto event_key_up = SDL_EVENT_KEY_UP;
+	constexpr auto event_text_input = SDL_EVENT_TEXT_INPUT;
+	constexpr auto event_window_size_changed = SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED;
+	constexpr auto event_window_leave = SDL_EVENT_WINDOW_MOUSE_LEAVE;
+	constexpr auto rmlsdl_true = true;
+	constexpr auto rmlsdl_false = false;
+#else
+	#define RMLSDL_WINDOW_EVENTS_BEGIN \
+	case SDL_WINDOWEVENT:              \
+	{                                  \
+		switch (ev.window.event)       \
+		{
+	#define RMLSDL_WINDOW_EVENTS_END \
+		}                            \
+		}                            \
+		break;
+	auto GetKey = [](const SDL_Event& event) { return event.key.keysym.sym; };
+	constexpr auto event_mouse_motion = SDL_MOUSEMOTION;
+	constexpr auto event_mouse_down = SDL_MOUSEBUTTONDOWN;
+	constexpr auto event_mouse_up = SDL_MOUSEBUTTONUP;
+	constexpr auto event_mouse_wheel = SDL_MOUSEWHEEL;
+	constexpr auto event_key_down = SDL_KEYDOWN;
+	constexpr auto event_key_up = SDL_KEYUP;
+	constexpr auto event_text_input = SDL_TEXTINPUT;
+	constexpr auto event_window_size_changed = SDL_WINDOWEVENT_SIZE_CHANGED;
+	constexpr auto event_window_leave = SDL_WINDOWEVENT_LEAVE;
+	constexpr auto rmlsdl_true = SDL_TRUE;
+	constexpr auto rmlsdl_false = SDL_FALSE;
+#endif
+
 	bool result = true;
 
 	switch (ev.type)
 	{
-	case SDL_MOUSEMOTION: result = context->ProcessMouseMove(ev.motion.x, ev.motion.y, GetKeyModifierState()); break;
-	case SDL_MOUSEBUTTONDOWN:
+	case event_mouse_motion:
+	{
+		result = context->ProcessMouseMove(int(ev.motion.x), int(ev.motion.y), GetKeyModifierState());
+	}
+	break;
+	case event_mouse_down:
+	{
 		result = context->ProcessMouseButtonDown(ConvertMouseButton(ev.button.button), GetKeyModifierState());
-		SDL_CaptureMouse(SDL_TRUE);
-		break;
-	case SDL_MOUSEBUTTONUP:
-		SDL_CaptureMouse(SDL_FALSE);
+		SDL_CaptureMouse(rmlsdl_true);
+	}
+	break;
+	case event_mouse_up:
+	{
+		SDL_CaptureMouse(rmlsdl_false);
 		result = context->ProcessMouseButtonUp(ConvertMouseButton(ev.button.button), GetKeyModifierState());
-		break;
-	case SDL_MOUSEWHEEL: result = context->ProcessMouseWheel(float(-ev.wheel.y), GetKeyModifierState()); break;
-	case SDL_KEYDOWN:
-		result = context->ProcessKeyDown(ConvertKey(ev.key.keysym.sym), GetKeyModifierState());
-		if (ev.key.keysym.sym == SDLK_RETURN || ev.key.keysym.sym == SDLK_KP_ENTER)
+	}
+	break;
+	case event_mouse_wheel:
+	{
+		result = context->ProcessMouseWheel(float(-ev.wheel.y), GetKeyModifierState());
+	}
+	break;
+	case event_key_down:
+	{
+		result = context->ProcessKeyDown(ConvertKey(GetKey(ev)), GetKeyModifierState());
+		if (GetKey(ev) == SDLK_RETURN || GetKey(ev) == SDLK_KP_ENTER)
 			result &= context->ProcessTextInput('\n');
-		break;
-	case SDL_KEYUP: result = context->ProcessKeyUp(ConvertKey(ev.key.keysym.sym), GetKeyModifierState()); break;
-	case SDL_TEXTINPUT: result = context->ProcessTextInput(Rml::String(&ev.text.text[0])); break;
-	case SDL_WINDOWEVENT:
+	}
+	break;
+	case event_key_up:
 	{
-		switch (ev.window.event)
-		{
-		case SDL_WINDOWEVENT_SIZE_CHANGED:
-		{
-			Rml::Vector2i dimensions(ev.window.data1, ev.window.data2);
-			context->SetDimensions(dimensions);
-		}
-		break;
-		case SDL_WINDOWEVENT_LEAVE: context->ProcessMouseLeave(); break;
-		}
+		result = context->ProcessKeyUp(ConvertKey(GetKey(ev)), GetKeyModifierState());
+	}
+	break;
+	case event_text_input:
+	{
+		result = context->ProcessTextInput(Rml::String(&ev.text.text[0]));
 	}
 	break;
+
+		RMLSDL_WINDOW_EVENTS_BEGIN
+
+	case event_window_size_changed:
+	{
+		Rml::Vector2i dimensions(ev.window.data1, ev.window.data2);
+		context->SetDimensions(dimensions);
+	}
+	break;
+	case event_window_leave:
+	{
+		context->ProcessMouseLeave();
+	}
+	break;
+
+		RMLSDL_WINDOW_EVENTS_END
+
 	default: break;
 	}
 
@@ -149,6 +256,66 @@ bool RmlSDL::InputEventHandler(Rml::Context* context, SDL_Event& ev)
 
 Rml::Input::KeyIdentifier RmlSDL::ConvertKey(int sdlkey)
 {
+#if SDL_MAJOR_VERSION >= 3
+	constexpr auto key_a = SDLK_A;
+	constexpr auto key_b = SDLK_B;
+	constexpr auto key_c = SDLK_C;
+	constexpr auto key_d = SDLK_D;
+	constexpr auto key_e = SDLK_E;
+	constexpr auto key_f = SDLK_F;
+	constexpr auto key_g = SDLK_G;
+	constexpr auto key_h = SDLK_H;
+	constexpr auto key_i = SDLK_I;
+	constexpr auto key_j = SDLK_J;
+	constexpr auto key_k = SDLK_K;
+	constexpr auto key_l = SDLK_L;
+	constexpr auto key_m = SDLK_M;
+	constexpr auto key_n = SDLK_N;
+	constexpr auto key_o = SDLK_O;
+	constexpr auto key_p = SDLK_P;
+	constexpr auto key_q = SDLK_Q;
+	constexpr auto key_r = SDLK_R;
+	constexpr auto key_s = SDLK_S;
+	constexpr auto key_t = SDLK_T;
+	constexpr auto key_u = SDLK_U;
+	constexpr auto key_v = SDLK_V;
+	constexpr auto key_w = SDLK_W;
+	constexpr auto key_x = SDLK_X;
+	constexpr auto key_y = SDLK_Y;
+	constexpr auto key_z = SDLK_Z;
+	constexpr auto key_grave = SDLK_GRAVE;
+	constexpr auto key_dblapostrophe = SDLK_DBLAPOSTROPHE;
+#else
+	constexpr auto key_a = SDLK_a;
+	constexpr auto key_b = SDLK_b;
+	constexpr auto key_c = SDLK_c;
+	constexpr auto key_d = SDLK_d;
+	constexpr auto key_e = SDLK_e;
+	constexpr auto key_f = SDLK_f;
+	constexpr auto key_g = SDLK_g;
+	constexpr auto key_h = SDLK_h;
+	constexpr auto key_i = SDLK_i;
+	constexpr auto key_j = SDLK_j;
+	constexpr auto key_k = SDLK_k;
+	constexpr auto key_l = SDLK_l;
+	constexpr auto key_m = SDLK_m;
+	constexpr auto key_n = SDLK_n;
+	constexpr auto key_o = SDLK_o;
+	constexpr auto key_p = SDLK_p;
+	constexpr auto key_q = SDLK_q;
+	constexpr auto key_r = SDLK_r;
+	constexpr auto key_s = SDLK_s;
+	constexpr auto key_t = SDLK_t;
+	constexpr auto key_u = SDLK_u;
+	constexpr auto key_v = SDLK_v;
+	constexpr auto key_w = SDLK_w;
+	constexpr auto key_x = SDLK_x;
+	constexpr auto key_y = SDLK_y;
+	constexpr auto key_z = SDLK_z;
+	constexpr auto key_grave = SDLK_BACKQUOTE;
+	constexpr auto key_dblapostrophe = SDLK_QUOTEDBL;
+#endif
+
 	// clang-format off
 	switch (sdlkey)
 	{
@@ -165,43 +332,43 @@ Rml::Input::KeyIdentifier RmlSDL::ConvertKey(int sdlkey)
 	case SDLK_7:            return Rml::Input::KI_7;
 	case SDLK_8:            return Rml::Input::KI_8;
 	case SDLK_9:            return Rml::Input::KI_9;
-	case SDLK_a:            return Rml::Input::KI_A;
-	case SDLK_b:            return Rml::Input::KI_B;
-	case SDLK_c:            return Rml::Input::KI_C;
-	case SDLK_d:            return Rml::Input::KI_D;
-	case SDLK_e:            return Rml::Input::KI_E;
-	case SDLK_f:            return Rml::Input::KI_F;
-	case SDLK_g:            return Rml::Input::KI_G;
-	case SDLK_h:            return Rml::Input::KI_H;
-	case SDLK_i:            return Rml::Input::KI_I;
-	case SDLK_j:            return Rml::Input::KI_J;
-	case SDLK_k:            return Rml::Input::KI_K;
-	case SDLK_l:            return Rml::Input::KI_L;
-	case SDLK_m:            return Rml::Input::KI_M;
-	case SDLK_n:            return Rml::Input::KI_N;
-	case SDLK_o:            return Rml::Input::KI_O;
-	case SDLK_p:            return Rml::Input::KI_P;
-	case SDLK_q:            return Rml::Input::KI_Q;
-	case SDLK_r:            return Rml::Input::KI_R;
-	case SDLK_s:            return Rml::Input::KI_S;
-	case SDLK_t:            return Rml::Input::KI_T;
-	case SDLK_u:            return Rml::Input::KI_U;
-	case SDLK_v:            return Rml::Input::KI_V;
-	case SDLK_w:            return Rml::Input::KI_W;
-	case SDLK_x:            return Rml::Input::KI_X;
-	case SDLK_y:            return Rml::Input::KI_Y;
-	case SDLK_z:            return Rml::Input::KI_Z;
+	case key_a:             return Rml::Input::KI_A;
+	case key_b:             return Rml::Input::KI_B;
+	case key_c:             return Rml::Input::KI_C;
+	case key_d:             return Rml::Input::KI_D;
+	case key_e:             return Rml::Input::KI_E;
+	case key_f:             return Rml::Input::KI_F;
+	case key_g:             return Rml::Input::KI_G;
+	case key_h:             return Rml::Input::KI_H;
+	case key_i:             return Rml::Input::KI_I;
+	case key_j:             return Rml::Input::KI_J;
+	case key_k:             return Rml::Input::KI_K;
+	case key_l:             return Rml::Input::KI_L;
+	case key_m:             return Rml::Input::KI_M;
+	case key_n:             return Rml::Input::KI_N;
+	case key_o:             return Rml::Input::KI_O;
+	case key_p:             return Rml::Input::KI_P;
+	case key_q:             return Rml::Input::KI_Q;
+	case key_r:             return Rml::Input::KI_R;
+	case key_s:             return Rml::Input::KI_S;
+	case key_t:             return Rml::Input::KI_T;
+	case key_u:             return Rml::Input::KI_U;
+	case key_v:             return Rml::Input::KI_V;
+	case key_w:             return Rml::Input::KI_W;
+	case key_x:             return Rml::Input::KI_X;
+	case key_y:             return Rml::Input::KI_Y;
+	case key_z:             return Rml::Input::KI_Z;
 	case SDLK_SEMICOLON:    return Rml::Input::KI_OEM_1;
 	case SDLK_PLUS:         return Rml::Input::KI_OEM_PLUS;
 	case SDLK_COMMA:        return Rml::Input::KI_OEM_COMMA;
 	case SDLK_MINUS:        return Rml::Input::KI_OEM_MINUS;
 	case SDLK_PERIOD:       return Rml::Input::KI_OEM_PERIOD;
 	case SDLK_SLASH:        return Rml::Input::KI_OEM_2;
-	case SDLK_BACKQUOTE:    return Rml::Input::KI_OEM_3;
+	case key_grave:         return Rml::Input::KI_OEM_3;
 	case SDLK_LEFTBRACKET:  return Rml::Input::KI_OEM_4;
 	case SDLK_BACKSLASH:    return Rml::Input::KI_OEM_5;
 	case SDLK_RIGHTBRACKET: return Rml::Input::KI_OEM_6;
-	case SDLK_QUOTEDBL:     return Rml::Input::KI_OEM_7;
+	case key_dblapostrophe: return Rml::Input::KI_OEM_7;
 	case SDLK_KP_0:         return Rml::Input::KI_NUMPAD0;
 	case SDLK_KP_1:         return Rml::Input::KI_NUMPAD1;
 	case SDLK_KP_2:         return Rml::Input::KI_NUMPAD2;
@@ -287,21 +454,35 @@ int RmlSDL::GetKeyModifierState()
 {
 	SDL_Keymod sdl_mods = SDL_GetModState();
 
+#if SDL_MAJOR_VERSION >= 3
+	constexpr auto mod_ctrl = SDL_KMOD_CTRL;
+	constexpr auto mod_shift = SDL_KMOD_SHIFT;
+	constexpr auto mod_alt = SDL_KMOD_ALT;
+	constexpr auto mod_num = SDL_KMOD_NUM;
+	constexpr auto mod_caps = SDL_KMOD_CAPS;
+#else
+	constexpr auto mod_ctrl = KMOD_CTRL;
+	constexpr auto mod_shift = KMOD_SHIFT;
+	constexpr auto mod_alt = KMOD_ALT;
+	constexpr auto mod_num = KMOD_NUM;
+	constexpr auto mod_caps = KMOD_CAPS;
+#endif
+
 	int retval = 0;
 
-	if (sdl_mods & KMOD_CTRL)
+	if (sdl_mods & mod_ctrl)
 		retval |= Rml::Input::KM_CTRL;
 
-	if (sdl_mods & KMOD_SHIFT)
+	if (sdl_mods & mod_shift)
 		retval |= Rml::Input::KM_SHIFT;
 
-	if (sdl_mods & KMOD_ALT)
+	if (sdl_mods & mod_alt)
 		retval |= Rml::Input::KM_ALT;
 
-	if (sdl_mods & KMOD_NUM)
+	if (sdl_mods & mod_num)
 		retval |= Rml::Input::KM_NUMLOCK;
 
-	if (sdl_mods & KMOD_CAPS)
+	if (sdl_mods & mod_caps)
 		retval |= Rml::Input::KM_CAPSLOCK;
 
 	return retval;

+ 11 - 1
Backends/RmlUi_Platform_SDL.h

@@ -32,7 +32,14 @@
 #include <RmlUi/Core/Input.h>
 #include <RmlUi/Core/SystemInterface.h>
 #include <RmlUi/Core/Types.h>
-#include <SDL.h>
+
+#if RMLUI_SDL_VERSION_MAJOR == 3
+	#include <SDL3/SDL.h>
+#elif RMLUI_SDL_VERSION_MAJOR == 2
+	#include <SDL.h>
+#else
+	#error "Unspecified RMLUI_SDL_VERSION_MAJOR. Please set this definition to the major version of the SDL library being linked to."
+#endif
 
 class SystemInterface_SDL : public Rml::SystemInterface {
 public:
@@ -51,6 +58,9 @@ public:
 	void SetClipboardText(const Rml::String& text) override;
 	void GetClipboardText(Rml::String& text) override;
 
+	void ActivateKeyboard(Rml::Vector2f caret_position, float line_height) override;
+	void DeactivateKeyboard() override;
+
 private:
 	SDL_Window* window = nullptr;
 

+ 79 - 25
Backends/RmlUi_Renderer_SDL.cpp

@@ -30,8 +30,33 @@
 #include <RmlUi/Core/Core.h>
 #include <RmlUi/Core/FileInterface.h>
 #include <RmlUi/Core/Types.h>
-#include <SDL.h>
-#include <SDL_image.h>
+
+#if SDL_MAJOR_VERSION >= 3
+	#include <SDL3_image/SDL_image.h>
+#else
+	#include <SDL_image.h>
+#endif
+
+#if SDL_MAJOR_VERSION == 2 && !(SDL_VIDEO_RENDER_OGL)
+	#error "Only the OpenGL SDL backend is supported."
+#endif
+
+static void SetRenderClipRect(SDL_Renderer* renderer, const SDL_Rect* rect)
+{
+#if SDL_MAJOR_VERSION >= 3
+	SDL_SetRenderClipRect(renderer, rect);
+#else
+	SDL_RenderSetClipRect(renderer, rect);
+#endif
+}
+static void SetRenderViewport(SDL_Renderer* renderer, const SDL_Rect* rect)
+{
+#if SDL_MAJOR_VERSION >= 3
+	SDL_SetRenderViewport(renderer, rect);
+#else
+	SDL_RenderSetViewport(renderer, rect);
+#endif
+}
 
 RenderInterface_SDL::RenderInterface_SDL(SDL_Renderer* renderer) : renderer(renderer)
 {
@@ -43,7 +68,7 @@ RenderInterface_SDL::RenderInterface_SDL(SDL_Renderer* renderer) : renderer(rend
 
 void RenderInterface_SDL::BeginFrame()
 {
-	SDL_RenderSetViewport(renderer, nullptr);
+	SetRenderViewport(renderer, nullptr);
 	SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
 	SDL_RenderClear(renderer);
 	SDL_SetRenderDrawBlendMode(renderer, blend_mode);
@@ -70,28 +95,32 @@ void RenderInterface_SDL::RenderGeometry(Rml::CompiledGeometryHandle handle, Rml
 	const int* indices = geometry->indices.data();
 	const size_t num_indices = geometry->indices.size();
 
-	SDL_FPoint* positions = new SDL_FPoint[num_vertices];
+	Rml::UniquePtr<SDL_Vertex[]> sdl_vertices{new SDL_Vertex[num_vertices]};
 
 	for (size_t i = 0; i < num_vertices; i++)
 	{
-		positions[i].x = vertices[i].position.x + translation.x;
-		positions[i].y = vertices[i].position.y + translation.y;
+		sdl_vertices[i].position = {vertices[i].position.x + translation.x, vertices[i].position.y + translation.y};
+		sdl_vertices[i].tex_coord = {vertices[i].tex_coord.x, vertices[i].tex_coord.y};
+
+		const auto& color = vertices[i].colour;
+#if SDL_MAJOR_VERSION >= 3
+		sdl_vertices[i].color = {color.red / 255.f, color.green / 255.f, color.blue / 255.f, color.alpha / 255.f};
+#else
+		sdl_vertices[i].color = {color.red, color.green, color.blue, color.alpha};
+#endif
 	}
 
 	SDL_Texture* sdl_texture = (SDL_Texture*)texture;
 
-	SDL_RenderGeometryRaw(renderer, sdl_texture, &positions[0].x, sizeof(SDL_FPoint), (const SDL_Color*)&vertices->colour, sizeof(Rml::Vertex),
-		&vertices->tex_coord.x, sizeof(Rml::Vertex), (int)num_vertices, indices, (int)num_indices, 4);
-
-	delete[] positions;
+	SDL_RenderGeometry(renderer, sdl_texture, sdl_vertices.get(), (int)num_vertices, indices, (int)num_indices);
 }
 
 void RenderInterface_SDL::EnableScissorRegion(bool enable)
 {
 	if (enable)
-		SDL_RenderSetClipRect(renderer, &rect_scissor);
+		SetRenderClipRect(renderer, &rect_scissor);
 	else
-		SDL_RenderSetClipRect(renderer, nullptr);
+		SetRenderClipRect(renderer, nullptr);
 
 	scissor_region_enabled = enable;
 }
@@ -104,7 +133,7 @@ void RenderInterface_SDL::SetScissorRegion(Rml::Rectanglei region)
 	rect_scissor.h = region.Height();
 
 	if (scissor_region_enabled)
-		SDL_RenderSetClipRect(renderer, &rect_scissor);
+		SetRenderClipRect(renderer, &rect_scissor);
 }
 
 Rml::TextureHandle RenderInterface_SDL::LoadTexture(Rml::Vector2i& texture_dimensions, const Rml::String& source)
@@ -126,15 +155,28 @@ Rml::TextureHandle RenderInterface_SDL::LoadTexture(Rml::Vector2i& texture_dimen
 	const size_t i_ext = source.rfind('.');
 	Rml::String extension = (i_ext == Rml::String::npos ? Rml::String() : source.substr(i_ext + 1));
 
-	SDL_Surface* surface = IMG_LoadTyped_RW(SDL_RWFromMem(buffer.get(), int(buffer_size)), 1, extension.c_str());
+#if SDL_MAJOR_VERSION >= 3
+	auto CreateSurface = [&]() { return IMG_LoadTyped_IO(SDL_IOFromMem(buffer.get(), int(buffer_size)), 1, extension.c_str()); };
+	auto GetSurfaceFormat = [](SDL_Surface* surface) { return surface->format; };
+	auto ConvertSurface = [](SDL_Surface* surface, SDL_PixelFormat format) { return SDL_ConvertSurface(surface, format); };
+	auto DestroySurface = [](SDL_Surface* surface) { SDL_DestroySurface(surface); };
+#else
+	auto CreateSurface = [&]() { return IMG_LoadTyped_RW(SDL_RWFromMem(buffer.get(), int(buffer_size)), 1, extension.c_str()); };
+	auto GetSurfaceFormat = [](SDL_Surface* surface) { return surface->format->format; };
+	auto ConvertSurface = [](SDL_Surface* surface, Uint32 format) { return SDL_ConvertSurfaceFormat(surface, format, 0); };
+	auto DestroySurface = [](SDL_Surface* surface) { SDL_FreeSurface(surface); };
+#endif
+
+	SDL_Surface* surface = CreateSurface();
 	if (!surface)
 		return {};
 
-	if (surface->format->format != SDL_PIXELFORMAT_RGBA32 && surface->format->format != SDL_PIXELFORMAT_BGRA32)
-	{
-		SDL_Surface* converted_surface = SDL_ConvertSurfaceFormat(surface, SDL_PIXELFORMAT_RGBA32, 0);
-		SDL_FreeSurface(surface);
+	texture_dimensions = {surface->w, surface->h};
 
+	if (GetSurfaceFormat(surface) != SDL_PIXELFORMAT_RGBA32 && GetSurfaceFormat(surface) != SDL_PIXELFORMAT_BGRA32)
+	{
+		SDL_Surface* converted_surface = ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
+		DestroySurface(surface);
 		if (!converted_surface)
 			return {};
 
@@ -142,18 +184,18 @@ Rml::TextureHandle RenderInterface_SDL::LoadTexture(Rml::Vector2i& texture_dimen
 	}
 
 	// Convert colors to premultiplied alpha, which is necessary for correct alpha compositing.
+	const size_t pixels_byte_size = surface->w * surface->h * 4;
 	byte* pixels = static_cast<byte*>(surface->pixels);
-	for (int i = 0; i < surface->w * surface->h * 4; i += 4)
+	for (size_t i = 0; i < pixels_byte_size; i += 4)
 	{
 		const byte alpha = pixels[i + 3];
-		for (int j = 0; j < 3; ++j)
+		for (size_t j = 0; j < 3; ++j)
 			pixels[i + j] = byte(int(pixels[i + j]) * int(alpha) / 255);
 	}
 
 	SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
-
 	texture_dimensions = Rml::Vector2i(surface->w, surface->h);
-	SDL_FreeSurface(surface);
+	DestroySurface(surface);
 
 	if (texture)
 		SDL_SetTextureBlendMode(texture, blend_mode);
@@ -163,13 +205,25 @@ Rml::TextureHandle RenderInterface_SDL::LoadTexture(Rml::Vector2i& texture_dimen
 
 Rml::TextureHandle RenderInterface_SDL::GenerateTexture(Rml::Span<const Rml::byte> source, Rml::Vector2i source_dimensions)
 {
-	SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormatFrom((void*)source.data(), source_dimensions.x, source_dimensions.y, 32,
-		source_dimensions.x * 4, SDL_PIXELFORMAT_RGBA32);
+#if SDL_MAJOR_VERSION >= 3
+	auto CreateSurface = [&]() {
+		return SDL_CreateSurfaceFrom(source_dimensions.x, source_dimensions.y, SDL_PIXELFORMAT_RGBA32, (void*)source.data(), source_dimensions.x * 4);
+	};
+	auto DestroySurface = [](SDL_Surface* surface) { SDL_DestroySurface(surface); };
+#else
+	auto CreateSurface = [&]() {
+		return SDL_CreateRGBSurfaceWithFormatFrom((void*)source.data(), source_dimensions.x, source_dimensions.y, 32, source_dimensions.x * 4,
+			SDL_PIXELFORMAT_RGBA32);
+	};
+	auto DestroySurface = [](SDL_Surface* surface) { SDL_FreeSurface(surface); };
+#endif
+
+	SDL_Surface* surface = CreateSurface();
 
 	SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
 	SDL_SetTextureBlendMode(texture, blend_mode);
 
-	SDL_FreeSurface(surface);
+	DestroySurface(surface);
 	return (Rml::TextureHandle)texture;
 }
 

+ 8 - 1
Backends/RmlUi_Renderer_SDL.h

@@ -30,7 +30,14 @@
 #define RMLUI_BACKENDS_RENDERER_SDL_H
 
 #include <RmlUi/Core/RenderInterface.h>
-#include <SDL.h>
+
+#if RMLUI_SDL_VERSION_MAJOR == 3
+	#include <SDL3/SDL.h>
+#elif RMLUI_SDL_VERSION_MAJOR == 2
+	#include <SDL.h>
+#else
+	#error "Unspecified RMLUI_SDL_VERSION_MAJOR. Please set this definition to the major version of the SDL library being linked to."
+#endif
 
 class RenderInterface_SDL : public Rml::RenderInterface {
 public:

+ 57 - 20
CMake/DependenciesForBackends.cmake

@@ -6,8 +6,53 @@
 ]]
 
 # --- Window/input APIs ---
-# SDL
+
+# SDL 2 and 3 common setup
 if(RMLUI_BACKEND MATCHES "^SDL")
+	set(RMLUI_SDL_VERSION_MAJOR "" CACHE STRING "Major version of SDL to search for, or empty for automatic search.")
+	mark_as_advanced(RMLUI_SDL_VERSION_MAJOR)
+
+	# List of SDL backends that require SDL_image to work with samples
+	set(RMLUI_SDL_BACKENDS_WITH_SDLIMAGE "SDL_GL2" "SDL_GL3" "SDL_SDLrenderer")
+
+	# Determine if the selected SDL backend requires SDL_image
+	if(RMLUI_BACKEND IN_LIST RMLUI_SDL_BACKENDS_WITH_SDLIMAGE)
+		set(RMLUI_SDLIMAGE_REQUIRED TRUE)
+	else()
+		set(RMLUI_SDLIMAGE_REQUIRED FALSE)
+	endif()
+	unset(RMLUI_SDL_BACKENDS_WITH_SDLIMAGE)
+endif()
+
+# SDL 3
+if(RMLUI_BACKEND MATCHES "^SDL" AND (RMLUI_SDL_VERSION_MAJOR EQUAL "3" OR RMLUI_SDL_VERSION_MAJOR STREQUAL ""))
+	find_package("SDL3" QUIET)
+
+	if(NOT TARGET SDL3::SDL3 AND RMLUI_SDL_VERSION_MAJOR EQUAL "3")
+		report_dependency_not_found("SDL3" "SDL3" SDL3::SDL3)
+	endif()
+
+	if(TARGET SDL3::SDL3)
+		#[[
+			The official target includes the major version number in the target name. However, since we want to link to
+			SDL independent of its major version, we create a new version-independent target to link against.
+		]]
+		report_dependency_found("SDL3" SDL3::SDL3)
+		add_library(SDL::SDL INTERFACE IMPORTED)
+		target_link_libraries(SDL::SDL INTERFACE SDL3::SDL3)
+		target_compile_definitions(SDL::SDL INTERFACE RMLUI_SDL_VERSION_MAJOR=3)
+
+		if(RMLUI_SDLIMAGE_REQUIRED)
+			find_package("SDL3_image")
+			report_dependency_found_or_error("SDL3_image" "SDL3_image" SDL3_image::SDL3_image)
+			add_library(SDL_image::SDL_image INTERFACE IMPORTED)
+			target_link_libraries(SDL_image::SDL_image INTERFACE SDL3_image::SDL3_image)
+		endif()
+	endif()
+endif()
+
+# SDL 2
+if(RMLUI_BACKEND MATCHES "^SDL" AND NOT TARGET SDL::SDL AND (RMLUI_SDL_VERSION_MAJOR EQUAL "2" OR RMLUI_SDL_VERSION_MAJOR STREQUAL ""))
 	# Although the official CMake find module is called FindSDL.cmake, the official config module provided by the SDL
 	# package for its version 2 is called SDL2Config.cmake. Following this trend, the official SDL config files change
 	# their name according to their major version number
@@ -32,38 +77,29 @@ if(RMLUI_BACKEND MATCHES "^SDL")
 			INTERFACE_INCLUDE_DIRECTORIES "${SDL2_INCLUDE_DIRS}"
 		)
 	endif()
+	report_dependency_found("SDL2" SDL2::SDL2)
 
-	# SDL_GL2 backend requires GLEW
-	if(RMLUI_BACKEND STREQUAL "SDL_GL2" AND NOT TARGET GLEW::GLEW)
-		find_package("GLEW")
-		if(NOT TARGET GLEW::GLEW)
-			report_dependency_not_found("GLEW" "GLEW" GLEW::GLEW)
-		endif()
-	endif()
+	add_library(SDL::SDL INTERFACE IMPORTED)
+	target_link_libraries(SDL::SDL INTERFACE SDL2::SDL2)
+	target_compile_definitions(SDL::SDL INTERFACE RMLUI_SDL_VERSION_MAJOR=2)
 
 	# Check version requirement for the SDL renderer
 	if(RMLUI_BACKEND STREQUAL "SDL_SDLrenderer" AND SDL2_VERSION VERSION_LESS "2.0.20")
 		message(FATAL_ERROR "SDL native renderer backend (${RMLUI_BACKEND}) requires SDL 2.0.20 (found ${SDL2_VERSION}).")
 	endif()
 
-	# List of SDL backends that require SDL_image to work with samples
-	set(RMLUI_SDL_BACKENDS_WITH_SDLIMAGE "SDL_GL2" "SDL_GL3" "SDL_SDLrenderer")
-
-	# Determine if the selected SDL backend requires SDL_image
-	if(RMLUI_BACKEND IN_LIST RMLUI_SDL_BACKENDS_WITH_SDLIMAGE)
-		set(RMLUI_SDLIMAGE_REQUIRED TRUE)
-	else()
-		set(RMLUI_SDLIMAGE_REQUIRED FALSE)
-	endif()
-	unset(RMLUI_SDL_BACKENDS_WITH_SDLIMAGE)
-
-	# Require SDL_image if needed
 	if(RMLUI_SDLIMAGE_REQUIRED)
 		find_package("SDL2_image")
 		report_dependency_found_or_error("SDL2_image" "SDL2_image" SDL2_image::SDL2_image)
+		add_library(SDL_image::SDL_image INTERFACE IMPORTED)
+		target_link_libraries(SDL_image::SDL_image INTERFACE SDL2_image::SDL2_image)
 	endif()
 endif()
 
+if(RMLUI_BACKEND MATCHES "^SDL" AND NOT TARGET SDL::SDL)
+	message(FATAL_ERROR "SDL version ${RMLUI_SDL_VERSION_MAJOR} is not supported.")
+endif()
+
 # GLFW
 if(RMLUI_BACKEND MATCHES "^(BackwardCompatible_)?GLFW")
 	find_package("glfw3" "3.3")
@@ -151,6 +187,7 @@ if(RMLUI_BACKEND MATCHES "^X11")
 endif()
 
 # --- Rendering APIs ---
+
 # OpenGL
 
 # Set preferred OpenGL ABI on Linux for target OpenGL::GL