From 8039b1276e65ad51afe83123d054d30563f8595c Mon Sep 17 00:00:00 2001 From: binaryDiv Date: Tue, 28 Apr 2026 23:28:53 +0200 Subject: [PATCH 1/7] Refactor: Move code to rutile module and namespace --- src/main.cpp | 10 +++--- src/{ => rutile}/app.cppm | 18 +++++------ src/{ => rutile}/config.cppm | 4 +-- src/{ => rutile}/core/drawing/sprite.cppm | 6 ++-- src/{ => rutile}/core/drawing/tile_map.cppm | 8 ++--- src/{ => rutile}/core/drawing/tile_set.cppm | 6 ++-- src/{ => rutile}/core/engine.cppm | 8 ++--- src/{ => rutile}/core/render_server.cppm | 8 ++--- src/{ => rutile}/core/resource_manager.cppm | 4 +-- src/{ => rutile}/core/texture_loader.cppm | 6 ++-- src/{ => rutile}/game/game.cppm | 36 ++++++++++----------- src/wrappers/sdl/rect.cppm | 4 +-- 12 files changed, 59 insertions(+), 59 deletions(-) rename src/{ => rutile}/app.cppm (89%) rename src/{ => rutile}/config.cppm (93%) rename src/{ => rutile}/core/drawing/sprite.cppm (96%) rename src/{ => rutile}/core/drawing/tile_map.cppm (96%) rename src/{ => rutile}/core/drawing/tile_set.cppm (96%) rename src/{ => rutile}/core/engine.cppm (95%) rename src/{ => rutile}/core/render_server.cppm (94%) rename src/{ => rutile}/core/resource_manager.cppm (98%) rename src/{ => rutile}/core/texture_loader.cppm (88%) rename src/{ => rutile}/game/game.cppm (84%) diff --git a/src/main.cpp b/src/main.cpp index 4a0a4fc..46d3c3e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,30 +1,30 @@ -import app; +import rutile.app; #define SDL_MAIN_USE_CALLBACKS #include SDL_AppResult SDL_AppInit(void** appstate, const int /*argc*/, char** /*argv*/) { - auto* app = new App; + auto* app = new rutile::App; *appstate = app; return static_cast(app->initialize()); } SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) { - auto* app = static_cast(appstate); + auto* app = static_cast(appstate); return static_cast(app->handle_event(event)); } SDL_AppResult SDL_AppIterate(void* appstate) { - auto* app = static_cast(appstate); + auto* app = static_cast(appstate); return static_cast(app->iterate()); } void SDL_AppQuit(void* appstate, const SDL_AppResult /*result*/) { - const auto* app = static_cast(appstate); + const auto* app = static_cast(appstate); app->shutdown(); delete app; } diff --git a/src/app.cppm b/src/rutile/app.cppm similarity index 89% rename from src/app.cppm rename to src/rutile/app.cppm index 7b59948..df7ab6a 100644 --- a/src/app.cppm +++ b/src/rutile/app.cppm @@ -4,19 +4,19 @@ module; #include #include -export module app; +export module rutile.app; -import config; -import core.engine; -import game.game; +import rutile.config; +import rutile.core.engine; +import rutile.game.game; import wrappers.sdl; -export +export namespace rutile { class App { - std::unique_ptr engine_{nullptr}; - std::unique_ptr game_{nullptr}; + std::unique_ptr engine_{nullptr}; + std::unique_ptr game_{nullptr}; public: App() = default; @@ -37,8 +37,8 @@ export sdl::Init(sdl::InitFlags::Video | sdl::InitFlags::Events); // Create engine (includes window and renderer) and game state - engine_ = core::Engine::create(); - game_ = game::Game::create(*engine_); + engine_ = Engine::create(); + game_ = Game::create(*engine_); } catch (const std::runtime_error& e) { std::cerr << "Unhandled exception during initialization: " << e.what() << '\n'; diff --git a/src/config.cppm b/src/rutile/config.cppm similarity index 93% rename from src/config.cppm rename to src/rutile/config.cppm index 0ed9a38..b5dd5ce 100644 --- a/src/config.cppm +++ b/src/rutile/config.cppm @@ -8,9 +8,9 @@ module; #define DEBUG_BOOL true #endif -export module config; +export module rutile.config; -export namespace config +export namespace rutile::config { constexpr auto debug = DEBUG_BOOL; diff --git a/src/core/drawing/sprite.cppm b/src/rutile/core/drawing/sprite.cppm similarity index 96% rename from src/core/drawing/sprite.cppm rename to src/rutile/core/drawing/sprite.cppm index 31224cd..6835962 100644 --- a/src/core/drawing/sprite.cppm +++ b/src/rutile/core/drawing/sprite.cppm @@ -4,12 +4,12 @@ module; #include #include -export module core.drawing.sprite; +export module rutile.core.drawing.sprite; -import core.render_server; +import rutile.core.render_server; import wrappers.sdl; -export namespace core +export namespace rutile { class Sprite { diff --git a/src/core/drawing/tile_map.cppm b/src/rutile/core/drawing/tile_map.cppm similarity index 96% rename from src/core/drawing/tile_map.cppm rename to src/rutile/core/drawing/tile_map.cppm index 31bfbfa..2fa9111 100644 --- a/src/core/drawing/tile_map.cppm +++ b/src/rutile/core/drawing/tile_map.cppm @@ -4,13 +4,13 @@ module; #include #include -export module core.drawing.tile_map; +export module rutile.core.drawing.tile_map; -import core.drawing.tile_set; -import core.render_server; +import rutile.core.drawing.tile_set; +import rutile.core.render_server; import wrappers.sdl; -export namespace core +export namespace rutile { // Restrict map size to 16 bit per dimension, so that width*height still fits within size_t using MapCoord = uint16_t; diff --git a/src/core/drawing/tile_set.cppm b/src/rutile/core/drawing/tile_set.cppm similarity index 96% rename from src/core/drawing/tile_set.cppm rename to src/rutile/core/drawing/tile_set.cppm index 39ba81d..6fcbec5 100644 --- a/src/core/drawing/tile_set.cppm +++ b/src/rutile/core/drawing/tile_set.cppm @@ -5,12 +5,12 @@ module; #include #include -export module core.drawing.tile_set; +export module rutile.core.drawing.tile_set; -import core.render_server; +import rutile.core.render_server; import wrappers.sdl; -export namespace core +export namespace rutile { using TileSetID = unsigned int; using TileID = unsigned int; diff --git a/src/core/engine.cppm b/src/rutile/core/engine.cppm similarity index 95% rename from src/core/engine.cppm rename to src/rutile/core/engine.cppm index d9bd5ad..de1f8ad 100644 --- a/src/core/engine.cppm +++ b/src/rutile/core/engine.cppm @@ -3,13 +3,13 @@ module; #include #include -export module core.engine; +export module rutile.core.engine; -import config; -import core.render_server; +import rutile.config; +import rutile.core.render_server; import wrappers.sdl; -export namespace core +export namespace rutile { class Engine { diff --git a/src/core/render_server.cppm b/src/rutile/core/render_server.cppm similarity index 94% rename from src/core/render_server.cppm rename to src/rutile/core/render_server.cppm index bcb4c9f..2420d3a 100644 --- a/src/core/render_server.cppm +++ b/src/rutile/core/render_server.cppm @@ -3,13 +3,13 @@ module; #include #include -export module core.render_server; +export module rutile.core.render_server; -import core.resource_manager; -import core.texture_loader; +import rutile.core.resource_manager; +import rutile.core.texture_loader; import wrappers.sdl; -export namespace core +export namespace rutile { using TextureID = unsigned int; using TextureManager = ResourceManager; diff --git a/src/core/resource_manager.cppm b/src/rutile/core/resource_manager.cppm similarity index 98% rename from src/core/resource_manager.cppm rename to src/rutile/core/resource_manager.cppm index 2e947f5..dddac0d 100644 --- a/src/core/resource_manager.cppm +++ b/src/rutile/core/resource_manager.cppm @@ -6,9 +6,9 @@ module; #include #include -export module core.resource_manager; +export module rutile.core.resource_manager; -export namespace core +export namespace rutile { template concept IsResourceLoader = requires(ResourceLoaderType loader, const std::string& name) diff --git a/src/core/texture_loader.cppm b/src/rutile/core/texture_loader.cppm similarity index 88% rename from src/core/texture_loader.cppm rename to src/rutile/core/texture_loader.cppm index 32548c4..04c5a83 100644 --- a/src/core/texture_loader.cppm +++ b/src/rutile/core/texture_loader.cppm @@ -2,13 +2,13 @@ module; #include -export module core.texture_loader; +export module rutile.core.texture_loader; -import core.resource_manager; +import rutile.core.resource_manager; import wrappers.sdl; import wrappers.sdl_image; -export namespace core +export namespace rutile { class TextureLoader { diff --git a/src/game/game.cppm b/src/rutile/game/game.cppm similarity index 84% rename from src/game/game.cppm rename to src/rutile/game/game.cppm index a18cc6f..e788c34 100644 --- a/src/game/game.cppm +++ b/src/rutile/game/game.cppm @@ -5,17 +5,17 @@ module; #include #include -export module game.game; +export module rutile.game.game; -import core.drawing.sprite; -import core.drawing.tile_map; -import core.drawing.tile_set; -import core.engine; -import core.render_server; +import rutile.core.drawing.sprite; +import rutile.core.drawing.tile_map; +import rutile.core.drawing.tile_set; +import rutile.core.engine; +import rutile.core.render_server; import wrappers.sdl; import wrappers.sdl_image; -export namespace game +export namespace rutile { class Game { @@ -23,21 +23,21 @@ export namespace game static bool instantiated_; // Reference to the engine - core::Engine& engine_; + Engine& engine_; // Sprites for testing - core::Sprite player_sprite_; - std::vector sprites_; + Sprite player_sprite_; + std::vector sprites_; // Tile set and tile map (TODO: tile set should be moved to resource manager in engine) - core::TileSet tile_set_; - core::TileMap tile_map_; + TileSet tile_set_; + TileMap tile_map_; // Private constructor - explicit Game(core::Engine& engine) + explicit Game(Engine& engine) : engine_(engine), player_sprite_{ - core::Sprite::create_from_texture( + Sprite::create_from_texture( engine_.get_render_server(), "assets/sprites/neocat_64.png", std::nullopt, @@ -46,7 +46,7 @@ export namespace game ) }, tile_set_{ - core::TileSet::create_from_texture( + TileSet::create_from_texture( engine_.get_render_server(), "assets/tilesets/terrain.png", 32, @@ -86,7 +86,7 @@ export namespace game instantiated_ = false; } - static std::unique_ptr create(core::Engine& engine) + static std::unique_ptr create(Engine& engine) { // Prevent the class from being instantiated multiple times assert(!instantiated_); @@ -105,7 +105,7 @@ export namespace game if (event->type == sdl::EventType::MouseButtonUp) { sprites_.push_back( - core::Sprite::create_from_texture( + Sprite::create_from_texture( engine_.get_render_server(), "assets/sprites/neofox_64.png", std::nullopt, @@ -132,7 +132,7 @@ export namespace game tile_map_.draw(render_server, tile_set_); // Render sprites - for (const core::Sprite& sprite : sprites_) { + for (const Sprite& sprite : sprites_) { sprite.draw(render_server); } player_sprite_.draw(render_server); diff --git a/src/wrappers/sdl/rect.cppm b/src/wrappers/sdl/rect.cppm index a42648a..dbe8c10 100644 --- a/src/wrappers/sdl/rect.cppm +++ b/src/wrappers/sdl/rect.cppm @@ -19,7 +19,7 @@ export namespace sdl * Wrapper around SDL_RectEmptyFloat(). * @return True if the floating point rectangle takes no space. */ - constexpr bool is_empty() const + bool is_empty() const { return SDL_RectEmptyFloat(this); } @@ -34,7 +34,7 @@ export namespace sdl * Wrapper around SDL_RectEmpty(). * @return True if the rectangle takes no space. */ - constexpr bool is_empty() const + bool is_empty() const { return SDL_RectEmpty(this); } From 7f37be386dc69c06d8c9110302df80566747245b Mon Sep 17 00:00:00 2001 From: binaryDiv Date: Sun, 10 May 2026 23:23:40 +0200 Subject: [PATCH 2/7] Implement UIs using RmlUi --- .gitignore | 4 + assets/fonts/LatoLatin-Regular.ttf | Bin 0 -> 148540 bytes assets/ui/base.rcss | 88 ++++++ assets/ui/main_ui.rml | 16 + shuriken.yaml | 14 +- src/main.cpp | 2 +- src/rutile/app.cppm | 4 +- src/rutile/core/engine.cppm | 22 +- src/rutile/core/render_server.cppm | 4 +- src/rutile/core/texture_loader.cppm | 2 +- .../core/ui/backend/rmlui_input_handler.cppm | 278 ++++++++++++++++++ .../ui/backend/rmlui_render_interface.cppm | 199 +++++++++++++ .../ui/backend/rmlui_system_interface.cppm | 118 ++++++++ src/rutile/core/ui/ui_server.cppm | 124 ++++++++ src/rutile/game/game.cppm | 24 +- src/wrappers/sdl.cppm | 5 + src/wrappers/sdl/clipboard.cppm | 38 +++ src/wrappers/sdl/events.cppm | 35 ++- src/wrappers/sdl/keyboard.cppm | 177 +++++++++++ src/wrappers/sdl/mouse.cppm | 27 ++ src/wrappers/sdl/rect.cppm | 10 +- src/wrappers/sdl/render.cppm | 66 ++++- src/wrappers/sdl/surface.cppm | 98 ++++++ src/wrappers/sdl/utils.cppm | 22 ++ src/wrappers/sdl/video.cppm | 67 ++++- src/wrappers/sdl_image.cppm | 25 +- vendor/README.md | 30 ++ 27 files changed, 1457 insertions(+), 42 deletions(-) create mode 100644 assets/fonts/LatoLatin-Regular.ttf create mode 100644 assets/ui/base.rcss create mode 100644 assets/ui/main_ui.rml create mode 100644 src/rutile/core/ui/backend/rmlui_input_handler.cppm create mode 100644 src/rutile/core/ui/backend/rmlui_render_interface.cppm create mode 100644 src/rutile/core/ui/backend/rmlui_system_interface.cppm create mode 100644 src/rutile/core/ui/ui_server.cppm create mode 100644 src/wrappers/sdl/clipboard.cppm create mode 100644 src/wrappers/sdl/keyboard.cppm create mode 100644 src/wrappers/sdl/mouse.cppm create mode 100644 src/wrappers/sdl/surface.cppm create mode 100644 src/wrappers/sdl/utils.cppm create mode 100644 vendor/README.md 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 0000000000000000000000000000000000000000..bcc57780d8f007bfc2106a89876f44cb56936e6b GIT binary patch literal 148540 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%0A9r7 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 tL>|gwg1_U;V;Qb + + + + + +

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 +``` From 7b8aac69fb876391b10b170eb6f1e665137e2564 Mon Sep 17 00:00:00 2001 From: binaryDiv Date: Sun, 10 May 2026 23:33:59 +0200 Subject: [PATCH 3/7] Refactoring: Rename SDL wrapper funcs to snake_case --- src/rutile/app.cppm | 16 ++++++++-------- src/rutile/core/engine.cppm | 2 +- src/wrappers/sdl/error.cppm | 2 +- src/wrappers/sdl/init.cppm | 12 ++++++------ src/wrappers/sdl/render.cppm | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/rutile/app.cppm b/src/rutile/app.cppm index a3059d5..2eea271 100644 --- a/src/rutile/app.cppm +++ b/src/rutile/app.cppm @@ -25,16 +25,16 @@ export namespace rutile { try { // Set SDL application metadata - sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING, config::app_name); - sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_VERSION_STRING, config::app_version); - sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_IDENTIFIER_STRING, config::app_identifier); - sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_CREATOR_STRING, config::app_creator); - sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_COPYRIGHT_STRING, config::app_copyright); - sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_URL_STRING, config::app_url); - sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_TYPE_STRING, "game"); + sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_NAME_STRING, config::app_name); + sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_VERSION_STRING, config::app_version); + sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_IDENTIFIER_STRING, config::app_identifier); + sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_CREATOR_STRING, config::app_creator); + sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_COPYRIGHT_STRING, config::app_copyright); + sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_URL_STRING, config::app_url); + sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_TYPE_STRING, "game"); // Initialize SDL subsystems - sdl::Init(sdl::InitFlags::Video | sdl::InitFlags::Events); + sdl::initialize_sdl(sdl::InitFlags::Video | sdl::InitFlags::Events); // Create engine (includes window and renderer) and game state engine_ = Engine::create(); diff --git a/src/rutile/core/engine.cppm b/src/rutile/core/engine.cppm index 7fec209..680d382 100644 --- a/src/rutile/core/engine.cppm +++ b/src/rutile/core/engine.cppm @@ -52,7 +52,7 @@ export namespace rutile // Prevent the class from being instantiated multiple times assert(!instantiated_); - auto [sdl_window, sdl_renderer] = sdl::CreateWindowAndRenderer( + auto [sdl_window, sdl_renderer] = sdl::create_window_and_renderer( config::get_window_title(), config::window_width, config::window_height, diff --git a/src/wrappers/sdl/error.cppm b/src/wrappers/sdl/error.cppm index ceaedf0..c4f23df 100644 --- a/src/wrappers/sdl/error.cppm +++ b/src/wrappers/sdl/error.cppm @@ -2,7 +2,7 @@ module; #include #include -#include +#include export module wrappers.sdl.error; diff --git a/src/wrappers/sdl/init.cppm b/src/wrappers/sdl/init.cppm index 9044776..9e3653a 100644 --- a/src/wrappers/sdl/init.cppm +++ b/src/wrappers/sdl/init.cppm @@ -1,7 +1,7 @@ module; #include -#include +#include export module wrappers.sdl.init; @@ -31,7 +31,7 @@ export namespace sdl } /** - * Wrapper around the SDL_AppResult enum. + * Wrapper for the SDL_AppResult enum. */ enum class AppResult : std::underlying_type_t { @@ -41,9 +41,9 @@ export namespace sdl }; /** - * Wrapper around SDL_Init(). + * Wrapper for SDL_Init(). */ - constexpr void Init(const InitFlags_t flags) + constexpr void initialize_sdl(const InitFlags_t flags) { if (!SDL_Init(flags)) { throw SDLException("SDL_Init"); @@ -51,9 +51,9 @@ export namespace sdl } /** - * Wrapper around SDL_SetAppMetadataProperty(). + * Wrapper for SDL_SetAppMetadataProperty(). */ - constexpr void SetAppMetadataProperty(const char* property_name, const char* value) + constexpr void set_app_metadata_property(const char* property_name, const char* value) { if (!SDL_SetAppMetadataProperty(property_name, value)) { throw SDLException("SDL_SetAppMetadataProperty"); diff --git a/src/wrappers/sdl/render.cppm b/src/wrappers/sdl/render.cppm index a7582dd..0ea45a9 100644 --- a/src/wrappers/sdl/render.cppm +++ b/src/wrappers/sdl/render.cppm @@ -173,7 +173,7 @@ export namespace sdl * Returns a pair of a Window and a Renderer object. * Throws an SDLException on failure. */ - std::pair CreateWindowAndRenderer( + std::pair create_window_and_renderer( const std::string& title, const int width, const int height, From d256de19c50146f896cfd46a23dc29d3875c3f7f Mon Sep 17 00:00:00 2001 From: binaryDiv Date: Mon, 11 May 2026 01:41:25 +0200 Subject: [PATCH 4/7] shuriken: Add lint command --- tools/shuriken.py | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/tools/shuriken.py b/tools/shuriken.py index e1ae8aa..fcb6402 100755 --- a/tools/shuriken.py +++ b/tools/shuriken.py @@ -3,7 +3,7 @@ ############################################ # shuriken - A ninja-based C++ build tool # ------------------------------------------ -# Version: 0.1.0 +# Version: 0.1.0 (modified) # Author: binaryDiv # License: MIT License # https://git.0xbd.space/binaryDiv/shuriken @@ -186,8 +186,8 @@ class ShurikenArgumentParser: help='Print generated Ninja file to stdout instead of writing to a file', ) - subparser_build = subparsers.add_parser('build', help='Build project (default command)') - subparser_compdb = subparsers.add_parser('compdb', help='Generate compilation database') + _subparser_build = subparsers.add_parser('build', help='Build project (default command)') + _subparser_compdb = subparsers.add_parser('compdb', help='Generate compilation database') subparser_run = subparsers.add_parser('run', help='Build and run project') subparser_run.add_argument( @@ -196,6 +196,13 @@ class ShurikenArgumentParser: help='Arguments that are passed to the application', ) + subparser_lint = subparsers.add_parser('lint', help='Lint the project') + subparser_lint.add_argument( + 'lint_files', + nargs='*', + help='Files to lint (defaults to all source files)', + ) + subparser_clean = subparsers.add_parser('clean', help='Remove all build files of the current target') subparser_clean.add_argument( '--all', @@ -204,11 +211,11 @@ class ShurikenArgumentParser: help='Remove all generated files from all targets', ) - subparser_dump_config = subparsers.add_parser( + _subparser_dump_config = subparsers.add_parser( 'dump-config', help='Dumps the parsed config as well as the effective build target config (for debugging)', ) - subparser_dyndep = subparsers.add_parser( + _subparser_dyndep = subparsers.add_parser( 'p1689-to-dyndeps', help='(Internal command) Parse p1689 file and generate Ninja dyndeps', ) @@ -298,6 +305,9 @@ class ShurikenCli: case 'run': self.run_project(selected_target) + case 'lint': + self.lint_project(selected_target) + case 'clean': if self.args.clean_all: self.log_info('Cleaning up *all* build targets') @@ -413,6 +423,26 @@ class ShurikenCli: except KeyboardInterrupt: self.log_info('Keyboard interrupt') + def lint_project(self, target: str) -> None: + # Build project using Ninja (necessary before linting) + self.build_project(target) + + # Collect files to lint + if self.args.lint_files: + files_args = shlex.join(self.args.lint_files) + else: + files_args = f"$(find {self.config.source_dir} -name '*.cpp' -or -name '*.cppm')" + + self.log_info('Running clangd-tidy') + run_result = subprocess.run( + f'clangd-tidy -p {self.config.build_dir} --allow-extensions cpp,cppm {files_args}', + shell=True, + ) + + if run_result.returncode > 0: + self.log_error(f'clangd-tidy exited with return code {run_result.returncode}') + exit(1) + def clean_project(self, target: str) -> None: ninja_file_path = self.config.get_ninja_file_path(target) From ae1df9bc8b743a1356dc7b1a835259157b66b2fb Mon Sep 17 00:00:00 2001 From: binaryDiv Date: Fri, 15 May 2026 01:49:53 +0200 Subject: [PATCH 5/7] shuriken: Add support for "import std" --- shuriken.yaml | 3 +- tools/shuriken.py | 141 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 124 insertions(+), 20 deletions(-) diff --git a/shuriken.yaml b/shuriken.yaml index fee3c69..0a07b3f 100644 --- a/shuriken.yaml +++ b/shuriken.yaml @@ -5,7 +5,6 @@ defaults: -Wextra -pedantic -Ivendor/RmlUi/Include - linker_args: >- -lSDL3 -lSDL3_image @@ -13,6 +12,8 @@ defaults: vendor/RmlUi/Build/librmlui_debugger.a vendor/RmlUi/Build/librmlui.a +enable_import_std: true + default_target: linux targets: diff --git a/tools/shuriken.py b/tools/shuriken.py index fcb6402..955c672 100755 --- a/tools/shuriken.py +++ b/tools/shuriken.py @@ -64,6 +64,7 @@ class TargetConfig: class ShurikenConfig: __path_fields = ['source_dir', 'build_dir'] __list_fields = [] + __bool_fields = ['enable_import_std'] source_dir: Path = Path('src') build_dir: Path = Path('build') @@ -76,6 +77,8 @@ class ShurikenConfig: linker_args='', )) + enable_import_std: bool = False + default_target: str = '' targets: dict[str, TargetConfig] = field(default_factory=dict) @@ -86,13 +89,13 @@ class ShurikenConfig: continue if key == 'defaults': - assert (type(value) is dict) + assert type(value) is dict self.defaults.update(value) elif key == 'targets': - assert (type(value) is dict) + assert type(value) is dict for target_name, target_config in value.items(): - assert (type(target_name) is str and len(target_name) > 0) - assert (type(target_config) is dict or target_config is None) + assert type(target_name) is str and len(target_name) > 0 + assert type(target_config) is dict or target_config is None # Ignore "hidden" targets starting with a dot (can be used for YAML anchors) if target_name[0] == '.': @@ -105,8 +108,11 @@ class ShurikenConfig: elif key in self.__path_fields: setattr(self, key, Path(value)) elif key in self.__list_fields: - assert (type(value) is list) + assert type(value) is list setattr(self, key, list(str(item) for item in value)) + elif key in self.__bool_fields: + assert type(value) is bool + setattr(self, key, value) else: setattr(self, key, str(value)) @@ -484,13 +490,14 @@ class ShurikenCli: print('ninja_dyndep_version = 1\n') for out, deps in build_dependencies.items(): if deps: - print(f'build {out}: dyndep | {" ".join(deps)}') + print(f'build {out}: dyndep | {" ".join(str(dep) for dep in deps)}') else: print(f'build {out}: dyndep') CompileTargetCpp = namedtuple('CompileTargetCpp', ['src', 'obj']) CompileTargetCppm = namedtuple('CompileTargetCppm', ['src', 'obj', 'pcm']) +CompileTargetStdModule = namedtuple('CompileTargetStdModule', ['src', 'pcm']) class NinjaFileGenerator: @@ -500,6 +507,7 @@ class NinjaFileGenerator: compile_targets_cpp: list[CompileTargetCpp] compile_targets_cppm: list[CompileTargetCppm] + compile_targets_std_module: list[CompileTargetStdModule] link_target_obj_files: list[str] def __init__(self, *, config: ShurikenConfig, target: str): @@ -517,6 +525,7 @@ class NinjaFileGenerator: def collect_compile_targets(self) -> None: self.compile_targets_cpp = [] self.compile_targets_cppm = [] + self.compile_targets_std_module = [] # Compile targets for .cpp files for src_file in self.find_source_files('**/*.cpp'): @@ -529,12 +538,24 @@ class NinjaFileGenerator: # Compile targets for .cppm files for src_file in self.find_source_files('**/*.cppm'): relative_name = src_file.relative_to(self.config.source_dir) + module_name = '.'.join(relative_name.with_suffix('.pcm').parts) self.compile_targets_cppm.append(CompileTargetCppm( src=str(src_file), obj=f"$builddir/obj/{relative_name.with_suffix('.o')}", - pcm=f"$builddir/pcm/{'.'.join(relative_name.with_suffix('.pcm').parts)}", + pcm=f"$builddir/pcm/{module_name}", )) + # Compile targets for standard library modules ("import std") + if self.config.enable_import_std: + # NOTE: Add std.compat to the list (here and a few other places) if we ever need it. For now, std is enough. + std_module_source_finder = StdModuleSourceFinder(config=self.config, target=self.target) + std_module_sources = std_module_source_finder.find_module_sources(['std']) + for module_name, module_src_path in std_module_sources.items(): + self.compile_targets_std_module.append(CompileTargetStdModule( + src=str(module_src_path), + pcm=f"$builddir/pcm/{module_name}.pcm", + )) + # Link target for output file (executable): gather all object files self.link_target_obj_files = sorted( target.obj for target in self.compile_targets_cpp + self.compile_targets_cppm @@ -560,7 +581,9 @@ compdb_file = {build_dir}/compile_commands.json dyndep_file = $builddir/.ninja_dyndep # Compiler flags -cpp_flags = -std={target_config.cpp_standard} -fprebuilt-module-path=$builddir/pcm/ {target_config.merged_cpp_flags} +cpp_base_flags = -std={target_config.cpp_standard} -fprebuilt-module-path=$builddir/pcm/ +cpp_flags = $cpp_base_flags {target_config.merged_cpp_flags} +cpp_std_module_flags = $cpp_base_flags -Wno-reserved-module-identifier -x c++-module linker_flags = {target_config.merged_linker_flags} linker_args = {target_config.merged_linker_args} @@ -574,6 +597,10 @@ rule cpp rule cppm command = {target_config.cpp_compiler} $cpp_flags -c $in -o $out -fmodule-output=$pcm_out +# Rule to compile a standard library module to a .pcm file (e.g. "import std") +rule std_module + command = {target_config.cpp_compiler} $cpp_std_module_flags --precompile $in -o $out + # Rule to link several .o files to an executable rule link command = {target_config.cpp_compiler} $linker_flags -o $out $in $linker_args @@ -611,9 +638,10 @@ default $builddir/{target_config.output_file} """.strip() def render_compile_targets(self) -> str: - rendered_targets_cpp = [self.render_compile_target_cpp(target) for target in self.compile_targets_cpp] - rendered_targets_cppm = [self.render_compile_target_cppm(target) for target in self.compile_targets_cppm] - return '\n\n'.join(rendered_targets_cpp + rendered_targets_cppm) + rendered_targets = list(map(self.render_compile_target_std_module, self.compile_targets_std_module)) + rendered_targets += list(map(self.render_compile_target_cpp, self.compile_targets_cpp)) + rendered_targets += list(map(self.render_compile_target_cppm, self.compile_targets_cppm)) + return '\n\n'.join(rendered_targets) @staticmethod def render_compile_target_cpp(target: CompileTargetCpp) -> str: @@ -630,23 +658,36 @@ build {target.obj} | {target.pcm}: cppm {target.src} || $dyndep_file pcm_out = {target.pcm} """.strip() + @staticmethod + def render_compile_target_std_module(target: CompileTargetStdModule) -> str: + return f""" +build {target.pcm}: std_module {target.src} || $dyndep_file + dyndep = $dyndep_file +""".strip() + class BuildDependencyParser: config: ShurikenConfig target: str - def __init__(self, config: ShurikenConfig, target: str): + def __init__(self, *, config: ShurikenConfig, target: str): self.config = config self.target = target - def parse_build_dependencies(self, parsed_p1689: dict[str, Any]) -> dict[str, list[str]]: + def parse_build_dependencies(self, parsed_p1689: dict[str, Any]) -> dict[Path, list[Path]]: # Shortcut variables source_dir = self.config.source_dir target_build_dir = self.config.build_dir / self.target default_target_build_dir = self.config.build_dir / self.config.default_target + # Map of module names to PCM file paths + module_map: dict[str, Path] = {} + + # Add standard library modules to module map (if "import std" support is enabled) + if self.config.enable_import_std: + module_map['std'] = Path(target_build_dir, 'pcm', 'std.pcm') + # Validate module name and path convention, construct map of module names to PCM file paths - module_map = {} for rule in parsed_p1689['rules']: provides = rule.get('provides', []) if provides: @@ -654,15 +695,23 @@ class BuildDependencyParser: assert provides[0]['is-interface'] is True, f"Rule provides non-interface module: {rule}" module_name: str = provides[0]['logical-name'] - assert module_name not in module_map, f"Module {module_name} is provided more than once: {rule}" + assert module_name not in module_map, f'Module "{module_name}" is provided more than once: {rule}' expected_source_path = str(Path(source_dir, module_name.replace('.', '/') + '.cppm')) - assert provides[0]['source-path'] == expected_source_path, f"Module name does not match source path: {rule}" + assert provides[0]['source-path'] == expected_source_path, \ + f"Module name does not match source path: {rule}" - module_map[module_name] = str(Path(target_build_dir, 'pcm', module_name + '.pcm')) + module_map[module_name] = Path(target_build_dir, 'pcm', module_name + '.pcm') + + # Collect all module dependencies to generate dyndeps + build_dependencies: dict[Path, list[Path]] = {} + + # If "import std" support is enabled, we need to add std to the build dependencies, even though it doesn't have + # any dependencies itself. This is needed, otherwise the module won't be built automatically. + if self.config.enable_import_std: + build_dependencies[target_build_dir / 'pcm/std.pcm'] = [] # Parse rules and get module dependencies - build_dependencies = {} for rule in parsed_p1689['rules']: # Compilation database contains "build/{default_target}" paths, we need to map them to "build/{target}" out = target_build_dir / Path(rule['primary-output']).relative_to(default_target_build_dir) @@ -670,11 +719,65 @@ class BuildDependencyParser: build_dependencies[out] = [] for require in rule.get('requires', []): module_name = require['logical-name'] - assert module_name in module_map, f'Module {module_name} not found in module map' + assert module_name in module_map, f'Module "{module_name}" not found in module map' build_dependencies[out].append(module_map[module_name]) return build_dependencies +class StdModuleSourceFinder: + cpp_compiler: str + + def __init__(self, *, config: ShurikenConfig, target: str): + self.cpp_compiler = config.get_merged_target_config(target).cpp_compiler + + def find_module_sources(self, module_names: list[str]) -> dict[str, Path]: + # Get normalized path of standard library "module manifest" (JSON file containing source paths for std modules) + module_manifest_path = self.get_module_manifest_path() + module_manifest_base_dir = module_manifest_path.parent + + # Parse module manifest JSON file + with module_manifest_path.open('r') as f: + module_manifest_json = json.load(f) + + # Collect the module source paths + std_module_sources: dict[str, Path] = {} + for modules_item in module_manifest_json['modules']: + # Ignore modules we don't need (probably just std.compat) + module_name = modules_item['logical-name'] + if module_name not in module_names: + continue + + # The source path is usually relative to the module manifest file, convert to absolute path + module_src_path = Path(modules_item['source-path']) + if not module_src_path.is_absolute(): + module_src_path = module_manifest_base_dir / module_src_path + if not module_src_path.is_file(): + raise Exception(f'Source path for module "{module_name}" is not a file: {module_src_path}') + + # Resolve symlinks and normalize path + std_module_sources[module_name] = module_src_path.resolve() + + return std_module_sources + + def get_module_manifest_path(self) -> Path: + # Use compiler to get the path to the module manifest JSON file + clang_cmd = [self.cpp_compiler, '-print-library-module-manifest-path'] + clang_cmd_result = subprocess.run(clang_cmd, capture_output=True) + + if clang_cmd_result.returncode != 0: + cmd_stderr = clang_cmd_result.stderr.decode().strip() + raise Exception(f'Subprocess "{" ".join(clang_cmd)}" failed. stderr:\n{cmd_stderr}') + + # Read path from subprocess stdout + module_manifest_path = Path(clang_cmd_result.stdout.decode().strip()) + + if not module_manifest_path.is_file(): + raise Exception(f'Could not locate stdlib module manifest: {module_manifest_path}') + + # Resolve symlinks and normalize path + return module_manifest_path.resolve() + + if __name__ == '__main__': ShurikenCli().run() From 5a53457817d487b70eadfb202943b896ebca903f Mon Sep 17 00:00:00 2001 From: binaryDiv Date: Sat, 16 May 2026 01:17:43 +0200 Subject: [PATCH 6/7] shuriken: Copy source file to build dir for "import std" Problem with the previous approach: CLion wouldn't recognize the std module, unless it's included in the compdb (but then /usr/include/c++/.../bits is listed as part of the project -.-) --- tools/shuriken.py | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/tools/shuriken.py b/tools/shuriken.py index 955c672..e5a1943 100755 --- a/tools/shuriken.py +++ b/tools/shuriken.py @@ -497,7 +497,7 @@ class ShurikenCli: CompileTargetCpp = namedtuple('CompileTargetCpp', ['src', 'obj']) CompileTargetCppm = namedtuple('CompileTargetCppm', ['src', 'obj', 'pcm']) -CompileTargetStdModule = namedtuple('CompileTargetStdModule', ['src', 'pcm']) +CompileTargetStdModule = namedtuple('CompileTargetStdModule', ['original_src', 'generated_src', 'pcm']) class NinjaFileGenerator: @@ -552,7 +552,8 @@ class NinjaFileGenerator: std_module_sources = std_module_source_finder.find_module_sources(['std']) for module_name, module_src_path in std_module_sources.items(): self.compile_targets_std_module.append(CompileTargetStdModule( - src=str(module_src_path), + original_src=str(module_src_path), + generated_src=f"$builddir/generated/stdlib/{module_name}.cppm", pcm=f"$builddir/pcm/{module_name}.pcm", )) @@ -589,6 +590,10 @@ linker_args = {target_config.merged_linker_args} # -- RULES +# Rule to copy an arbitrary file (or multiple) from in to out +rule copy + command = cp $in $out + # Rule to compile a .cpp file to a .o file rule cpp command = {target_config.cpp_compiler} $cpp_flags -c $in -o $out @@ -607,7 +612,7 @@ rule link # Rule to generate a compilation database (JSON file) from the build targets defined in a Ninja file rule generate_compdb - command = ninja -f $in -t compdb cpp cppm > $out + command = ninja -f $in -t compdb cpp cppm std_module > $out # Rule to generate a Ninja dyndep file for C++ module dependencies based on a compilation database rule generate_dyndeps @@ -617,7 +622,7 @@ rule generate_dyndeps # -- STATIC BUILD TARGETS # Generate compilation database from default target Ninja file -build $compdb_file: generate_compdb {self.config.get_ninja_file_path(config.default_target)} +build $compdb_file: generate_compdb {self.config.get_ninja_file_path(config.default_target)} | generate_source_files # Shortcut alias to generate compilation database build compdb: phony $compdb_file @@ -632,11 +637,18 @@ build $dyndep_file: generate_dyndeps $compdb_file build $builddir/{target_config.output_file}: link {' '.join(self.link_target_obj_files)} default $builddir/{target_config.output_file} +# Phony target that depends on all generated source files +build generate_source_files: phony {self.list_generated_source_files()} + {self.render_compile_targets()} # End of build.{target}.ninja """.strip() + def list_generated_source_files(self) -> str: + generated_files = [target.generated_src for target in self.compile_targets_std_module] + return ' '.join(generated_files) + def render_compile_targets(self) -> str: rendered_targets = list(map(self.render_compile_target_std_module, self.compile_targets_std_module)) rendered_targets += list(map(self.render_compile_target_cpp, self.compile_targets_cpp)) @@ -661,7 +673,9 @@ build {target.obj} | {target.pcm}: cppm {target.src} || $dyndep_file @staticmethod def render_compile_target_std_module(target: CompileTargetStdModule) -> str: return f""" -build {target.pcm}: std_module {target.src} || $dyndep_file +build {target.generated_src}: copy {target.original_src} + +build {target.pcm}: std_module {target.generated_src} || $dyndep_file dyndep = $dyndep_file """.strip() @@ -683,10 +697,6 @@ class BuildDependencyParser: # Map of module names to PCM file paths module_map: dict[str, Path] = {} - # Add standard library modules to module map (if "import std" support is enabled) - if self.config.enable_import_std: - module_map['std'] = Path(target_build_dir, 'pcm', 'std.pcm') - # Validate module name and path convention, construct map of module names to PCM file paths for rule in parsed_p1689['rules']: provides = rule.get('provides', []) @@ -697,20 +707,16 @@ class BuildDependencyParser: module_name: str = provides[0]['logical-name'] assert module_name not in module_map, f'Module "{module_name}" is provided more than once: {rule}' - expected_source_path = str(Path(source_dir, module_name.replace('.', '/') + '.cppm')) - assert provides[0]['source-path'] == expected_source_path, \ - f"Module name does not match source path: {rule}" + if module_name != 'std': + expected_source_path = str(Path(source_dir, module_name.replace('.', '/') + '.cppm')) + assert provides[0]['source-path'] == expected_source_path, \ + f"Module name does not match source path: {rule}" module_map[module_name] = Path(target_build_dir, 'pcm', module_name + '.pcm') # Collect all module dependencies to generate dyndeps build_dependencies: dict[Path, list[Path]] = {} - # If "import std" support is enabled, we need to add std to the build dependencies, even though it doesn't have - # any dependencies itself. This is needed, otherwise the module won't be built automatically. - if self.config.enable_import_std: - build_dependencies[target_build_dir / 'pcm/std.pcm'] = [] - # Parse rules and get module dependencies for rule in parsed_p1689['rules']: # Compilation database contains "build/{default_target}" paths, we need to map them to "build/{target}" From d5d480d85497c24dc28093fa2cf645a4050fa6e7 Mon Sep 17 00:00:00 2001 From: binaryDiv Date: Sat, 16 May 2026 01:58:45 +0200 Subject: [PATCH 7/7] Replace stdlib headers with import std --- src/rutile/app.cppm | 3 +-- src/rutile/config.cppm | 4 ++-- src/rutile/core/drawing/sprite.cppm | 3 +-- src/rutile/core/drawing/tile_map.cppm | 5 ++--- src/rutile/core/drawing/tile_set.cppm | 4 +--- src/rutile/core/engine.cppm | 2 +- src/rutile/core/render_server.cppm | 4 +--- src/rutile/core/resource_manager.cppm | 6 ++---- src/rutile/core/texture_loader.cppm | 3 +-- src/rutile/core/ui/backend/rmlui_render_interface.cppm | 7 ++++--- src/rutile/core/ui/ui_server.cppm | 2 +- src/rutile/game/game.cppm | 4 +--- src/wrappers/sdl/clipboard.cppm | 2 +- src/wrappers/sdl/error.cppm | 4 ++-- src/wrappers/sdl/init.cppm | 2 +- src/wrappers/sdl/mouse.cppm | 4 +++- src/wrappers/sdl/render.cppm | 2 +- src/wrappers/sdl/surface.cppm | 2 +- src/wrappers/sdl/utils.cppm | 3 ++- src/wrappers/sdl/video.cppm | 2 +- src/wrappers/sdl_image.cppm | 2 +- 21 files changed, 31 insertions(+), 39 deletions(-) diff --git a/src/rutile/app.cppm b/src/rutile/app.cppm index 2eea271..37deff0 100644 --- a/src/rutile/app.cppm +++ b/src/rutile/app.cppm @@ -1,11 +1,10 @@ module; -#include -#include #include export module rutile.app; +import std; import rutile.config; import rutile.core.engine; import rutile.game.game; diff --git a/src/rutile/config.cppm b/src/rutile/config.cppm index b5dd5ce..2e36783 100644 --- a/src/rutile/config.cppm +++ b/src/rutile/config.cppm @@ -1,7 +1,5 @@ module; -#include - #ifdef NDEBUG #define DEBUG_BOOL false #else @@ -10,6 +8,8 @@ module; export module rutile.config; +import std; + export namespace rutile::config { constexpr auto debug = DEBUG_BOOL; diff --git a/src/rutile/core/drawing/sprite.cppm b/src/rutile/core/drawing/sprite.cppm index 6835962..1158bde 100644 --- a/src/rutile/core/drawing/sprite.cppm +++ b/src/rutile/core/drawing/sprite.cppm @@ -1,11 +1,10 @@ module; #include -#include -#include export module rutile.core.drawing.sprite; +import std; import rutile.core.render_server; import wrappers.sdl; diff --git a/src/rutile/core/drawing/tile_map.cppm b/src/rutile/core/drawing/tile_map.cppm index 2fa9111..de66b2c 100644 --- a/src/rutile/core/drawing/tile_map.cppm +++ b/src/rutile/core/drawing/tile_map.cppm @@ -1,11 +1,10 @@ module; #include -#include -#include export module rutile.core.drawing.tile_map; +import std; import rutile.core.drawing.tile_set; import rutile.core.render_server; import wrappers.sdl; @@ -13,7 +12,7 @@ import wrappers.sdl; export namespace rutile { // Restrict map size to 16 bit per dimension, so that width*height still fits within size_t - using MapCoord = uint16_t; + using MapCoord = std::uint16_t; class TileMap { diff --git a/src/rutile/core/drawing/tile_set.cppm b/src/rutile/core/drawing/tile_set.cppm index 6fcbec5..ddad86c 100644 --- a/src/rutile/core/drawing/tile_set.cppm +++ b/src/rutile/core/drawing/tile_set.cppm @@ -1,12 +1,10 @@ module; #include -#include -#include -#include export module rutile.core.drawing.tile_set; +import std; import rutile.core.render_server; import wrappers.sdl; diff --git a/src/rutile/core/engine.cppm b/src/rutile/core/engine.cppm index 680d382..984f323 100644 --- a/src/rutile/core/engine.cppm +++ b/src/rutile/core/engine.cppm @@ -1,10 +1,10 @@ module; #include -#include export module rutile.core.engine; +import std; import rutile.config; import rutile.core.render_server; import rutile.core.ui.ui_server; diff --git a/src/rutile/core/render_server.cppm b/src/rutile/core/render_server.cppm index ad32e73..7366b13 100644 --- a/src/rutile/core/render_server.cppm +++ b/src/rutile/core/render_server.cppm @@ -1,10 +1,8 @@ module; -#include -#include - export module rutile.core.render_server; +import std; import rutile.core.resource_manager; import rutile.core.texture_loader; import wrappers.sdl; diff --git a/src/rutile/core/resource_manager.cppm b/src/rutile/core/resource_manager.cppm index dddac0d..b73f184 100644 --- a/src/rutile/core/resource_manager.cppm +++ b/src/rutile/core/resource_manager.cppm @@ -1,13 +1,11 @@ module; #include -#include -#include -#include -#include export module rutile.core.resource_manager; +import std; + export namespace rutile { template diff --git a/src/rutile/core/texture_loader.cppm b/src/rutile/core/texture_loader.cppm index 2bf1f08..78cb3df 100644 --- a/src/rutile/core/texture_loader.cppm +++ b/src/rutile/core/texture_loader.cppm @@ -1,9 +1,8 @@ module; -#include - export module rutile.core.texture_loader; +import std; import rutile.core.resource_manager; import wrappers.sdl; import wrappers.sdl_image; diff --git a/src/rutile/core/ui/backend/rmlui_render_interface.cppm b/src/rutile/core/ui/backend/rmlui_render_interface.cppm index c2412f3..f64528f 100644 --- a/src/rutile/core/ui/backend/rmlui_render_interface.cppm +++ b/src/rutile/core/ui/backend/rmlui_render_interface.cppm @@ -11,6 +11,7 @@ module; export module rutile.core.ui.backend.rmlui_render_interface; +import std; import wrappers.sdl; import wrappers.sdl_image; @@ -78,9 +79,9 @@ export namespace rutile { const GeometryView* geometry = reinterpret_cast(handle); const Rml::Vertex* vertices = geometry->vertices.data(); - const size_t num_vertices = geometry->vertices.size(); + const std::size_t num_vertices = geometry->vertices.size(); const int* indices = geometry->indices.data(); - const size_t num_indices = geometry->indices.size(); + const std::size_t num_indices = geometry->indices.size(); // Convert RmlUi vertices to SDL vertices auto sdl_vertices = std::make_unique(num_vertices); @@ -125,7 +126,7 @@ export namespace rutile } // 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; + const std::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) { diff --git a/src/rutile/core/ui/ui_server.cppm b/src/rutile/core/ui/ui_server.cppm index 3f15b70..66eb4c9 100644 --- a/src/rutile/core/ui/ui_server.cppm +++ b/src/rutile/core/ui/ui_server.cppm @@ -1,12 +1,12 @@ module; #include -#include #include #include export module rutile.core.ui.ui_server; +import std; import rutile.core.ui.backend.rmlui_render_interface; import rutile.core.ui.backend.rmlui_system_interface; import rutile.core.ui.backend.rmlui_input_handler; diff --git a/src/rutile/game/game.cppm b/src/rutile/game/game.cppm index c7a45b9..8b7a1ff 100644 --- a/src/rutile/game/game.cppm +++ b/src/rutile/game/game.cppm @@ -1,13 +1,11 @@ module; #include -#include -#include -#include #include export module rutile.game.game; +import std; import rutile.core.drawing.sprite; import rutile.core.drawing.tile_map; import rutile.core.drawing.tile_set; diff --git a/src/wrappers/sdl/clipboard.cppm b/src/wrappers/sdl/clipboard.cppm index bb84b79..8ab1278 100644 --- a/src/wrappers/sdl/clipboard.cppm +++ b/src/wrappers/sdl/clipboard.cppm @@ -1,10 +1,10 @@ module; -#include #include export module wrappers.sdl.clipboard; +import std; import wrappers.sdl.error; import wrappers.sdl.utils; diff --git a/src/wrappers/sdl/error.cppm b/src/wrappers/sdl/error.cppm index c4f23df..0b12725 100644 --- a/src/wrappers/sdl/error.cppm +++ b/src/wrappers/sdl/error.cppm @@ -1,11 +1,11 @@ module; -#include -#include #include export module wrappers.sdl.error; +import std; + export namespace sdl { // Exception that wraps an SDL error (which SDL function caused the error, what's the error). diff --git a/src/wrappers/sdl/init.cppm b/src/wrappers/sdl/init.cppm index 9e3653a..f673011 100644 --- a/src/wrappers/sdl/init.cppm +++ b/src/wrappers/sdl/init.cppm @@ -1,10 +1,10 @@ module; -#include #include export module wrappers.sdl.init; +import std; import wrappers.sdl.error; export namespace sdl diff --git a/src/wrappers/sdl/mouse.cppm b/src/wrappers/sdl/mouse.cppm index a9671f2..e612c00 100644 --- a/src/wrappers/sdl/mouse.cppm +++ b/src/wrappers/sdl/mouse.cppm @@ -5,9 +5,11 @@ module; export module wrappers.sdl.mouse; +import std; + export namespace sdl { - using MouseButtonIndex = uint8_t; + using MouseButtonIndex = std::uint8_t; namespace MouseButtons { diff --git a/src/wrappers/sdl/render.cppm b/src/wrappers/sdl/render.cppm index 0ea45a9..a556c3f 100644 --- a/src/wrappers/sdl/render.cppm +++ b/src/wrappers/sdl/render.cppm @@ -1,11 +1,11 @@ module; #include -#include #include export module wrappers.sdl.render; +import std; import utils.memory; import wrappers.sdl.error; import wrappers.sdl.rect; diff --git a/src/wrappers/sdl/surface.cppm b/src/wrappers/sdl/surface.cppm index c48d8c3..88cba61 100644 --- a/src/wrappers/sdl/surface.cppm +++ b/src/wrappers/sdl/surface.cppm @@ -1,11 +1,11 @@ module; #include -#include #include export module wrappers.sdl.surface; +import std; import utils.memory; import wrappers.sdl.error; diff --git a/src/wrappers/sdl/utils.cppm b/src/wrappers/sdl/utils.cppm index c544c51..96dd45e 100644 --- a/src/wrappers/sdl/utils.cppm +++ b/src/wrappers/sdl/utils.cppm @@ -1,10 +1,11 @@ module; -#include #include export module wrappers.sdl.utils; +import std; + export namespace sdl { /** diff --git a/src/wrappers/sdl/video.cppm b/src/wrappers/sdl/video.cppm index 4141b01..f393a43 100644 --- a/src/wrappers/sdl/video.cppm +++ b/src/wrappers/sdl/video.cppm @@ -1,11 +1,11 @@ module; #include -#include #include export module wrappers.sdl.video; +import std; import utils.memory; import wrappers.sdl.error; import wrappers.sdl.rect; diff --git a/src/wrappers/sdl_image.cppm b/src/wrappers/sdl_image.cppm index 5418c93..7312e9c 100644 --- a/src/wrappers/sdl_image.cppm +++ b/src/wrappers/sdl_image.cppm @@ -1,11 +1,11 @@ module; -#include #include #include export module wrappers.sdl_image; +import std; import utils.memory; import wrappers.sdl.error; import wrappers.sdl.render;