diff --git a/.gitignore b/.gitignore index 51269c9..e92ae95 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ __pycache__/ /build /build.ninja /shuriken.override.yaml + +# Dependencies +/vendor +!/vendor/README.md diff --git a/assets/fonts/LatoLatin-Regular.ttf b/assets/fonts/LatoLatin-Regular.ttf new file mode 100644 index 0000000..bcc5778 Binary files /dev/null and b/assets/fonts/LatoLatin-Regular.ttf differ diff --git a/assets/ui/base.rcss b/assets/ui/base.rcss new file mode 100644 index 0000000..b325039 --- /dev/null +++ b/assets/ui/base.rcss @@ -0,0 +1,88 @@ +/* Base style sheet based on: https://mikke89.github.io/RmlUiDoc/pages/rml/html4_style_sheet.html */ + +body, div, h1, h2, h3, h4, h5, h6, p, hr, pre { + display: block; +} + +h1 { + font-size: 2em; + margin: .67em 0; +} + +h2 { + font-size: 1.5em; + margin: .75em 0; +} + +h3 { + font-size: 1.17em; + margin: .83em 0; +} + +h4 { + margin: 1.12em 0; +} + +h5 { + font-size: .83em; + margin: 1.5em 0; +} + +h6 { + font-size: .75em; + margin: 1.67em 0; +} + +h1, h2, h3, h4, h5, h6, strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +pre { + white-space: pre; +} + +hr { + border-width: 1px; +} + +table { + box-sizing: border-box; + display: table; +} + +tr { + box-sizing: border-box; + display: table-row; +} + +td { + box-sizing: border-box; + display: table-cell; +} + +col { + box-sizing: border-box; + display: table-column; +} + +colgroup { + display: table-column-group; +} + +thead, tbody, tfoot { + display: table-row-group; +} + +tabset tabs { + display: block; +} + +input { + background-color: white; + color: black; + cursor: text; +} diff --git a/assets/ui/main_ui.rml b/assets/ui/main_ui.rml new file mode 100644 index 0000000..9dc12f1 --- /dev/null +++ b/assets/ui/main_ui.rml @@ -0,0 +1,16 @@ + + + + + + +

Meow!

