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 +```