diff --git a/.gitignore b/.gitignore
index 51269c9..e92ae95 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,7 @@ __pycache__/
/build
/build.ninja
/shuriken.override.yaml
+
+# Dependencies
+/vendor
+!/vendor/README.md
diff --git a/assets/fonts/LatoLatin-Regular.ttf b/assets/fonts/LatoLatin-Regular.ttf
new file mode 100644
index 0000000..bcc5778
Binary files /dev/null and b/assets/fonts/LatoLatin-Regular.ttf differ
diff --git a/assets/ui/base.rcss b/assets/ui/base.rcss
new file mode 100644
index 0000000..b325039
--- /dev/null
+++ b/assets/ui/base.rcss
@@ -0,0 +1,88 @@
+/* Base style sheet based on: https://mikke89.github.io/RmlUiDoc/pages/rml/html4_style_sheet.html */
+
+body, div, h1, h2, h3, h4, h5, h6, p, hr, pre {
+ display: block;
+}
+
+h1 {
+ font-size: 2em;
+ margin: .67em 0;
+}
+
+h2 {
+ font-size: 1.5em;
+ margin: .75em 0;
+}
+
+h3 {
+ font-size: 1.17em;
+ margin: .83em 0;
+}
+
+h4 {
+ margin: 1.12em 0;
+}
+
+h5 {
+ font-size: .83em;
+ margin: 1.5em 0;
+}
+
+h6 {
+ font-size: .75em;
+ margin: 1.67em 0;
+}
+
+h1, h2, h3, h4, h5, h6, strong {
+ font-weight: bold;
+}
+
+em {
+ font-style: italic;
+}
+
+pre {
+ white-space: pre;
+}
+
+hr {
+ border-width: 1px;
+}
+
+table {
+ box-sizing: border-box;
+ display: table;
+}
+
+tr {
+ box-sizing: border-box;
+ display: table-row;
+}
+
+td {
+ box-sizing: border-box;
+ display: table-cell;
+}
+
+col {
+ box-sizing: border-box;
+ display: table-column;
+}
+
+colgroup {
+ display: table-column-group;
+}
+
+thead, tbody, tfoot {
+ display: table-row-group;
+}
+
+tabset tabs {
+ display: block;
+}
+
+input {
+ background-color: white;
+ color: black;
+ cursor: text;
+}
diff --git a/assets/ui/main_ui.rml b/assets/ui/main_ui.rml
new file mode 100644
index 0000000..9dc12f1
--- /dev/null
+++ b/assets/ui/main_ui.rml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ Meow!
+
+
diff --git a/shuriken.yaml b/shuriken.yaml
index 5983465..fee3c69 100644
--- a/shuriken.yaml
+++ b/shuriken.yaml
@@ -1,7 +1,17 @@
defaults:
cpp_standard: c++23
- cpp_flags: -Wall -Wextra -Werror -pedantic
- linker_args: -lSDL3 -lSDL3_image
+ cpp_flags: >-
+ -Wall
+ -Wextra
+ -pedantic
+ -Ivendor/RmlUi/Include
+
+ linker_args: >-
+ -lSDL3
+ -lSDL3_image
+ -lfreetype
+ vendor/RmlUi/Build/librmlui_debugger.a
+ vendor/RmlUi/Build/librmlui.a
default_target: linux
diff --git a/src/main.cpp b/src/main.cpp
index 46d3c3e..a6e0353 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -13,7 +13,7 @@ SDL_AppResult SDL_AppInit(void** appstate, const int /*argc*/, char** /*argv*/)
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{
auto* app = static_cast(appstate);
- return static_cast(app->handle_event(event));
+ return static_cast(app->handle_event(*event));
}
SDL_AppResult SDL_AppIterate(void* appstate)
diff --git a/src/rutile/app.cppm b/src/rutile/app.cppm
index df7ab6a..a3059d5 100644
--- a/src/rutile/app.cppm
+++ b/src/rutile/app.cppm
@@ -2,7 +2,7 @@ module;
#include
#include
-#include
+#include
export module rutile.app;
@@ -48,7 +48,7 @@ export namespace rutile
return sdl::AppResult::Continue;
}
- sdl::AppResult handle_event(const sdl::Event* event)
+ sdl::AppResult handle_event(const sdl::Event& event)
{
try {
if (!engine_->handle_event(event)) {
diff --git a/src/rutile/core/engine.cppm b/src/rutile/core/engine.cppm
index de1f8ad..7fec209 100644
--- a/src/rutile/core/engine.cppm
+++ b/src/rutile/core/engine.cppm
@@ -7,6 +7,7 @@ export module rutile.core.engine;
import rutile.config;
import rutile.core.render_server;
+import rutile.core.ui.ui_server;
import wrappers.sdl;
export namespace rutile
@@ -21,11 +22,13 @@ export namespace rutile
sdl::Window window_;
RenderServer render_server_;
+ UIServer ui_server_;
// Private constructor
Engine(sdl::Window&& window, sdl::Renderer&& renderer)
: window_{std::move(window)},
- render_server_{std::move(renderer)}
+ render_server_{std::move(renderer)},
+ ui_server_{render_server_.get_renderer(), window_}
{
instantiated_ = true;
}
@@ -56,12 +59,14 @@ export namespace rutile
0
);
+ // NOLINTBEGIN: "Allocated memory is leaked"
return std::unique_ptr{
new Engine{
std::move(sdl_window),
std::move(sdl_renderer)
}
};
+ // NOLINTEND
}
bool keep_running() const
@@ -79,15 +84,24 @@ export namespace rutile
return render_server_;
}
- // Handles an SDL event. Returns true if the event has been handled.
- bool handle_event(const sdl::Event* event)
+ UIServer& get_ui_server()
{
- if (event->type == sdl::EventType::Quit) {
+ return ui_server_;
+ }
+
+ // Handles an SDL event. Returns true if the event has been handled.
+ bool handle_event(const sdl::Event& event)
+ {
+ if (event.type == sdl::EventTypes::Quit) {
// Exit the application
keep_running_ = false;
return true;
}
+ if (ui_server_.handle_event(event)) {
+ return true;
+ }
+
return false;
}
diff --git a/src/rutile/core/render_server.cppm b/src/rutile/core/render_server.cppm
index 2420d3a..ad32e73 100644
--- a/src/rutile/core/render_server.cppm
+++ b/src/rutile/core/render_server.cppm
@@ -27,7 +27,9 @@ export namespace rutile
: renderer_{std::move(renderer)},
texture_loader_{renderer_},
texture_manager_{texture_loader_}
- {}
+ {
+ renderer_.set_vsync(1);
+ }
// No copy or move operations
RenderServer(const RenderServer&) = delete;
diff --git a/src/rutile/core/texture_loader.cppm b/src/rutile/core/texture_loader.cppm
index 04c5a83..2bf1f08 100644
--- a/src/rutile/core/texture_loader.cppm
+++ b/src/rutile/core/texture_loader.cppm
@@ -29,7 +29,7 @@ export namespace rutile
sdl::Texture load_resource(const std::string& filename) const
{
- return sdl_image::LoadTexture(renderer_, filename);
+ return sdl_image::load_texture(renderer_, filename);
}
};
}
diff --git a/src/rutile/core/ui/backend/rmlui_input_handler.cppm b/src/rutile/core/ui/backend/rmlui_input_handler.cppm
new file mode 100644
index 0000000..41622ff
--- /dev/null
+++ b/src/rutile/core/ui/backend/rmlui_input_handler.cppm
@@ -0,0 +1,278 @@
+module;
+
+#include
+#include
+#include
+
+export module rutile.core.ui.backend.rmlui_input_handler;
+
+import wrappers.sdl;
+
+export namespace rutile
+{
+ class RmlUiInputHandler
+ {
+ sdl::Window& window_;
+
+ public:
+ explicit RmlUiInputHandler(sdl::Window& window)
+ : window_{window}
+ {}
+
+ static Rml::Input::KeyIdentifier convert_key(const int sdl_key)
+ {
+ namespace Key = sdl::Key;
+
+ // clang-format off
+ switch (sdl_key) {
+ case Key::Unknown: return Rml::Input::KI_UNKNOWN;
+ case Key::Escape: return Rml::Input::KI_ESCAPE;
+ case Key::Space: return Rml::Input::KI_SPACE;
+ case Key::Num0: return Rml::Input::KI_0;
+ case Key::Num1: return Rml::Input::KI_1;
+ case Key::Num2: return Rml::Input::KI_2;
+ case Key::Num3: return Rml::Input::KI_3;
+ case Key::Num4: return Rml::Input::KI_4;
+ case Key::Num5: return Rml::Input::KI_5;
+ case Key::Num6: return Rml::Input::KI_6;
+ case Key::Num7: return Rml::Input::KI_7;
+ case Key::Num8: return Rml::Input::KI_8;
+ case Key::Num9: return Rml::Input::KI_9;
+ 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 Key::Semicolon: return Rml::Input::KI_OEM_1;
+ case Key::Plus: return Rml::Input::KI_OEM_PLUS;
+ case Key::Comma: return Rml::Input::KI_OEM_COMMA;
+ case Key::Minus: return Rml::Input::KI_OEM_MINUS;
+ case Key::Period: return Rml::Input::KI_OEM_PERIOD;
+ case Key::Slash: return Rml::Input::KI_OEM_2;
+ case Key::Grave: return Rml::Input::KI_OEM_3;
+ case Key::LeftBracket: return Rml::Input::KI_OEM_4;
+ case Key::Backslash: return Rml::Input::KI_OEM_5;
+ case Key::RightBracket: return Rml::Input::KI_OEM_6;
+ case Key::DblApostrophe: return Rml::Input::KI_OEM_7;
+ case Key::Numpad0: return Rml::Input::KI_NUMPAD0;
+ case Key::Numpad1: return Rml::Input::KI_NUMPAD1;
+ case Key::Numpad2: return Rml::Input::KI_NUMPAD2;
+ case Key::Numpad3: return Rml::Input::KI_NUMPAD3;
+ case Key::Numpad4: return Rml::Input::KI_NUMPAD4;
+ case Key::Numpad5: return Rml::Input::KI_NUMPAD5;
+ case Key::Numpad6: return Rml::Input::KI_NUMPAD6;
+ case Key::Numpad7: return Rml::Input::KI_NUMPAD7;
+ case Key::Numpad8: return Rml::Input::KI_NUMPAD8;
+ case Key::Numpad9: return Rml::Input::KI_NUMPAD9;
+ case Key::NumpadEnter: return Rml::Input::KI_NUMPADENTER;
+ case Key::NumpadMultiply: return Rml::Input::KI_MULTIPLY;
+ case Key::NumpadPlus: return Rml::Input::KI_ADD;
+ case Key::NumpadMinus: return Rml::Input::KI_SUBTRACT;
+ case Key::NumpadPeriod: return Rml::Input::KI_DECIMAL;
+ case Key::NumpadDivide: return Rml::Input::KI_DIVIDE;
+ case Key::NumpadEquals: return Rml::Input::KI_OEM_NEC_EQUAL;
+ case Key::Backspace: return Rml::Input::KI_BACK;
+ case Key::Tab: return Rml::Input::KI_TAB;
+ case Key::Clear: return Rml::Input::KI_CLEAR;
+ case Key::Return: return Rml::Input::KI_RETURN;
+ case Key::Pause: return Rml::Input::KI_PAUSE;
+ case Key::CapsLock: return Rml::Input::KI_CAPITAL;
+ case Key::PageUp: return Rml::Input::KI_PRIOR;
+ case Key::PageDown: return Rml::Input::KI_NEXT;
+ case Key::End: return Rml::Input::KI_END;
+ case Key::Home: return Rml::Input::KI_HOME;
+ case Key::Left: return Rml::Input::KI_LEFT;
+ case Key::Up: return Rml::Input::KI_UP;
+ case Key::Right: return Rml::Input::KI_RIGHT;
+ case Key::Down: return Rml::Input::KI_DOWN;
+ case Key::Insert: return Rml::Input::KI_INSERT;
+ case Key::Delete: return Rml::Input::KI_DELETE;
+ case Key::Help: return Rml::Input::KI_HELP;
+ case Key::F1: return Rml::Input::KI_F1;
+ case Key::F2: return Rml::Input::KI_F2;
+ case Key::F3: return Rml::Input::KI_F3;
+ case Key::F4: return Rml::Input::KI_F4;
+ case Key::F5: return Rml::Input::KI_F5;
+ case Key::F6: return Rml::Input::KI_F6;
+ case Key::F7: return Rml::Input::KI_F7;
+ case Key::F8: return Rml::Input::KI_F8;
+ case Key::F9: return Rml::Input::KI_F9;
+ case Key::F10: return Rml::Input::KI_F10;
+ case Key::F11: return Rml::Input::KI_F11;
+ case Key::F12: return Rml::Input::KI_F12;
+ case Key::F13: return Rml::Input::KI_F13;
+ case Key::F14: return Rml::Input::KI_F14;
+ case Key::F15: return Rml::Input::KI_F15;
+ case Key::NumLockClear: return Rml::Input::KI_NUMLOCK;
+ case Key::ScrollLock: return Rml::Input::KI_SCROLL;
+ case Key::LShift: return Rml::Input::KI_LSHIFT;
+ case Key::RShift: return Rml::Input::KI_RSHIFT;
+ case Key::LCtrl: return Rml::Input::KI_LCONTROL;
+ case Key::RCtrl: return Rml::Input::KI_RCONTROL;
+ case Key::LAlt: return Rml::Input::KI_LMENU;
+ case Key::RAlt: return Rml::Input::KI_RMENU;
+ case Key::LGui: return Rml::Input::KI_LMETA;
+ case Key::RGui: return Rml::Input::KI_RMETA;
+ default: return Rml::Input::KI_UNKNOWN;
+ }
+ // clang-format on
+ }
+
+ static int convert_mouse_button(const sdl::MouseButtonIndex sdl_mouse_button)
+ {
+ switch (sdl_mouse_button) {
+ case sdl::MouseButtons::Left: return 0;
+ case sdl::MouseButtons::Right: return 1;
+ case sdl::MouseButtons::Middle: return 2;
+ default: return 3;
+ }
+ }
+
+ static int get_key_modifiers()
+ {
+ const sdl::KeyModState sdl_mods = sdl::get_key_mod_state();
+ int retval = 0;
+
+ if (sdl_mods & sdl::KeyMods::Ctrl) {
+ retval |= Rml::Input::KM_CTRL;
+ }
+ if (sdl_mods & sdl::KeyMods::Shift) {
+ retval |= Rml::Input::KM_SHIFT;
+ }
+ if (sdl_mods & sdl::KeyMods::Alt) {
+ retval |= Rml::Input::KM_ALT;
+ }
+ if (sdl_mods & sdl::KeyMods::NumLock) {
+ retval |= Rml::Input::KM_NUMLOCK;
+ }
+ if (sdl_mods & sdl::KeyMods::CapsLock) {
+ retval |= Rml::Input::KM_CAPSLOCK;
+ }
+
+ return retval;
+ }
+
+ static Rml::TouchList convert_touch_event(const Rml::Context& context, const sdl::Event& ev)
+ {
+ const Rml::Vector2f position = Rml::Vector2f{ev.tfinger.x, ev.tfinger.y}
+ * Rml::Vector2f{context.GetDimensions()};
+ return {Rml::Touch{static_cast(ev.tfinger.fingerID), position}};
+ }
+
+ /**
+ * Handle input events and forward them to RmlUi if relevant.
+ * Returns true if the event was consumed (inverting the return values of RmlUi, where true means the event
+ * was NOT consumed).
+ */
+ bool handle_input_event(Rml::Context& context, const sdl::Event& ev) const
+ {
+ // Return value of RmlUi event handlers, true if the event was NOT consumed.
+ // Result will be inverted at the end of this method to return true if the event WAS consumed.
+ bool result = true;
+
+ switch (ev.type) {
+ case sdl::EventTypes::MouseMotion: {
+ const float pixel_density = window_.get_pixel_density();
+ result = context.ProcessMouseMove(
+ static_cast(ev.motion.x * pixel_density),
+ static_cast(ev.motion.y * pixel_density),
+ get_key_modifiers()
+ );
+ break;
+ }
+
+ case sdl::EventTypes::MouseButtonDown:
+ result = context.ProcessMouseButtonDown(
+ convert_mouse_button(ev.button.button),
+ get_key_modifiers()
+ );
+ sdl::capture_mouse(true);
+ break;
+
+ case sdl::EventTypes::MouseButtonUp:
+ sdl::capture_mouse(false);
+ result = context.ProcessMouseButtonUp(
+ convert_mouse_button(ev.button.button), get_key_modifiers()
+ );
+ break;
+
+ case sdl::EventTypes::MouseWheel:
+ result = context.ProcessMouseWheel(
+ Rml::Vector2f{ev.wheel.x, -ev.wheel.y},
+ get_key_modifiers()
+ );
+ break;
+
+ case sdl::EventTypes::KeyDown:
+ result = context.ProcessKeyDown(convert_key(ev.key.key), get_key_modifiers());
+
+ if (ev.key.key == sdl::Key::Return || ev.key.key == sdl::Key::NumpadEnter) {
+ result &= context.ProcessTextInput('\n');
+ }
+ break;
+
+ case sdl::EventTypes::KeyUp:
+ result = context.ProcessKeyUp(convert_key(ev.key.key), get_key_modifiers());
+ break;
+
+ case sdl::EventTypes::TextInput:
+ result = context.ProcessTextInput(Rml::String(&ev.text.text[0]));
+ break;
+
+ case sdl::EventTypes::FingerDown:
+ result = context.ProcessTouchStart(convert_touch_event(context, ev), get_key_modifiers());
+ break;
+
+ case sdl::EventTypes::FingerMotion:
+ result = context.ProcessTouchMove(convert_touch_event(context, ev), get_key_modifiers());
+ break;
+
+ case sdl::EventTypes::FingerUp:
+ result = context.ProcessTouchEnd(convert_touch_event(context, ev), get_key_modifiers());
+ break;
+
+ case sdl::EventTypes::WindowMouseLeave:
+ context.ProcessMouseLeave();
+ break;
+
+ case sdl::EventTypes::WindowPixelSizeChanged:
+ context.SetDimensions(
+ Rml::Vector2i{ev.window.data1, ev.window.data2}
+ );
+ break;
+
+ case sdl::EventTypes::WindowDisplayScaleChanged:
+ context.SetDensityIndependentPixelRatio(window_.get_display_scale());
+ break;
+
+ default:
+ break;
+ }
+
+ // Invert result to be consistent with the event handling of this app. Return true if the event was
+ // consumed, i.e. no further event handling is necessary.
+ return !result;
+ }
+ };
+}
diff --git a/src/rutile/core/ui/backend/rmlui_render_interface.cppm b/src/rutile/core/ui/backend/rmlui_render_interface.cppm
new file mode 100644
index 0000000..c2412f3
--- /dev/null
+++ b/src/rutile/core/ui/backend/rmlui_render_interface.cppm
@@ -0,0 +1,199 @@
+// RmlUi RenderInterface implementation for SDL using SDLrenderer.
+// Based on RmlUi example code: vendor/RmlUi/Backends/RmlUi_Renderer_SDL.{cpp,h}
+
+module;
+
+#include
+#include
+#include
+#include
+#include
+
+export module rutile.core.ui.backend.rmlui_render_interface;
+
+import wrappers.sdl;
+import wrappers.sdl_image;
+
+export namespace rutile
+{
+ class RmlUiRenderInterface final : public Rml::RenderInterface
+ {
+ struct GeometryView
+ {
+ Rml::Span vertices;
+ Rml::Span indices;
+ };
+
+ sdl::Renderer& renderer_;
+ SDL_BlendMode blend_mode_ = {};
+ sdl::Rect rect_scissor_ = {};
+ bool scissor_region_enabled_ = false;
+
+ public:
+ explicit RmlUiRenderInterface(sdl::Renderer& renderer)
+ : renderer_{renderer}
+ {
+ // RmlUi serves vertex colors and textures with premultiplied alpha, set the blend mode accordingly.
+ // Equivalent to glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA).
+ blend_mode_ = SDL_ComposeCustomBlendMode(
+ SDL_BLENDFACTOR_ONE,
+ SDL_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
+ SDL_BLENDOPERATION_ADD,
+ SDL_BLENDFACTOR_ONE,
+ SDL_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
+ SDL_BLENDOPERATION_ADD
+ );
+ }
+
+ /**
+ * Sets up OpenGL states for taking rendering commands from RmlUi.
+ */
+ void prepare_render() const
+ {
+ renderer_.set_draw_blend_mode(blend_mode_);
+ }
+
+ // -- Inherited from Rml::RenderInterface
+
+ Rml::CompiledGeometryHandle CompileGeometry(
+ const Rml::Span vertices,
+ const Rml::Span indices
+ ) override
+ {
+ // RmlUi handles lifetime and calls ReleaseGeometry to delete the object
+ GeometryView* data = new GeometryView{vertices, indices}; // NOLINT: "Allocated memory is leaked"
+ return reinterpret_cast(data);
+ }
+
+ void ReleaseGeometry(const Rml::CompiledGeometryHandle geometry) override
+ {
+ delete reinterpret_cast(geometry);
+ }
+
+ void RenderGeometry(
+ const Rml::CompiledGeometryHandle handle,
+ const Rml::Vector2f translation,
+ const Rml::TextureHandle texture
+ ) override
+ {
+ const GeometryView* geometry = reinterpret_cast(handle);
+ const Rml::Vertex* vertices = geometry->vertices.data();
+ const size_t num_vertices = geometry->vertices.size();
+ const int* indices = geometry->indices.data();
+ const size_t num_indices = geometry->indices.size();
+
+ // Convert RmlUi vertices to SDL vertices
+ auto sdl_vertices = std::make_unique(num_vertices);
+
+ for (size_t i = 0; i < num_vertices; i++) {
+ const auto& [position, color, tex_coord] = vertices[i];
+
+ sdl_vertices[i].position = {
+ position.x + translation.x,
+ position.y + translation.y,
+ };
+ sdl_vertices[i].tex_coord = {
+ tex_coord.x,
+ tex_coord.y,
+ };
+ sdl_vertices[i].color = {
+ color.red / 255.f,
+ color.green / 255.f,
+ color.blue / 255.f,
+ color.alpha / 255.f,
+ };
+ }
+
+ renderer_.render_geometry(
+ reinterpret_cast(texture),
+ sdl_vertices.get(),
+ static_cast(num_vertices),
+ indices,
+ static_cast(num_indices)
+ );
+ }
+
+ Rml::TextureHandle LoadTexture(
+ Rml::Vector2i& texture_dimensions,
+ const Rml::String& source
+ ) override
+ {
+ sdl::Surface surface = sdl_image::load(source.c_str());
+
+ if (surface.get_format() != SDL_PIXELFORMAT_RGBA32 && surface.get_format() != SDL_PIXELFORMAT_BGRA32) {
+ surface = surface.convert_format(SDL_PIXELFORMAT_RGBA32);
+ }
+
+ // Convert colors to premultiplied alpha, which is necessary for correct alpha compositing.
+ const size_t pixels_byte_size = surface.get_width() * surface.get_height() * 4;
+ Rml::byte* pixels = static_cast(surface.get_raw_pixels());
+
+ for (size_t i = 0; i < pixels_byte_size; i += 4) {
+ const Rml::byte alpha = pixels[i + 3];
+ for (size_t j = 0; j < 3; ++j) {
+ pixels[i + j] = static_cast(
+ static_cast(pixels[i + j]) * static_cast(alpha) / 255
+ );
+ }
+ }
+
+ SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer_.get_raw(), surface.get_raw());
+ if (texture) {
+ SDL_SetTextureBlendMode(texture, blend_mode_);
+ }
+
+ texture_dimensions = {surface.get_width(), surface.get_height()};
+ return reinterpret_cast(texture);
+ }
+
+ Rml::TextureHandle GenerateTexture(
+ const Rml::Span source,
+ const Rml::Vector2i source_dimensions
+ ) override
+ {
+ RMLUI_ASSERT(
+ source.data() && source.size() == static_cast(source_dimensions.x * source_dimensions.y * 4)
+ );
+
+ const sdl::Surface surface = sdl::Surface::create_from(
+ source_dimensions.x,
+ source_dimensions.y,
+ SDL_PIXELFORMAT_RGBA32,
+ // Cast away the const because SDL_CreateSurfaceFrom() expects a void*.
+ // This is fine as long as we don't modify surface.get_raw_pixels().
+ const_cast(source.data()),
+ source_dimensions.x * 4
+ );
+
+ SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer_.get_raw(), surface.get_raw());
+ if (texture) {
+ SDL_SetTextureBlendMode(texture, blend_mode_);
+ }
+
+ return reinterpret_cast(texture);
+ }
+
+ void ReleaseTexture(const Rml::TextureHandle texture_handle) override
+ {
+ SDL_DestroyTexture(reinterpret_cast(texture_handle));
+ }
+
+ void EnableScissorRegion(const bool enable) override
+ {
+ renderer_.set_clip_rect(enable ? &rect_scissor_ : nullptr);
+ scissor_region_enabled_ = enable;
+ }
+
+ void SetScissorRegion(const Rml::Rectanglei region) override
+ {
+ rect_scissor_.x = region.Left();
+ rect_scissor_.y = region.Top();
+ rect_scissor_.w = region.Width();
+ rect_scissor_.h = region.Height();
+
+ if (scissor_region_enabled_) {
+ renderer_.set_clip_rect(&rect_scissor_);
+ }
+ }
+ };
+}
diff --git a/src/rutile/core/ui/backend/rmlui_system_interface.cppm b/src/rutile/core/ui/backend/rmlui_system_interface.cppm
new file mode 100644
index 0000000..6c74193
--- /dev/null
+++ b/src/rutile/core/ui/backend/rmlui_system_interface.cppm
@@ -0,0 +1,118 @@
+// RmlUi SystemInterface implementation for SDL.
+// Based on RmlUi example code: vendor/RmlUi/Backends/RmlUi_Platform_SDL.{cpp,h}
+
+module;
+
+#include
+#include
+#include
+#include
+
+export module rutile.core.ui.backend.rmlui_system_interface;
+
+import utils.memory;
+import wrappers.sdl;
+
+// Shorthand for a unique pointer to an SDL_Cursor
+// TODO: Build actual wrapper sdl::Cursor at some point?
+using SDL_CursorPtr = std::unique_ptr>;
+
+export namespace rutile
+{
+ class RmlUiSystemInterface final : public Rml::SystemInterface
+ {
+ sdl::Window& window_;
+
+ SDL_CursorPtr cursor_default;
+ SDL_CursorPtr cursor_move;
+ SDL_CursorPtr cursor_pointer;
+ SDL_CursorPtr cursor_resize;
+ SDL_CursorPtr cursor_cross;
+ SDL_CursorPtr cursor_text;
+ SDL_CursorPtr cursor_unavailable;
+
+ public:
+ explicit RmlUiSystemInterface(sdl::Window& window)
+ : window_{window}
+ {
+ cursor_default = SDL_CursorPtr{SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT)};
+ cursor_move = SDL_CursorPtr{SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_MOVE)};
+ cursor_pointer = SDL_CursorPtr{SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_POINTER)};
+ cursor_resize = SDL_CursorPtr{SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NWSE_RESIZE)};
+ cursor_cross = SDL_CursorPtr{SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_CROSSHAIR)};
+ cursor_text = SDL_CursorPtr{SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_TEXT)};
+ cursor_unavailable = SDL_CursorPtr{SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NOT_ALLOWED)};
+ }
+
+ // -- Inherited from Rml::SystemInterface
+
+ double GetElapsedTime() override
+ {
+ // TODO: Add a shared timer to the engine and use that here.
+ static const Uint64 start = SDL_GetPerformanceCounter();
+ static const double frequency = static_cast(SDL_GetPerformanceFrequency());
+ return static_cast(SDL_GetPerformanceCounter() - start) / frequency;
+ }
+
+ void SetMouseCursor(const Rml::String& cursor_name) override
+ {
+ SDL_Cursor* cursor = nullptr;
+
+ if (cursor_name.empty() || cursor_name == "arrow") {
+ cursor = cursor_default.get();
+ }
+ else if (cursor_name == "move") {
+ cursor = cursor_move.get();
+ }
+ else if (cursor_name == "pointer") {
+ cursor = cursor_pointer.get();
+ }
+ else if (cursor_name == "resize") {
+ cursor = cursor_resize.get();
+ }
+ else if (cursor_name == "cross") {
+ cursor = cursor_cross.get();
+ }
+ else if (cursor_name == "text") {
+ cursor = cursor_text.get();
+ }
+ else if (cursor_name == "unavailable") {
+ cursor = cursor_unavailable.get();
+ }
+ else if (Rml::StringUtilities::StartsWith(cursor_name, "rmlui-scroll")) {
+ cursor = cursor_move.get();
+ }
+
+ if (cursor) {
+ SDL_SetCursor(cursor);
+ }
+ }
+
+ void SetClipboardText(const Rml::String& text) override
+ {
+ sdl::set_clipboard_text(text);
+ }
+
+ void GetClipboardText(Rml::String& text) override
+ {
+ text = sdl::get_clipboard_text();
+ }
+
+ void ActivateKeyboard(const Rml::Vector2f caret_position, const float line_height) override
+ {
+ const sdl::Rect rect{
+ static_cast(caret_position.x),
+ static_cast(caret_position.y),
+ 1,
+ static_cast(line_height),
+ };
+ window_.set_text_input_area(&rect, 0);
+ window_.start_text_input();
+ }
+
+ void DeactivateKeyboard() override
+ {
+ window_.stop_text_input();
+ }
+ };
+}
diff --git a/src/rutile/core/ui/ui_server.cppm b/src/rutile/core/ui/ui_server.cppm
new file mode 100644
index 0000000..3f15b70
--- /dev/null
+++ b/src/rutile/core/ui/ui_server.cppm
@@ -0,0 +1,124 @@
+module;
+
+#include
+#include
+#include
+#include
+
+export module rutile.core.ui.ui_server;
+
+import rutile.core.ui.backend.rmlui_render_interface;
+import rutile.core.ui.backend.rmlui_system_interface;
+import rutile.core.ui.backend.rmlui_input_handler;
+import wrappers.sdl;
+
+export namespace rutile
+{
+ class UIServer
+ {
+ // Whether this class is currently instantiated (to prevent multiple instances because we need to do
+ // Rml::Initialise() and Rml::Shutdown()).
+ static bool instantiated_;
+
+ RmlUiSystemInterface system_interface_;
+ RmlUiRenderInterface render_interface_;
+ RmlUiInputHandler input_handler_;
+
+ // RmlUi context (non-owning pointer, lifetime managed by library)
+ Rml::Context* context_;
+
+ public:
+ explicit UIServer(sdl::Renderer& renderer, sdl::Window& window)
+ : system_interface_{window},
+ render_interface_{renderer},
+ input_handler_{window}
+ {
+ // Prevent the class from being instantiated multiple times
+ assert(!instantiated_);
+
+ Rml::SetSystemInterface(&system_interface_);
+ Rml::SetRenderInterface(&render_interface_);
+ Rml::Initialise();
+
+ // Create the main RmlUi context
+ auto [window_width, window_height] = window.get_size();
+ context_ = Rml::CreateContext("main", Rml::Vector2i(window_width, window_height));
+ if (!context_) {
+ throw std::runtime_error("RmlUi: Couldn't create context");
+ }
+
+ // TODO: Implement some kind of switch to disable debugger (just use config::debug?)
+ Rml::Debugger::Initialise(context_);
+
+ instantiated_ = true;
+ }
+
+ // No copy or move operations
+ UIServer(const UIServer&) = delete;
+ UIServer& operator=(const UIServer&) = delete;
+ UIServer(UIServer&&) = delete;
+ UIServer& operator=(UIServer&&) = delete;
+
+ ~UIServer()
+ {
+ Rml::Shutdown();
+ instantiated_ = false;
+ }
+
+ constexpr Rml::Context& get_context() const
+ {
+ // TODO: Can we assume context_ is always non-nullptr?
+ assert(context_);
+ return *context_;
+ }
+
+ // Handles an SDL event. Returns true if the event has been handled.
+ bool handle_event(const sdl::Event& event) const
+ {
+ // Toggle RmlUi debugger with F12
+ if (event.type == sdl::EventTypes::KeyDown && event.key.key == sdl::Key::F12) {
+ Rml::Debugger::SetVisible(!Rml::Debugger::IsVisible());
+ return true;
+ }
+
+ // Let RmlUi handle relevant events
+ if (input_handler_.handle_input_event(get_context(), event)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ void update() const
+ {
+ get_context().Update();
+ }
+
+ void render() const
+ {
+ render_interface_.prepare_render();
+ get_context().Render();
+ }
+
+ static void load_font_face(const std::string& file_path)
+ {
+ if (!Rml::LoadFontFace(file_path)) {
+ throw std::runtime_error(std::format("RmlUi: Couldn't load font face \"{}\"", file_path));
+ }
+ }
+
+ // TODO: Can/should we avoid the (non-owning) raw pointers here?
+ Rml::ElementDocument* load_document(const std::string& file_path) const
+ {
+ Rml::ElementDocument* document = get_context().LoadDocument(file_path);
+
+ if (!document) {
+ throw std::runtime_error(std::format("RmlUi: Couldn't load document \"{}\"", file_path));
+ }
+
+ return document;
+ }
+ };
+
+ bool UIServer::instantiated_ = false;
+}
diff --git a/src/rutile/game/game.cppm b/src/rutile/game/game.cppm
index e788c34..c7a45b9 100644
--- a/src/rutile/game/game.cppm
+++ b/src/rutile/game/game.cppm
@@ -4,6 +4,7 @@ module;
#include
#include
#include
+#include
export module rutile.game.game;
@@ -63,6 +64,12 @@ export namespace rutile
},
tile_map_(0, 32, 16, 12, {50, 50})
{
+ // Initialize UI
+ const auto& ui_server = engine_.get_ui_server();
+ ui_server.load_font_face("assets/fonts/LatoLatin-Regular.ttf");
+ Rml::ElementDocument* document = ui_server.load_document("assets/ui/main_ui.rml");
+ document->Show();
+
// Initialize tile map with example data
for (auto x = 0; x < 10; x++) {
for (auto y = 0; y < 10; y++) {
@@ -70,6 +77,8 @@ export namespace rutile
tile_map_.set_tile(x, y, tile_index);
}
}
+
+ instantiated_ = true;
}
public:
@@ -96,20 +105,20 @@ export namespace rutile
}
// Handles an SDL event. Returns true if the event has been handled.
- bool handle_event(const sdl::Event* event)
+ bool handle_event(const sdl::Event& event)
{
- if (event->type == sdl::EventType::MouseMotion) {
- player_sprite_.set_position({event->motion.x, event->motion.y});
+ if (event.type == sdl::EventTypes::MouseMotion) {
+ player_sprite_.set_position({event.motion.x, event.motion.y});
return true;
}
- if (event->type == sdl::EventType::MouseButtonUp) {
+ if (event.type == sdl::EventTypes::MouseButtonUp) {
sprites_.push_back(
Sprite::create_from_texture(
engine_.get_render_server(),
"assets/sprites/neofox_64.png",
std::nullopt,
- sdl::FPoint{event->motion.x, event->motion.y},
+ sdl::FPoint{event.motion.x, event.motion.y},
sdl::FPoint{-32, -32}
)
);
@@ -121,6 +130,8 @@ export namespace rutile
void update()
{
+ // TODO: Move this to Engine
+ engine_.get_ui_server().update();
}
void render() const
@@ -137,6 +148,9 @@ export namespace rutile
}
player_sprite_.draw(render_server);
+ // TODO: Move this to Engine
+ engine_.get_ui_server().render();
+
render_server.finish_frame();
}
diff --git a/src/wrappers/sdl.cppm b/src/wrappers/sdl.cppm
index 8e8088d..78f7c72 100644
--- a/src/wrappers/sdl.cppm
+++ b/src/wrappers/sdl.cppm
@@ -3,9 +3,14 @@ module;
export module wrappers.sdl;
// Export submodules
+export import wrappers.sdl.clipboard;
export import wrappers.sdl.error;
export import wrappers.sdl.events;
export import wrappers.sdl.init;
+export import wrappers.sdl.keyboard;
+export import wrappers.sdl.mouse;
export import wrappers.sdl.rect;
export import wrappers.sdl.render;
+export import wrappers.sdl.surface;
+export import wrappers.sdl.utils;
export import wrappers.sdl.video;
diff --git a/src/wrappers/sdl/clipboard.cppm b/src/wrappers/sdl/clipboard.cppm
new file mode 100644
index 0000000..bb84b79
--- /dev/null
+++ b/src/wrappers/sdl/clipboard.cppm
@@ -0,0 +1,38 @@
+module;
+
+#include
+#include
+
+export module wrappers.sdl.clipboard;
+
+import wrappers.sdl.error;
+import wrappers.sdl.utils;
+
+export namespace sdl
+{
+ /**
+ * Put UTF-8 text into the clipboard.
+ *
+ * Wrapper for SDL_SetClipboardText().
+ *
+ * Throws an SDLException on failure.
+ */
+ void set_clipboard_text(const std::string& text)
+ {
+ if (!SDL_SetClipboardText(text.c_str())) {
+ throw SDLException("SDL_SetClipboardText");
+ }
+ }
+
+ /**
+ * Get UTF-8 text from the clipboard.
+ *
+ * Wrapper for SDL_GetClipboardText().
+ *
+ * Returns an empty string on failure.
+ */
+ std::string get_clipboard_text()
+ {
+ return wrap_string(SDL_GetClipboardText());
+ }
+}
diff --git a/src/wrappers/sdl/events.cppm b/src/wrappers/sdl/events.cppm
index 7bb2e61..8ef341a 100644
--- a/src/wrappers/sdl/events.cppm
+++ b/src/wrappers/sdl/events.cppm
@@ -1,6 +1,6 @@
module;
-#include
+#include
export module wrappers.sdl.events;
@@ -10,7 +10,7 @@ export namespace sdl
using Event = SDL_Event;
// Alias for EventType enum
- using EventType_t = SDL_EventType;
+ using EventType = SDL_EventType;
/**
* Wrapper for the SDL_EventType enum.
@@ -18,13 +18,30 @@ export namespace sdl
* We're using a namespace here to emulate an enum-like interface, without having to copy the entire enum.
* More constants can be added on demand.
*/
- namespace EventType
+ namespace EventTypes
{
- constexpr EventType_t Quit = SDL_EVENT_QUIT;
- constexpr EventType_t KeyDown = SDL_EVENT_KEY_DOWN;
- constexpr EventType_t KeyUp = SDL_EVENT_KEY_UP;
- constexpr EventType_t MouseMotion = SDL_EVENT_MOUSE_MOTION;
- constexpr EventType_t MouseButtonUp = SDL_EVENT_MOUSE_BUTTON_UP;
- constexpr EventType_t MouseButtonDown = SDL_EVENT_MOUSE_BUTTON_DOWN;
+ // Application events
+ constexpr EventType Quit = SDL_EVENT_QUIT;
+
+ // Window events
+ constexpr EventType WindowMouseLeave = SDL_EVENT_WINDOW_MOUSE_LEAVE;
+ constexpr EventType WindowDisplayScaleChanged = SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED;
+ constexpr EventType WindowPixelSizeChanged = SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED;
+
+ // Keyboard events
+ constexpr EventType KeyDown = SDL_EVENT_KEY_DOWN;
+ constexpr EventType KeyUp = SDL_EVENT_KEY_UP;
+ constexpr EventType TextInput = SDL_EVENT_TEXT_INPUT;
+
+ // Mouse events
+ constexpr EventType MouseMotion = SDL_EVENT_MOUSE_MOTION;
+ constexpr EventType MouseButtonDown = SDL_EVENT_MOUSE_BUTTON_DOWN;
+ constexpr EventType MouseButtonUp = SDL_EVENT_MOUSE_BUTTON_UP;
+ constexpr EventType MouseWheel = SDL_EVENT_MOUSE_WHEEL;
+
+ // Touch events
+ constexpr EventType FingerDown = SDL_EVENT_FINGER_DOWN;
+ constexpr EventType FingerUp = SDL_EVENT_FINGER_UP;
+ constexpr EventType FingerMotion = SDL_EVENT_FINGER_MOTION;
}
}
diff --git a/src/wrappers/sdl/keyboard.cppm b/src/wrappers/sdl/keyboard.cppm
new file mode 100644
index 0000000..bed6d25
--- /dev/null
+++ b/src/wrappers/sdl/keyboard.cppm
@@ -0,0 +1,177 @@
+module;
+
+#include
+#include
+
+export module wrappers.sdl.keyboard;
+
+export namespace sdl
+{
+ using Keycode = SDL_Keycode;
+
+ /**
+ * SDL keycode constants (SDLK_...).
+ * This is not a complete collection of all keycodes, only those we need. Add more when needed.
+ * Namespace is just called "Key" instead of "Keycodes" to keep it short.
+ */
+ namespace Key
+ {
+ // clang-format off
+ constexpr Keycode Unknown = SDLK_UNKNOWN;
+ constexpr Keycode Return = SDLK_RETURN;
+ constexpr Keycode Escape = SDLK_ESCAPE;
+ constexpr Keycode Backspace = SDLK_BACKSPACE;
+ constexpr Keycode Tab = SDLK_TAB;
+ constexpr Keycode Space = SDLK_SPACE;
+ constexpr Keycode DblApostrophe = SDLK_DBLAPOSTROPHE;
+ constexpr Keycode Plus = SDLK_PLUS;
+ constexpr Keycode Comma = SDLK_COMMA;
+ constexpr Keycode Minus = SDLK_MINUS;
+ constexpr Keycode Period = SDLK_PERIOD;
+ constexpr Keycode Slash = SDLK_SLASH;
+ constexpr Keycode Num0 = SDLK_0;
+ constexpr Keycode Num1 = SDLK_1;
+ constexpr Keycode Num2 = SDLK_2;
+ constexpr Keycode Num3 = SDLK_3;
+ constexpr Keycode Num4 = SDLK_4;
+ constexpr Keycode Num5 = SDLK_5;
+ constexpr Keycode Num6 = SDLK_6;
+ constexpr Keycode Num7 = SDLK_7;
+ constexpr Keycode Num8 = SDLK_8;
+ constexpr Keycode Num9 = SDLK_9;
+ constexpr Keycode Semicolon = SDLK_SEMICOLON;
+ constexpr Keycode LeftBracket = SDLK_LEFTBRACKET;
+ constexpr Keycode Backslash = SDLK_BACKSLASH;
+ constexpr Keycode RightBracket = SDLK_RIGHTBRACKET;
+ constexpr Keycode Grave = SDLK_GRAVE;
+ constexpr Keycode A = SDLK_A;
+ constexpr Keycode B = SDLK_B;
+ constexpr Keycode C = SDLK_C;
+ constexpr Keycode D = SDLK_D;
+ constexpr Keycode E = SDLK_E;
+ constexpr Keycode F = SDLK_F;
+ constexpr Keycode G = SDLK_G;
+ constexpr Keycode H = SDLK_H;
+ constexpr Keycode I = SDLK_I;
+ constexpr Keycode J = SDLK_J;
+ constexpr Keycode K = SDLK_K;
+ constexpr Keycode L = SDLK_L;
+ constexpr Keycode M = SDLK_M;
+ constexpr Keycode N = SDLK_N;
+ constexpr Keycode O = SDLK_O;
+ constexpr Keycode P = SDLK_P;
+ constexpr Keycode Q = SDLK_Q;
+ constexpr Keycode R = SDLK_R;
+ constexpr Keycode S = SDLK_S;
+ constexpr Keycode T = SDLK_T;
+ constexpr Keycode U = SDLK_U;
+ constexpr Keycode V = SDLK_V;
+ constexpr Keycode W = SDLK_W;
+ constexpr Keycode X = SDLK_X;
+ constexpr Keycode Y = SDLK_Y;
+ constexpr Keycode Z = SDLK_Z;
+ constexpr Keycode Delete = SDLK_DELETE;
+ constexpr Keycode CapsLock = SDLK_CAPSLOCK;
+ constexpr Keycode F1 = SDLK_F1;
+ constexpr Keycode F2 = SDLK_F2;
+ constexpr Keycode F3 = SDLK_F3;
+ constexpr Keycode F4 = SDLK_F4;
+ constexpr Keycode F5 = SDLK_F5;
+ constexpr Keycode F6 = SDLK_F6;
+ constexpr Keycode F7 = SDLK_F7;
+ constexpr Keycode F8 = SDLK_F8;
+ constexpr Keycode F9 = SDLK_F9;
+ constexpr Keycode F10 = SDLK_F10;
+ constexpr Keycode F11 = SDLK_F11;
+ constexpr Keycode F12 = SDLK_F12;
+ constexpr Keycode PrintScreen = SDLK_PRINTSCREEN;
+ constexpr Keycode ScrollLock = SDLK_SCROLLLOCK;
+ constexpr Keycode Pause = SDLK_PAUSE;
+ constexpr Keycode Insert = SDLK_INSERT;
+ constexpr Keycode Home = SDLK_HOME;
+ constexpr Keycode PageUp = SDLK_PAGEUP;
+ constexpr Keycode End = SDLK_END;
+ constexpr Keycode PageDown = SDLK_PAGEDOWN;
+ constexpr Keycode Right = SDLK_RIGHT;
+ constexpr Keycode Left = SDLK_LEFT;
+ constexpr Keycode Down = SDLK_DOWN;
+ constexpr Keycode Up = SDLK_UP;
+ constexpr Keycode NumLockClear = SDLK_NUMLOCKCLEAR;
+ constexpr Keycode NumpadDivide = SDLK_KP_DIVIDE;
+ constexpr Keycode NumpadMultiply = SDLK_KP_MULTIPLY;
+ constexpr Keycode NumpadMinus = SDLK_KP_MINUS;
+ constexpr Keycode NumpadPlus = SDLK_KP_PLUS;
+ constexpr Keycode NumpadEnter = SDLK_KP_ENTER;
+ constexpr Keycode Numpad1 = SDLK_KP_1;
+ constexpr Keycode Numpad2 = SDLK_KP_2;
+ constexpr Keycode Numpad3 = SDLK_KP_3;
+ constexpr Keycode Numpad4 = SDLK_KP_4;
+ constexpr Keycode Numpad5 = SDLK_KP_5;
+ constexpr Keycode Numpad6 = SDLK_KP_6;
+ constexpr Keycode Numpad7 = SDLK_KP_7;
+ constexpr Keycode Numpad8 = SDLK_KP_8;
+ constexpr Keycode Numpad9 = SDLK_KP_9;
+ constexpr Keycode Numpad0 = SDLK_KP_0;
+ constexpr Keycode NumpadPeriod = SDLK_KP_PERIOD;
+ constexpr Keycode NumpadEquals = SDLK_KP_EQUALS;
+ constexpr Keycode F13 = SDLK_F13;
+ constexpr Keycode F14 = SDLK_F14;
+ constexpr Keycode F15 = SDLK_F15;
+ constexpr Keycode Help = SDLK_HELP;
+ constexpr Keycode Clear = SDLK_CLEAR;
+ constexpr Keycode LCtrl = SDLK_LCTRL;
+ constexpr Keycode LShift = SDLK_LSHIFT;
+ constexpr Keycode LAlt = SDLK_LALT;
+ constexpr Keycode LGui = SDLK_LGUI;
+ constexpr Keycode RCtrl = SDLK_RCTRL;
+ constexpr Keycode RShift = SDLK_RSHIFT;
+ constexpr Keycode RAlt = SDLK_RALT;
+ constexpr Keycode RGui = SDLK_RGUI;
+ // clang-format on
+ }
+
+ // Key modifier (e.g. left shift, left ctrl, ...)
+ using KeyMod = SDL_Keymod;
+
+ // Key modifier state: Bitmask of key modifiers (just an alias for KeyMod for slightly better semantics)
+ using KeyModState = KeyMod;
+
+ /**
+ * SDL key modifier constants (SDL_KMOD_...).
+ * This is not a complete collection of all key modifiers, only those we need. Add more when needed.
+ */
+ namespace KeyMods
+ {
+ // clang-format off
+
+ // Single modifiers
+ constexpr KeyMod None = SDL_KMOD_NONE;
+ constexpr KeyMod LShift = SDL_KMOD_LSHIFT;
+ constexpr KeyMod RShift = SDL_KMOD_RSHIFT;
+ constexpr KeyMod LCtrl = SDL_KMOD_LCTRL;
+ constexpr KeyMod RCtrl = SDL_KMOD_RCTRL;
+ constexpr KeyMod LAlt = SDL_KMOD_LALT;
+ constexpr KeyMod RAlt = SDL_KMOD_RALT;
+ constexpr KeyMod LGui = SDL_KMOD_LGUI;
+ constexpr KeyMod RGui = SDL_KMOD_RGUI;
+ constexpr KeyMod NumLock = SDL_KMOD_NUM;
+ constexpr KeyMod CapsLock = SDL_KMOD_CAPS;
+
+ // Combined modifiers
+ constexpr KeyModState Ctrl = SDL_KMOD_CTRL; // Any Ctrl key
+ constexpr KeyModState Shift = SDL_KMOD_SHIFT; // Any Shift key
+ constexpr KeyModState Alt = SDL_KMOD_ALT; // Any Alt key
+ constexpr KeyModState Gui = SDL_KMOD_GUI; // Any GUI key
+
+ // clang-format on
+ }
+
+ /**
+ * Get the current key modifier state for the keyboard.
+ * Wrapper for SDL_GetModState().
+ */
+ constexpr KeyModState get_key_mod_state()
+ {
+ return SDL_GetModState();
+ }
+}
diff --git a/src/wrappers/sdl/mouse.cppm b/src/wrappers/sdl/mouse.cppm
new file mode 100644
index 0000000..a9671f2
--- /dev/null
+++ b/src/wrappers/sdl/mouse.cppm
@@ -0,0 +1,27 @@
+module;
+
+#include
+#include
+
+export module wrappers.sdl.mouse;
+
+export namespace sdl
+{
+ using MouseButtonIndex = uint8_t;
+
+ namespace MouseButtons
+ {
+ constexpr MouseButtonIndex Left = SDL_BUTTON_LEFT;
+ constexpr MouseButtonIndex Middle = SDL_BUTTON_MIDDLE;
+ constexpr MouseButtonIndex Right = SDL_BUTTON_RIGHT;
+ }
+
+ /**
+ * Wrapper for SDL_CaptureMouse().
+ */
+ constexpr void capture_mouse(const bool enabled)
+ {
+ // Ignore errors (probably means it's not supported on this platform)
+ SDL_CaptureMouse(enabled);
+ }
+}
diff --git a/src/wrappers/sdl/rect.cppm b/src/wrappers/sdl/rect.cppm
index dbe8c10..6e5dd32 100644
--- a/src/wrappers/sdl/rect.cppm
+++ b/src/wrappers/sdl/rect.cppm
@@ -1,6 +1,6 @@
module;
-#include
+#include
export module wrappers.sdl.rect;
@@ -11,12 +11,12 @@ export namespace sdl
using Point = SDL_Point;
/**
- * Wrapper around SDL_FRect using class inheritance.
+ * Wrapper for SDL_FRect using class inheritance.
*/
class FRect : public SDL_FRect
{
/**
- * Wrapper around SDL_RectEmptyFloat().
+ * Wrapper for SDL_RectEmptyFloat().
* @return True if the floating point rectangle takes no space.
*/
bool is_empty() const
@@ -26,12 +26,12 @@ export namespace sdl
};
/**
- * Wrapper around SDL_Rect using class inheritance.
+ * Wrapper for SDL_Rect using class inheritance.
*/
class Rect : public SDL_Rect
{
/**
- * Wrapper around SDL_RectEmpty().
+ * Wrapper for SDL_RectEmpty().
* @return True if the rectangle takes no space.
*/
bool is_empty() const
diff --git a/src/wrappers/sdl/render.cppm b/src/wrappers/sdl/render.cppm
index 0cf34e1..a7582dd 100644
--- a/src/wrappers/sdl/render.cppm
+++ b/src/wrappers/sdl/render.cppm
@@ -13,8 +13,10 @@ import wrappers.sdl.video;
export namespace sdl
{
+ using Vertex = SDL_Vertex;
+
/**
- * Wrapper around SDL_Texture that manages its lifecycle with a unique_ptr.
+ * Wrapper for SDL_Texture that manages its lifecycle with a unique_ptr.
*/
class Texture
{
@@ -53,7 +55,7 @@ export namespace sdl
};
/**
- * Wrapper around SDL_Renderer that manages its lifecycle with a unique_ptr.
+ * Wrapper for SDL_Renderer that manages its lifecycle with a unique_ptr.
*/
class Renderer
{
@@ -76,7 +78,41 @@ export namespace sdl
}
/**
- * Wrapper around SDL_RenderClear().
+ * Wrapper for SDL_SetRenderClipRect().
+ * Uses assert to verify the function returned true.
+ */
+ constexpr void set_clip_rect(const Rect* rect) const
+ {
+ if (!SDL_SetRenderClipRect(get_raw(), rect)) {
+ assert(!"SDL_SetRenderClipRect returned false");
+ }
+ }
+
+ /**
+ * Wrapper for SDL_SetRenderDrawBlendMode().
+ * Uses assert to verify the function returned true.
+ */
+ constexpr void set_draw_blend_mode(SDL_BlendMode blend_mode) const
+ {
+ if (!SDL_SetRenderDrawBlendMode(get_raw(), blend_mode)) {
+ assert(!"SDL_SetRenderDrawBlendMode returned false");
+ }
+ }
+
+ /**
+ * Wrapper for SDL_SetRenderVSync().
+ * Uses assert to verify the function returned true.
+ */
+ constexpr void set_vsync(const int vsync) const
+ {
+ // TODO: These functions probably should never fail? Change this to a runtime exception if necessary.
+ if (!SDL_SetRenderVSync(get_raw(), vsync)) {
+ assert(!"SDL_SetRenderVSync returned false");
+ }
+ }
+
+ /**
+ * Wrapper for SDL_RenderClear().
* Uses assert to verify the function returned true.
*/
constexpr void clear() const
@@ -88,7 +124,7 @@ export namespace sdl
}
/**
- * Wrapper around SDL_RenderPresent().
+ * Wrapper for SDL_RenderPresent().
* Uses assert to verify the function returned true.
*/
constexpr void present() const
@@ -99,7 +135,7 @@ export namespace sdl
}
/**
- * Wrapper around SDL_RenderTexture().
+ * Wrapper for SDL_RenderTexture().
* Uses assert to verify the function returned true.
*/
constexpr void render_texture(
@@ -112,10 +148,28 @@ export namespace sdl
assert(!"SDL_RenderTexture returned false");
}
}
+
+ /**
+ * Wrapper for SDL_RenderGeometry().
+ * Uses assert to verify the function returned true.
+ */
+ // TODO: Function uses raw SDL_Texture pointer. Add overload for sdl::Texture& when needed.
+ constexpr void render_geometry(
+ SDL_Texture* texture,
+ const Vertex* vertices,
+ const int num_vertices,
+ const int* indices,
+ const int num_indices
+ ) const
+ {
+ if (!SDL_RenderGeometry(get_raw(), texture, vertices, num_vertices, indices, num_indices)) {
+ assert(!"SDL_RenderGeometry returned false");
+ }
+ }
};
/**
- * Wrapper around SDL_CreateWindowAndRenderer().
+ * Wrapper for SDL_CreateWindowAndRenderer().
* Returns a pair of a Window and a Renderer object.
* Throws an SDLException on failure.
*/
diff --git a/src/wrappers/sdl/surface.cppm b/src/wrappers/sdl/surface.cppm
new file mode 100644
index 0000000..c48d8c3
--- /dev/null
+++ b/src/wrappers/sdl/surface.cppm
@@ -0,0 +1,98 @@
+module;
+
+#include
+#include
+#include
+
+export module wrappers.sdl.surface;
+
+import utils.memory;
+import wrappers.sdl.error;
+
+export namespace sdl
+{
+ /**
+ * Wrapper for SDL_Texture that manages its lifecycle with a unique_ptr.
+ */
+ class Surface
+ {
+ std::unique_ptr<
+ SDL_Surface,
+ utils::FuncDeleter
+ > raw_surface_;
+
+ public:
+ Surface() = delete;
+
+ explicit Surface(SDL_Surface* raw_surface)
+ : raw_surface_{raw_surface}
+ {}
+
+ /**
+ * Allocate a new surface with a specific pixel format and existing pixel data.
+ *
+ * NOTE: No copy is made of the pixel data. Pixel data is not managed automatically; you must free the
+ * surface before you free the pixel data.
+ *
+ * Wrapper for SDL_CreateSurfaceFrom().
+ */
+ static Surface create_from(
+ const int width,
+ const int height,
+ const SDL_PixelFormat format,
+ void* pixels,
+ const int pitch
+ )
+ {
+ SDL_Surface* raw_surface = SDL_CreateSurfaceFrom(width, height, format, pixels, pitch);
+
+ if (raw_surface == nullptr) {
+ throw SDLException("SDL_CreateSurfaceFrom");
+ }
+
+ return Surface(raw_surface);
+ }
+
+ constexpr SDL_Surface* get_raw() const
+ {
+ assert(raw_surface_);
+ return raw_surface_.get();
+ }
+
+ // TODO: Wrap SDL_PixelFormat?
+ constexpr SDL_PixelFormat get_format() const
+ {
+ return raw_surface_->format;
+ }
+
+ constexpr int get_width() const
+ {
+ return raw_surface_->w;
+ }
+
+ constexpr int get_height() const
+ {
+ return raw_surface_->h;
+ }
+
+ constexpr void* get_raw_pixels() const
+ {
+ return raw_surface_->pixels;
+ }
+
+ /**
+ * Wrapper for SDL_ConvertSurface().
+ */
+ Surface convert_format(const SDL_PixelFormat new_format) const
+ {
+ SDL_Surface* new_raw_surface = SDL_ConvertSurface(get_raw(), new_format);
+
+ // TODO: When does this function fail?
+ if (new_raw_surface == nullptr) {
+ throw SDLException("SDL_ConvertSurface");
+ }
+
+ return Surface(new_raw_surface);
+ }
+ };
+}
diff --git a/src/wrappers/sdl/utils.cppm b/src/wrappers/sdl/utils.cppm
new file mode 100644
index 0000000..c544c51
--- /dev/null
+++ b/src/wrappers/sdl/utils.cppm
@@ -0,0 +1,22 @@
+module;
+
+#include
+#include
+
+export module wrappers.sdl.utils;
+
+export namespace sdl
+{
+ /**
+ * Convert a string returned by an SDL function to std::string and free the original memory using SDL_free().
+ *
+ * Usage:
+ * std::string foo = wrap_string(SDL_FunctionThatReturnsAString());
+ */
+ constexpr std::string wrap_string(char* raw_str)
+ {
+ std::string str{raw_str};
+ SDL_free(raw_str);
+ return str;
+ }
+}
diff --git a/src/wrappers/sdl/video.cppm b/src/wrappers/sdl/video.cppm
index f80c93f..4141b01 100644
--- a/src/wrappers/sdl/video.cppm
+++ b/src/wrappers/sdl/video.cppm
@@ -7,11 +7,13 @@ module;
export module wrappers.sdl.video;
import utils.memory;
+import wrappers.sdl.error;
+import wrappers.sdl.rect;
export namespace sdl
{
/**
- * Wrapper around SDL_Window that manages its lifecycle with a unique_ptr.
+ * Wrapper for SDL_Window that manages its lifecycle with a unique_ptr.
*/
class Window
{
@@ -32,5 +34,68 @@ export namespace sdl
assert(raw_window_);
return raw_window_.get();
}
+
+ /**
+ * Wrapper for SDL_GetWindowPixelDensity().
+ */
+ constexpr float get_pixel_density() const
+ {
+ return SDL_GetWindowPixelDensity(get_raw());
+ }
+
+ /**
+ * Wrapper for SDL_GetWindowDisplayScale().
+ */
+ constexpr float get_display_scale() const
+ {
+ return SDL_GetWindowDisplayScale(get_raw());
+ }
+
+ /**
+ * Wrapper for SDL_GetWindowSize().
+ */
+ std::pair get_size() const
+ {
+ int width, height;
+
+ if (!SDL_GetWindowSize(get_raw(), &width, &height)) {
+ throw SDLException("SDL_GetWindowSize");
+ }
+
+ return {width, height};
+ }
+
+ /**
+ * Wrapper for SDL_SetTextInputArea().
+ * Uses assert to verify the function returned true.
+ */
+ constexpr void set_text_input_area(const Rect* rect, const int cursor) const
+ {
+ if (!SDL_SetTextInputArea(get_raw(), rect, cursor)) {
+ assert(!"SDL_SetTextInputArea returned false");
+ }
+ }
+
+ /**
+ * Wrapper for SDL_StartTextInput().
+ * Uses assert to verify the function returned true.
+ */
+ constexpr void start_text_input() const
+ {
+ if (!SDL_StartTextInput(get_raw())) {
+ assert(!"SDL_StartTextInput returned false");
+ }
+ }
+
+ /**
+ * Wrapper for SDL_StopTextInput().
+ * Uses assert to verify the function returned true.
+ */
+ constexpr void stop_text_input() const
+ {
+ if (!SDL_StopTextInput(get_raw())) {
+ assert(!"SDL_StopTextInput returned false");
+ }
+ }
};
}
diff --git a/src/wrappers/sdl_image.cppm b/src/wrappers/sdl_image.cppm
index a434564..5418c93 100644
--- a/src/wrappers/sdl_image.cppm
+++ b/src/wrappers/sdl_image.cppm
@@ -9,20 +9,35 @@ export module wrappers.sdl_image;
import utils.memory;
import wrappers.sdl.error;
import wrappers.sdl.render;
+import wrappers.sdl.surface;
export namespace sdl_image
{
/**
- * Wrapper around IMG_LoadTexture().
+ * Wrapper for IMG_Load().
*/
- sdl::Texture LoadTexture(const sdl::Renderer& renderer, const std::string& filename)
+ sdl::Surface load(const std::string& filename)
{
- SDL_Texture* sdl_texture = IMG_LoadTexture(renderer.get_raw(), filename.c_str());
+ SDL_Surface* raw_surface = IMG_Load(filename.c_str());
- if (sdl_texture == nullptr) {
+ if (raw_surface == nullptr) {
+ throw sdl::SDLException("IMG_Load");
+ }
+
+ return sdl::Surface{raw_surface};
+ }
+
+ /**
+ * Wrapper for IMG_LoadTexture().
+ */
+ sdl::Texture load_texture(const sdl::Renderer& renderer, const std::string& filename)
+ {
+ SDL_Texture* raw_texture = IMG_LoadTexture(renderer.get_raw(), filename.c_str());
+
+ if (raw_texture == nullptr) {
throw sdl::SDLException("IMG_LoadTexture");
}
- return sdl::Texture{sdl_texture};
+ return sdl::Texture{raw_texture};
}
}
diff --git a/vendor/README.md b/vendor/README.md
new file mode 100644
index 0000000..c729343
--- /dev/null
+++ b/vendor/README.md
@@ -0,0 +1,30 @@
+# Vendor dependencies
+
+TODO: This should be automated with shuriken at some point. For now, manual instructions.
+
+
+## System dependencies
+
+For now, SDL3 and SDL3_image need to be installed on the system. They should be vendored at some point.
+
+Same for freetype, which is required by RmlUi.
+
+
+## RmlUi
+
+[RmlUi](https://github.com/mikke89/RmlUi) is a UI library.
+
+Within the `vendor` directory, run:
+
+```shell
+# Either clone git repository or download and unpack archive (version 6.2)
+git clone git@github.com:mikke89/RmlUi.git RmlUi --branch 6.2
+cd RmlUi
+
+# Configure and build with cmake (use samples or standalone preset)
+cmake -G Ninja -B Build --preset samples -DBUILD_SHARED_LIBS=false -DRMLUI_BACKEND=SDL_SDLrenderer
+cmake --build Build
+
+# Optionally test by running a sample
+./Build/rmlui_sample_demo
+```