+ +
diff --git a/shuriken.yaml b/shuriken.yaml index 5983465..0a07b3f 100644 --- a/shuriken.yaml +++ b/shuriken.yaml @@ -1,7 +1,18 @@ 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 + +enable_import_std: true default_target: linux diff --git a/src/main.cpp b/src/main.cpp index 4a0a4fc..a6e0353 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); - return static_cast(app->handle_event(event)); + 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 59% rename from src/app.cppm rename to src/rutile/app.cppm index 7b59948..37deff0 100644 --- a/src/app.cppm +++ b/src/rutile/app.cppm @@ -1,22 +1,21 @@ module; -#include -#include -#include +#include -export module app; +export module rutile.app; -import config; -import core.engine; -import game.game; +import std; +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; @@ -25,20 +24,20 @@ export { 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_ = 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'; @@ -48,7 +47,7 @@ export 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/config.cppm b/src/rutile/config.cppm similarity index 91% rename from src/config.cppm rename to src/rutile/config.cppm index 0ed9a38..2e36783 100644 --- a/src/config.cppm +++ b/src/rutile/config.cppm @@ -1,16 +1,16 @@ module; -#include - #ifdef NDEBUG #define DEBUG_BOOL false #else #define DEBUG_BOOL true #endif -export module config; +export module rutile.config; -export namespace config +import std; + +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 95% rename from src/core/drawing/sprite.cppm rename to src/rutile/core/drawing/sprite.cppm index 31224cd..1158bde 100644 --- a/src/core/drawing/sprite.cppm +++ b/src/rutile/core/drawing/sprite.cppm @@ -1,15 +1,14 @@ module; #include -#include -#include -export module core.drawing.sprite; +export module rutile.core.drawing.sprite; -import core.render_server; +import std; +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 94% rename from src/core/drawing/tile_map.cppm rename to src/rutile/core/drawing/tile_map.cppm index 31bfbfa..de66b2c 100644 --- a/src/core/drawing/tile_map.cppm +++ b/src/rutile/core/drawing/tile_map.cppm @@ -1,19 +1,18 @@ module; #include -#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 std; +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; + using MapCoord = std::uint16_t; class TileMap { diff --git a/src/core/drawing/tile_set.cppm b/src/rutile/core/drawing/tile_set.cppm similarity index 95% rename from src/core/drawing/tile_set.cppm rename to src/rutile/core/drawing/tile_set.cppm index 39ba81d..ddad86c 100644 --- a/src/core/drawing/tile_set.cppm +++ b/src/rutile/core/drawing/tile_set.cppm @@ -1,16 +1,14 @@ module; #include -#include -#include -#include -export module core.drawing.tile_set; +export module rutile.core.drawing.tile_set; -import core.render_server; +import std; +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 73% rename from src/core/engine.cppm rename to src/rutile/core/engine.cppm index d9bd5ad..984f323 100644 --- a/src/core/engine.cppm +++ b/src/rutile/core/engine.cppm @@ -1,15 +1,16 @@ module; #include -#include -export module core.engine; +export module rutile.core.engine; -import config; -import core.render_server; +import std; +import rutile.config; +import rutile.core.render_server; +import rutile.core.ui.ui_server; import wrappers.sdl; -export namespace core +export namespace rutile { class Engine { @@ -21,11 +22,13 @@ export namespace core 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; } @@ -49,19 +52,21 @@ export namespace core // 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, 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 core 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/core/render_server.cppm b/src/rutile/core/render_server.cppm similarity index 91% rename from src/core/render_server.cppm rename to src/rutile/core/render_server.cppm index bcb4c9f..7366b13 100644 --- a/src/core/render_server.cppm +++ b/src/rutile/core/render_server.cppm @@ -1,15 +1,13 @@ module; -#include -#include +export module rutile.core.render_server; -export module core.render_server; - -import core.resource_manager; -import core.texture_loader; +import std; +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; @@ -27,7 +25,9 @@ export namespace core : 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/core/resource_manager.cppm b/src/rutile/core/resource_manager.cppm similarity index 96% rename from src/core/resource_manager.cppm rename to src/rutile/core/resource_manager.cppm index 2e947f5..b73f184 100644 --- a/src/core/resource_manager.cppm +++ b/src/rutile/core/resource_manager.cppm @@ -1,14 +1,12 @@ module; #include -#include -#include -#include -#include -export module core.resource_manager; +export module rutile.core.resource_manager; -export namespace core +import std; + +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 79% rename from src/core/texture_loader.cppm rename to src/rutile/core/texture_loader.cppm index 32548c4..78cb3df 100644 --- a/src/core/texture_loader.cppm +++ b/src/rutile/core/texture_loader.cppm @@ -1,14 +1,13 @@ module; -#include +export module rutile.core.texture_loader; -export module core.texture_loader; - -import core.resource_manager; +import std; +import rutile.core.resource_manager; import wrappers.sdl; import wrappers.sdl_image; -export namespace core +export namespace rutile { class TextureLoader { @@ -29,7 +28,7 @@ export namespace core 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..f64528f --- /dev/null +++ b/src/rutile/core/ui/backend/rmlui_render_interface.cppm @@ -0,0 +1,200 @@ +// 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 std; +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 std::size_t num_vertices = geometry->vertices.size(); + const int* indices = geometry->indices.data(); + const std::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 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) { + 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..66eb4c9 --- /dev/null +++ b/src/rutile/core/ui/ui_server.cppm @@ -0,0 +1,124 @@ +module; + +#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; +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/game/game.cppm b/src/rutile/game/game.cppm similarity index 68% rename from src/game/game.cppm rename to src/rutile/game/game.cppm index a18cc6f..8b7a1ff 100644 --- a/src/game/game.cppm +++ b/src/rutile/game/game.cppm @@ -1,21 +1,20 @@ module; #include -#include -#include -#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 std; +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 +22,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 +45,7 @@ export namespace game ) }, tile_set_{ - core::TileSet::create_from_texture( + TileSet::create_from_texture( engine_.get_render_server(), "assets/tilesets/terrain.png", 32, @@ -63,6 +62,12 @@ export namespace game }, 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 +75,8 @@ export namespace game tile_map_.set_tile(x, y, tile_index); } } + + instantiated_ = true; } public: @@ -86,7 +93,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_); @@ -96,20 +103,20 @@ export namespace game } // 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( - core::Sprite::create_from_texture( + 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 +128,8 @@ export namespace game void update() { + // TODO: Move this to Engine + engine_.get_ui_server().update(); } void render() const @@ -132,11 +141,14 @@ 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); + // 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..8ab1278 --- /dev/null +++ b/src/wrappers/sdl/clipboard.cppm @@ -0,0 +1,38 @@ +module; + +#include + +export module wrappers.sdl.clipboard; + +import std; +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/error.cppm b/src/wrappers/sdl/error.cppm index ceaedf0..0b12725 100644 --- a/src/wrappers/sdl/error.cppm +++ b/src/wrappers/sdl/error.cppm @@ -1,11 +1,11 @@ module; -#include -#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/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/init.cppm b/src/wrappers/sdl/init.cppm index 9044776..f673011 100644 --- a/src/wrappers/sdl/init.cppm +++ b/src/wrappers/sdl/init.cppm @@ -1,10 +1,10 @@ module; -#include -#include +#include export module wrappers.sdl.init; +import std; import wrappers.sdl.error; export namespace sdl @@ -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/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..e612c00 --- /dev/null +++ b/src/wrappers/sdl/mouse.cppm @@ -0,0 +1,29 @@ +module; + +#include +#include + +export module wrappers.sdl.mouse; + +import std; + +export namespace sdl +{ + using MouseButtonIndex = std::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 a42648a..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,30 +11,30 @@ 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. */ - constexpr bool is_empty() const + bool is_empty() const { return SDL_RectEmptyFloat(this); } }; /** - * 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. */ - constexpr bool is_empty() const + bool is_empty() const { return SDL_RectEmpty(this); } diff --git a/src/wrappers/sdl/render.cppm b/src/wrappers/sdl/render.cppm index 0cf34e1..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; @@ -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,14 +148,32 @@ 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. */ - std::pair CreateWindowAndRenderer( + std::pair create_window_and_renderer( const std::string& title, const int width, const int height, diff --git a/src/wrappers/sdl/surface.cppm b/src/wrappers/sdl/surface.cppm new file mode 100644 index 0000000..88cba61 --- /dev/null +++ b/src/wrappers/sdl/surface.cppm @@ -0,0 +1,98 @@ +module; + +#include +#include + +export module wrappers.sdl.surface; + +import std; +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..96dd45e --- /dev/null +++ b/src/wrappers/sdl/utils.cppm @@ -0,0 +1,23 @@ +module; + +#include + +export module wrappers.sdl.utils; + +import std; + +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..f393a43 100644 --- a/src/wrappers/sdl/video.cppm +++ b/src/wrappers/sdl/video.cppm @@ -1,17 +1,19 @@ module; #include -#include #include export module wrappers.sdl.video; +import std; 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..7312e9c 100644 --- a/src/wrappers/sdl_image.cppm +++ b/src/wrappers/sdl_image.cppm @@ -1,28 +1,43 @@ module; -#include #include #include export module wrappers.sdl_image; +import std; 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/tools/shuriken.py b/tools/shuriken.py index e1ae8aa..e5a1943 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 @@ -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)) @@ -186,8 +192,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 +202,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 +217,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 +311,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 +429,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) @@ -454,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', ['original_src', 'generated_src', 'pcm']) class NinjaFileGenerator: @@ -470,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): @@ -487,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'): @@ -499,12 +538,25 @@ 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( + original_src=str(module_src_path), + generated_src=f"$builddir/generated/stdlib/{module_name}.cppm", + 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 @@ -530,12 +582,18 @@ 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} # -- 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 @@ -544,13 +602,17 @@ 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 # 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 @@ -560,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 @@ -575,15 +637,23 @@ 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_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: @@ -600,23 +670,34 @@ 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.generated_src}: copy {target.original_src} + +build {target.pcm}: std_module {target.generated_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] = {} + # 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: @@ -624,15 +705,19 @@ 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}" + 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] = 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]] = {} # 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) @@ -640,11 +725,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() 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 +```