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/neofox.png b/assets/neofox.png new file mode 100644 index 0000000..64dd563 Binary files /dev/null and b/assets/neofox.png differ diff --git a/assets/sprites/neocat_64.png b/assets/sprites/neocat_64.png new file mode 100644 index 0000000..b8c0957 Binary files /dev/null and b/assets/sprites/neocat_64.png differ diff --git a/assets/sprites/neofox_64.png b/assets/sprites/neofox_64.png new file mode 100644 index 0000000..b00da05 Binary files /dev/null and b/assets/sprites/neofox_64.png differ diff --git a/assets/tilesets/terrain.png b/assets/tilesets/terrain.png new file mode 100644 index 0000000..d14db67 Binary files /dev/null and b/assets/tilesets/terrain.png 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 8dd0a71..0a07b3f 100644 --- a/shuriken.yaml +++ b/shuriken.yaml @@ -1,10 +1,22 @@ 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 targets: linux: output_file: rutile_game + # TODO: In a release build, set -DNDEBUG diff --git a/src/main.cpp b/src/main.cpp index cdfe69e..a6e0353 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,27 +1,30 @@ -import sdl_app; +import rutile.app; #define SDL_MAIN_USE_CALLBACKS #include -SDL_AppResult SDL_AppInit(void** appstate, int /*argc*/, char** /*argv*/) +SDL_AppResult SDL_AppInit(void** appstate, const int /*argc*/, char** /*argv*/) { - *appstate = new AppState; - return sdl_app_init(static_cast(*appstate)); -} - -SDL_AppResult SDL_AppIterate(void* appstate) -{ - return sdl_app_iterate(static_cast(appstate)); + auto* app = new rutile::App; + *appstate = app; + return static_cast(app->initialize()); } SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) { - return sdl_app_event(static_cast(appstate), event); + auto* app = static_cast(appstate); + return static_cast(app->handle_event(*event)); } -void SDL_AppQuit(void* appstate, SDL_AppResult /*result*/) +SDL_AppResult SDL_AppIterate(void* appstate) { - const auto* game_app_state = static_cast(appstate); - sdl_app_shutdown(game_app_state); - delete game_app_state; + 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); + app->shutdown(); + delete app; } diff --git a/src/rutile/app.cppm b/src/rutile/app.cppm new file mode 100644 index 0000000..37deff0 --- /dev/null +++ b/src/rutile/app.cppm @@ -0,0 +1,86 @@ +module; + +#include + +export module rutile.app; + +import std; +import rutile.config; +import rutile.core.engine; +import rutile.game.game; +import wrappers.sdl; + +export namespace rutile +{ + class App + { + std::unique_ptr engine_{nullptr}; + std::unique_ptr game_{nullptr}; + + public: + App() = default; + + sdl::AppResult initialize() + { + try { + // Set SDL application metadata + 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::initialize_sdl(sdl::InitFlags::Video | sdl::InitFlags::Events); + + // Create engine (includes window and renderer) and game state + engine_ = Engine::create(); + game_ = Game::create(*engine_); + } + catch (const std::runtime_error& e) { + std::cerr << "Unhandled exception during initialization: " << e.what() << '\n'; + return sdl::AppResult::Failure; + } + + return sdl::AppResult::Continue; + } + + sdl::AppResult handle_event(const sdl::Event& event) + { + try { + if (!engine_->handle_event(event)) { + game_->handle_event(event); + } + } + catch (const std::runtime_error& e) { + std::cerr << "Unhandled exception during event handling: " << e.what() << '\n'; + return sdl::AppResult::Failure; + } + + return engine_->keep_running() ? sdl::AppResult::Continue : sdl::AppResult::Success; + } + + sdl::AppResult iterate() + { + try { + engine_->update(); + game_->update(); + game_->render(); + } + catch (const std::runtime_error& e) { + std::cerr << "Unhandled exception during updating: " << e.what() << '\n'; + return sdl::AppResult::Failure; + } + + return engine_->keep_running() ? sdl::AppResult::Continue : sdl::AppResult::Success; + } + + void shutdown() const + { + engine_->shutdown(); + game_->shutdown(); + } + }; +} diff --git a/src/rutile/config.cppm b/src/rutile/config.cppm new file mode 100644 index 0000000..2e36783 --- /dev/null +++ b/src/rutile/config.cppm @@ -0,0 +1,36 @@ +module; + +#ifdef NDEBUG +#define DEBUG_BOOL false +#else +#define DEBUG_BOOL true +#endif + +export module rutile.config; + +import std; + +export namespace rutile::config +{ + constexpr auto debug = DEBUG_BOOL; + + constexpr auto app_name = "Rutile Game Prototype"; + constexpr auto app_version = "0.0.1-dev"; + constexpr auto app_identifier = "dev.binarydiv.rutile_game"; + constexpr auto app_creator = "binaryDiv"; + constexpr auto app_copyright = "Copyright (c) 2025 binaryDiv"; + constexpr auto app_url = "https://git.0xbd.space/binaryDiv/rutile-game"; + + constexpr auto window_width = 640; + constexpr auto window_height = 480; + + constexpr auto get_window_title() + { + if constexpr (debug) { + return std::format("{} ({}) [DEBUG]", app_name, app_version); + } + else { + return std::format("{} ({})", app_name, app_version); + } + } +} diff --git a/src/rutile/core/drawing/sprite.cppm b/src/rutile/core/drawing/sprite.cppm new file mode 100644 index 0000000..1158bde --- /dev/null +++ b/src/rutile/core/drawing/sprite.cppm @@ -0,0 +1,82 @@ +module; + +#include + +export module rutile.core.drawing.sprite; + +import std; +import rutile.core.render_server; +import wrappers.sdl; + +export namespace rutile +{ + class Sprite + { + // Texture ID (managed by TextureManager in RenderServer) + TextureID texture_id_; + + // Texture source boundaries: which part of the texture to render + sdl::FRect texture_boundaries_; + + // Texture offset: static offset that is added to the render position (BEFORE scale is applied) + sdl::FPoint texture_offset_{0, 0}; + + // Texture scale: factors applied to width/height (AFTER offset is applied) + // TODO: Proper vector class? + sdl::FPoint texture_scale_{1, 1}; + + // Sprite position: where to render the sprite (world coordinates) + sdl::FPoint position_{0, 0}; + + public: + explicit Sprite( + const TextureID texture_id, + const sdl::FRect texture_boundaries, + const sdl::FPoint position, + const sdl::FPoint texture_offset = {0, 0}, + const sdl::FPoint texture_scale = {1, 1} + ) + : texture_id_{texture_id}, + texture_boundaries_{texture_boundaries}, + texture_offset_{texture_offset}, + texture_scale_{texture_scale}, + position_{position} + { + assert(texture_boundaries_.w > 0 && texture_boundaries_.h > 0); + } + + static Sprite create_from_texture( + RenderServer& render_server, + const std::string& filename, + std::optional texture_boundaries, + auto... args + ) + { + const TextureID texture_id = render_server.load_texture(filename); + + if (!texture_boundaries.has_value()) { + const sdl::Texture& texture = render_server.get_texture(texture_id); + texture_boundaries = texture.get_boundaries(); + } + + return Sprite(texture_id, texture_boundaries.value(), args...); + } + + void set_position(const sdl::FPoint new_position) + { + position_ = new_position; + } + + void draw(const RenderServer& render_server) const + { + const sdl::FRect dest_rect{ + position_.x + (texture_scale_.x * texture_offset_.x), + position_.y + (texture_scale_.y * texture_offset_.y), + texture_scale_.x * texture_boundaries_.w, + texture_scale_.y * texture_boundaries_.h, + }; + + render_server.render_texture(texture_id_, &texture_boundaries_, &dest_rect); + } + }; +} diff --git a/src/rutile/core/drawing/tile_map.cppm b/src/rutile/core/drawing/tile_map.cppm new file mode 100644 index 0000000..de66b2c --- /dev/null +++ b/src/rutile/core/drawing/tile_map.cppm @@ -0,0 +1,108 @@ +module; + +#include + +export module rutile.core.drawing.tile_map; + +import std; +import rutile.core.drawing.tile_set; +import rutile.core.render_server; +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 = std::uint16_t; + + class TileMap + { + // Tile set ID + // TODO: Needed later when we centralize tile set management using a ResourceManager + [[maybe_unused]] + TileSetID tile_set_id_; + + // Tile size + int tile_size_; + + // Map boundaries + MapCoord width_; + MapCoord height_; + + // Tile map origin in world coordinates + sdl::FPoint position_{0, 0}; + + // Tile map data: Tiles in a continuous array + std::unique_ptr tiles_{nullptr}; + + public: + explicit TileMap( + const TileSetID tile_set_id, + const int tile_size, + const MapCoord width, + const MapCoord height, + const sdl::FPoint position = {0, 0} + ) + : tile_set_id_(tile_set_id), + tile_size_(tile_size), + width_(width), + height_(height), + position_{position} + { + // TODO: This maybe shouldn't be an assert... + assert(width_ > 0 && height_ > 0); + tiles_ = std::make_unique( + static_cast(width * height) + ); + } + + constexpr std::size_t map_coords_to_index(const MapCoord x, const MapCoord y) const + { + // TODO: Properly handle x<0, y<0, x>=width, y>=height + assert(x < width_ && y < height_); + return y * width_ + x; + } + + constexpr TileID get_tile(const MapCoord x, const MapCoord y) const + { + return tiles_[map_coords_to_index(x, y)]; + } + + constexpr void set_tile(const MapCoord x, const MapCoord y, const TileID tile_id) + { + tiles_[map_coords_to_index(x, y)] = tile_id; + } + + // TODO: Should we get the TileSet from the RenderServer, similar to how we get textures? + void draw(const RenderServer& render_server, const TileSet& tile_set) const + { + // Get tile set texture + const sdl::Texture& tile_set_texture = render_server.get_texture(tile_set.get_texture_id()); + + // TODO: Implement more efficiently (we could just iterate over tiles_ directly) + for (MapCoord y = 0; y < height_; y++) { + for (MapCoord x = 0; x < width_; x++) { + const int tile_index = get_tile(x, y); + + // Ignore tiles with ID 0 + if (tile_index == 0) { + continue; + } + + const auto [tile_atlas_src_rect] = tile_set.get_tile_definition(tile_index); + const sdl::FRect tile_dest_rect{ + position_.x + x * tile_size_, + position_.y + y * tile_size_, + static_cast(tile_size_), + static_cast(tile_size_), + }; + + render_server.render_texture( + tile_set_texture, + &tile_atlas_src_rect, + &tile_dest_rect + ); + } + } + } + }; +} diff --git a/src/rutile/core/drawing/tile_set.cppm b/src/rutile/core/drawing/tile_set.cppm new file mode 100644 index 0000000..ddad86c --- /dev/null +++ b/src/rutile/core/drawing/tile_set.cppm @@ -0,0 +1,92 @@ +module; + +#include + +export module rutile.core.drawing.tile_set; + +import std; +import rutile.core.render_server; +import wrappers.sdl; + +export namespace rutile +{ + using TileSetID = unsigned int; + using TileID = unsigned int; + + struct TileDefinition + { + sdl::FRect atlas_src_rect; + }; + + class TileSet + { + // Texture ID of tileset atlas texture + TextureID texture_id_; + + // Size of each tile in pixels + // TODO: Do we need this? + // int tile_size_; + + // Tile definitions: Maps tile ID to a struct describing the tile, e.g. the texture source boundaries, + // collision data, etc. + std::vector tile_definitions_; + + public: + TileSet() = delete; + + explicit TileSet( + const TextureID texture_id, + std::vector tile_definitions + ) + : texture_id_{texture_id}, + tile_definitions_{std::move(tile_definitions)} + {} + + static TileSet create_from_texture( + RenderServer& render_server, + const std::string& filename, + const int tile_size, + const std::vector& tile_atlas_coords + ) + { + const TextureID texture_id = render_server.load_texture(filename); + const sdl::Texture& texture = render_server.get_texture(texture_id); + + assert(tile_size > 0); + // TODO: These probably shouldn't be asserts... + assert(texture.get_width() % tile_size == 0); + assert(texture.get_height() % tile_size == 0); + + std::vector tile_definitions; + + // TODO: Should we automatically create tile 0 (no tile) instead of listing it in tile_atlas_coords? + for (const auto& [x, y] : tile_atlas_coords) { + const TileDefinition tile{ + { + static_cast(x * tile_size), + static_cast(y * tile_size), + static_cast(tile_size), + static_cast(tile_size), + } + }; + + // TODO: These probably shouldn't be asserts... + assert(tile.atlas_src_rect.x + tile.atlas_src_rect.w <= texture.get_width()); + assert(tile.atlas_src_rect.y + tile.atlas_src_rect.h <= texture.get_height()); + tile_definitions.push_back(tile); + } + + return TileSet(texture_id, tile_definitions); + } + + constexpr TextureID get_texture_id() const + { + return texture_id_; + } + + constexpr const TileDefinition& get_tile_definition(const TileID tile_id) const + { + return tile_definitions_.at(tile_id); + } + }; +} diff --git a/src/rutile/core/engine.cppm b/src/rutile/core/engine.cppm new file mode 100644 index 0000000..984f323 --- /dev/null +++ b/src/rutile/core/engine.cppm @@ -0,0 +1,118 @@ +module; + +#include + +export module rutile.core.engine; + +import std; +import rutile.config; +import rutile.core.render_server; +import rutile.core.ui.ui_server; +import wrappers.sdl; + +export namespace rutile +{ + class Engine + { + // Whether this class is currently instantiated (to prevent multiple instances) + static bool instantiated_; + + // If this is set to false, the application will exit + bool keep_running_ = true; + + 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)}, + ui_server_{render_server_.get_renderer(), window_} + { + instantiated_ = true; + } + + public: + Engine() = delete; + + // No copy or move operations + Engine(const Engine&) = delete; + Engine& operator=(const Engine&) = delete; + Engine(Engine&&) = delete; + Engine& operator=(Engine&&) = delete; + + ~Engine() + { + instantiated_ = false; + } + + static std::unique_ptr create() + { + // Prevent the class from being instantiated multiple times + assert(!instantiated_); + + 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 + { + return keep_running_; + } + + sdl::Window& get_window() + { + return window_; + } + + RenderServer& get_render_server() + { + return render_server_; + } + + UIServer& get_ui_server() + { + 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; + } + + void update() + { + } + + void shutdown() + { + } + }; + + bool Engine::instantiated_ = false; +} diff --git a/src/rutile/core/render_server.cppm b/src/rutile/core/render_server.cppm new file mode 100644 index 0000000..7366b13 --- /dev/null +++ b/src/rutile/core/render_server.cppm @@ -0,0 +1,89 @@ +module; + +export module rutile.core.render_server; + +import std; +import rutile.core.resource_manager; +import rutile.core.texture_loader; +import wrappers.sdl; + +export namespace rutile +{ + using TextureID = unsigned int; + using TextureManager = ResourceManager; + + class RenderServer + { + sdl::Renderer renderer_; + TextureLoader texture_loader_; + TextureManager texture_manager_; + + public: + RenderServer() = delete; + + explicit RenderServer(sdl::Renderer&& renderer) + : renderer_{std::move(renderer)}, + texture_loader_{renderer_}, + texture_manager_{texture_loader_} + { + renderer_.set_vsync(1); + } + + // No copy or move operations + RenderServer(const RenderServer&) = delete; + RenderServer& operator=(const RenderServer&) = delete; + RenderServer(RenderServer&&) = delete; + RenderServer& operator=(RenderServer&&) = delete; + + ~RenderServer() = default; + + constexpr sdl::Renderer& get_renderer() + { + return renderer_; + } + + /** + * Load a texture from a file (if not loaded yet) and return its texture ID. + */ + constexpr TextureID load_texture(const std::string& filename) + { + return texture_manager_.load_resource_by_name(filename); + } + + /** + * Get a reference to a texture by its texture ID. + */ + constexpr const sdl::Texture& get_texture(const TextureID texture_id) const + { + return texture_manager_.get_resource(texture_id); + } + + void start_frame() const + { + renderer_.clear(); + } + + void finish_frame() const + { + renderer_.present(); + } + + void render_texture( + const TextureID texture_id, + const sdl::FRect* src_rect, + const sdl::FRect* dest_rect + ) const + { + render_texture(get_texture(texture_id), src_rect, dest_rect); + } + + constexpr void render_texture( + const sdl::Texture& texture, + const sdl::FRect* src_rect, + const sdl::FRect* dest_rect + ) const + { + renderer_.render_texture(texture, src_rect, dest_rect); + } + }; +} diff --git a/src/rutile/core/resource_manager.cppm b/src/rutile/core/resource_manager.cppm new file mode 100644 index 0000000..b73f184 --- /dev/null +++ b/src/rutile/core/resource_manager.cppm @@ -0,0 +1,120 @@ +module; + +#include + +export module rutile.core.resource_manager; + +import std; + +export namespace rutile +{ + template + concept IsResourceLoader = requires(ResourceLoaderType loader, const std::string& name) + { + { loader.load_resource(name) } -> std::same_as; + }; + + template < + typename ResourceIDType, + typename ResourceType, + IsResourceLoader ResourceLoaderType + > + class ResourceManager + { + // Resource loader + ResourceLoaderType& resource_loader_; + + // Registry of loaded resources, array index is resource ID + std::vector resource_registry_; + + // Mapping of all loaded resources from (file) name to resource ID + std::map resource_name_map_; + + public: + ResourceManager() = delete; + + explicit ResourceManager(ResourceLoaderType& resource_loader) + : resource_loader_{resource_loader} + {} + + // No copy or move operations + ResourceManager(const ResourceManager&) = delete; + ResourceManager& operator=(const ResourceManager&) = delete; + ResourceManager(ResourceManager&&) = delete; + ResourceManager& operator=(ResourceManager&&) = delete; + + ~ResourceManager() = default; + + /** + * Adds a new resource to the registry and returns its ID. + */ + ResourceIDType add_resource(ResourceType&& resource) + { + // Add resource to end of vector + resource_registry_.push_back(std::move(resource)); + + // Return the index of the newly added resource + return static_cast(resource_registry_.size() - 1); + } + + /** + * Adds a new resource to the registry, associates the name with it and returns its ID. + */ + ResourceIDType add_resource(ResourceType&& resource, const std::string& name) + { + // Add resource + ResourceIDType resource_id = add_resource(std::move(resource)); + + // Associate name with resource ID for future access + resource_name_map_.emplace(name, resource_id); + + return resource_id; + } + + /** + * Gets the resource ID for a given resource name, or `std::nullopt` if the resource was not found. + */ + std::optional get_resource_id_by_name(const std::string& name) const + { + // Check if resource is already loaded + if ( + const auto search = resource_name_map_.find(name); + search != resource_name_map_.end() + ) { + // Return resource ID + return search->second; + } + + return std::nullopt; + } + + /** + * Loads a resource (e.g. from a file) and adds it to the registry if it isn't loaded yet. + * Returns the resource ID. + */ + ResourceIDType load_resource_by_name(const std::string& name) + { + // Check if resource is already loaded + if ( + auto resource_id = get_resource_id_by_name(name); + resource_id.has_value() + ) { + return resource_id.value(); + } + + // Load resource and add it to the registry + return add_resource(resource_loader_.load_resource(name), name); + } + + /** + * Returns a reference to the resource with the given ID. + * The reference is not guaranteed to be valid after adding new resources to the registry. + * Assumes that the resource ID is valid. + */ + const ResourceType& get_resource(const ResourceIDType resource_id) const + { + assert(resource_id < resource_registry_.size()); + return resource_registry_[resource_id]; + } + }; +} diff --git a/src/rutile/core/texture_loader.cppm b/src/rutile/core/texture_loader.cppm new file mode 100644 index 0000000..78cb3df --- /dev/null +++ b/src/rutile/core/texture_loader.cppm @@ -0,0 +1,34 @@ +module; + +export module rutile.core.texture_loader; + +import std; +import rutile.core.resource_manager; +import wrappers.sdl; +import wrappers.sdl_image; + +export namespace rutile +{ + class TextureLoader + { + sdl::Renderer& renderer_; + + public: + explicit TextureLoader(sdl::Renderer& renderer) + : renderer_{renderer} + {} + + // No copy or move operations (reference) + TextureLoader(const TextureLoader&) = delete; + TextureLoader& operator=(const TextureLoader&) = delete; + TextureLoader(TextureLoader&&) = delete; + TextureLoader& operator=(TextureLoader&&) = delete; + + ~TextureLoader() = default; + + sdl::Texture load_resource(const std::string& filename) const + { + 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..2dedff2 --- /dev/null +++ b/src/rutile/core/ui/backend/rmlui_input_handler.cppm @@ -0,0 +1,275 @@ +module; + +export module rutile.core.ui.backend.rmlui_input_handler; + +import wrappers.rmlui.core; +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..28724c9 --- /dev/null +++ b/src/rutile/core/ui/backend/rmlui_render_interface.cppm @@ -0,0 +1,198 @@ +// RmlUi RenderInterface implementation for SDL using SDLrenderer. +// Based on RmlUi example code: vendor/RmlUi/Backends/RmlUi_Renderer_SDL.{cpp,h} + +module; + +#include +#include + +export module rutile.core.ui.backend.rmlui_render_interface; + +import std; +import wrappers.rmlui.core; +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 + { + 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..3f2ac72 --- /dev/null +++ b/src/rutile/core/ui/backend/rmlui_system_interface.cppm @@ -0,0 +1,117 @@ +// RmlUi SystemInterface implementation for SDL. +// Based on RmlUi example code: vendor/RmlUi/Backends/RmlUi_Platform_SDL.{cpp,h} + +module; + +#include + +export module rutile.core.ui.backend.rmlui_system_interface; + +import std; +import utils.memory; +import wrappers.rmlui.core; +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..3e5d3ab --- /dev/null +++ b/src/rutile/core/ui/ui_server.cppm @@ -0,0 +1,124 @@ +module; + +#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.rmlui.core; +import wrappers.rmlui.debugger; +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 new file mode 100644 index 0000000..9f8ca08 --- /dev/null +++ b/src/rutile/game/game.cppm @@ -0,0 +1,161 @@ +module; + +#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; +import rutile.core.engine; +import rutile.core.render_server; +import wrappers.rmlui.core; +import wrappers.sdl; +import wrappers.sdl_image; + +export namespace rutile +{ + class Game + { + // Whether this class is currently instantiated (to prevent multiple instances) + static bool instantiated_; + + // Reference to the engine + Engine& engine_; + + // Sprites for testing + Sprite player_sprite_; + std::vector sprites_; + + // Tile set and tile map (TODO: tile set should be moved to resource manager in engine) + TileSet tile_set_; + TileMap tile_map_; + + // Private constructor + explicit Game(Engine& engine) + : engine_(engine), + player_sprite_{ + Sprite::create_from_texture( + engine_.get_render_server(), + "assets/sprites/neocat_64.png", + std::nullopt, + sdl::FPoint{0, 0}, + sdl::FPoint{-32, -32} + ) + }, + tile_set_{ + TileSet::create_from_texture( + engine_.get_render_server(), + "assets/tilesets/terrain.png", + 32, + { + // Tile 0: No tile + {0, 0}, + // Tile 1: Actual first tile in the texture + {0, 0}, + {1, 0}, + {2, 0}, + {3, 0}, + } + ) + }, + 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++) { + const auto tile_index = 2 + (x + y) % 2; + tile_map_.set_tile(x, y, tile_index); + } + } + + instantiated_ = true; + } + + public: + Game() = delete; + + // No copy or move operations because we have a reference to the engine + Game(const Game&) = delete; + Game& operator=(const Game&) = delete; + Game(Game&&) = delete; + Game& operator=(Game&&) = delete; + + ~Game() + { + instantiated_ = false; + } + + static std::unique_ptr create(Engine& engine) + { + // Prevent the class from being instantiated multiple times + assert(!instantiated_); + return std::unique_ptr{ + new Game(engine) + }; + } + + // Handles an SDL event. Returns true if the event has been handled. + bool handle_event(const sdl::Event& event) + { + if (event.type == sdl::EventTypes::MouseMotion) { + player_sprite_.set_position({event.motion.x, event.motion.y}); + return true; + } + + 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{-32, -32} + ) + ); + return true; + } + + return false; + } + + void update() + { + // TODO: Move this to Engine + engine_.get_ui_server().update(); + } + + void render() const + { + const auto& render_server = engine_.get_render_server(); + render_server.start_frame(); + + // Render tilemap + tile_map_.draw(render_server, tile_set_); + + // Render 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(); + } + + void shutdown() + { + } + }; + + bool Game::instantiated_ = false; +} diff --git a/src/sdl_app.cppm b/src/sdl_app.cppm deleted file mode 100644 index 7e14111..0000000 --- a/src/sdl_app.cppm +++ /dev/null @@ -1,95 +0,0 @@ -module; - -#include -#include -#include - -export module sdl_app; - -import sprite; - -export { - struct AppState - { - SDL_Window* window = nullptr; - SDL_Renderer* renderer = nullptr; - Sprite* sprite = nullptr; - }; - - SDL_AppResult sdl_panic( - const std::string& error_prefix, - SDL_Window* window = nullptr, - SDL_Renderer* renderer = nullptr - ) - { - std::cerr << error_prefix << ": " << SDL_GetError() << '\n'; - - if (renderer != nullptr) { - SDL_DestroyRenderer(renderer); - } - if (window != nullptr) { - SDL_DestroyWindow(window); - } - - SDL_Quit(); - return SDL_APP_FAILURE; - } - - SDL_AppResult sdl_app_init(AppState* app_state) - { - if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) { - return sdl_panic("SDL_Init error"); - } - - app_state->window = SDL_CreateWindow("BuildSystemTest", 640, 480, 0); - if (app_state->window == nullptr) { - return sdl_panic("SDL_CreateWindow error"); - } - - app_state->renderer = SDL_CreateRenderer(app_state->window, nullptr); - if (app_state->renderer == nullptr) { - return sdl_panic("SDL_CreateRenderer error", app_state->window); - } - - try { - app_state->sprite = new Sprite(app_state->renderer, "assets/neocat.png", 100, 100); - } - catch (const std::runtime_error& e) { - return sdl_panic(e.what(), app_state->window, app_state->renderer); - } - - return SDL_APP_CONTINUE; - } - - SDL_AppResult sdl_app_event(const AppState* app_state, const SDL_Event* event) - { - if (event->type == SDL_EVENT_QUIT) { - return SDL_APP_SUCCESS; - } - - if (event->type == SDL_EVENT_MOUSE_MOTION) { - app_state->sprite->move( - event->motion.x - 50, - event->motion.y - 50 - ); - } - - return SDL_APP_CONTINUE; - } - - SDL_AppResult sdl_app_iterate(const AppState* app_state) - { - SDL_RenderClear(app_state->renderer); - app_state->sprite->render(app_state->renderer); - SDL_RenderPresent(app_state->renderer); - - return SDL_APP_CONTINUE; - } - - void sdl_app_shutdown(const AppState* app_state) - { - delete app_state->sprite; - SDL_DestroyRenderer(app_state->renderer); - SDL_DestroyWindow(app_state->window); - } -} diff --git a/src/sprite.cppm b/src/sprite.cppm deleted file mode 100644 index 3318180..0000000 --- a/src/sprite.cppm +++ /dev/null @@ -1,81 +0,0 @@ -module; - -#include -#include -#include -#include - -export module sprite; - -export class Sprite -{ - SDL_Texture* sdl_texture; - SDL_FRect dest_rect{0, 0, 0, 0}; - - public: - explicit Sprite( - SDL_Renderer* renderer, - const std::string& filename, - const int width, - const int height - ) - { - SDL_Surface* texture_surface = IMG_Load(filename.c_str()); - if (texture_surface == nullptr) { - throw std::runtime_error("IMG_Load error"); - } - - sdl_texture = SDL_CreateTextureFromSurface(renderer, texture_surface); - SDL_DestroySurface(texture_surface); - - if (sdl_texture == nullptr) { - throw std::runtime_error("SDL_CreateTextureFromSurface error"); - } - - dest_rect.w = static_cast(width); - dest_rect.h = static_cast(height); - } - - // Don't allow copy operations - Sprite(const Sprite&) = delete; - Sprite& operator=(const Sprite&) = delete; - - // Move constructor - Sprite(Sprite&& other) noexcept - : sdl_texture(other.sdl_texture), - dest_rect(other.dest_rect) - { - other.sdl_texture = nullptr; - } - - // Move assignment - Sprite& operator=(Sprite&& other) noexcept - { - // Move inner resources from other - sdl_texture = other.sdl_texture; - dest_rect = other.dest_rect; - - // Reset other to make it safe for deletion - other.sdl_texture = nullptr; - - return *this; - } - - ~Sprite() - { - if (sdl_texture != nullptr) { - SDL_DestroyTexture(sdl_texture); - } - } - - void move(const float x, const float y) - { - dest_rect.x = x; - dest_rect.y = y; - } - - void render(SDL_Renderer* renderer) const - { - SDL_RenderTexture(renderer, sdl_texture, nullptr, &dest_rect); - } -}; diff --git a/src/utils/memory.cppm b/src/utils/memory.cppm new file mode 100644 index 0000000..d1e16ad --- /dev/null +++ b/src/utils/memory.cppm @@ -0,0 +1,21 @@ +module; + +export module utils.memory; + +export namespace utils +{ + /** + * Template to generate deleters for `std::unique_ptr` from functions, e.g. to free SDL resources. + * + * @tparam delete_func Function that takes a pointer to a resource and deletes the resource. + */ + template + struct FuncDeleter + { + template + constexpr void operator()(T* ptr) const noexcept + { + delete_func(ptr); + } + }; +} diff --git a/src/wrappers/rmlui/core.cppm b/src/wrappers/rmlui/core.cppm new file mode 100644 index 0000000..fa209d4 --- /dev/null +++ b/src/wrappers/rmlui/core.cppm @@ -0,0 +1,530 @@ +module; + +#include + +export module wrappers.rmlui.core; + +export +{ + namespace Rml + { + using Rml::Animation; + using Rml::AnimationList; + using Rml::Any; + using Rml::Array; + using Rml::ArrayDefinition; + using Rml::Assert; + using Rml::AttributeNameList; + using Rml::BasePointerDefinition; + using Rml::BaseXMLParser; + using Rml::BlendMode; + using Rml::Box; + using Rml::BoxArea; + using Rml::BoxDirection; + using Rml::BoxEdge; + using Rml::BoxShadow; + using Rml::BoxShadowList; + using Rml::CallbackTexture; + using Rml::CallbackTextureFunction; + using Rml::CallbackTextureInterface; + using Rml::CallbackTextureSource; + using Rml::Character; + using Rml::ClipMaskGeometry; + using Rml::ClipMaskGeometryList; + using Rml::ClipMaskOperation; + using Rml::ColorFormat; + using Rml::ColorStop; + using Rml::ColorStopList; + using Rml::Colour; + using Rml::Colourb; + using Rml::ColourbPremultiplied; + using Rml::Colourf; + using Rml::ColumnMajorMatrix4f; + using Rml::ColumnMajorStorage; + using Rml::CompiledFilter; + using Rml::CompiledFilterHandle; + using Rml::CompiledGeometryHandle; + using Rml::CompiledShader; + using Rml::CompiledShaderHandle; + using Rml::ComputedValues; + using Rml::ContainerBox; + using Rml::Context; + using Rml::ContextInstancer; + using Rml::ContextPtr; + using Rml::ConvolutionFilter; + using Rml::CornerSizes; + using Rml::CreateContext; + using Rml::CreateString; + using Rml::DataAddress; + using Rml::DataAddressEntry; + using Rml::DataController; + using Rml::DataControllerInstancer; + using Rml::DataControllerPtr; + using Rml::DataEventFunc; + using Rml::DataGetFunc; + using Rml::DataModel; + using Rml::DataModelConstructor; + using Rml::DataModelHandle; + using Rml::DataSetFunc; + using Rml::DataTransformFunc; + using Rml::DataTypeGetFunc; + using Rml::DataTypeRegister; + using Rml::DataTypeSetFunc; + using Rml::DataVariable; + using Rml::DataVariableType; + using Rml::DataView; + using Rml::DataViewInstancer; + using Rml::DataViewPtr; + using Rml::Decorator; + using Rml::DecoratorDataHandle; + using Rml::DecoratorDeclaration; + using Rml::DecoratorDeclarationList; + using Rml::DecoratorInstancer; + using Rml::DecoratorInstancerInterface; + using Rml::DecoratorPtrList; + using Rml::DecoratorsPtr; + using Rml::DefaultActionPhase; + using Rml::DefaultStyleSheetParsers; + using Rml::Dictionary; + using Rml::DirtyVariables; + using Rml::DocumentHeader; + using Rml::EdgeSizes; + using Rml::EffectSpecification; + using Rml::Element; + using Rml::ElementAnimation; + using Rml::ElementAnimationList; + using Rml::ElementAttributes; + using Rml::ElementBackgroundBorder; + using Rml::ElementDefinition; + using Rml::ElementDocument; + using Rml::ElementForm; + using Rml::ElementFormControl; + using Rml::ElementFormControlInput; + using Rml::ElementFormControlSelect; + using Rml::ElementFormControlTextArea; + using Rml::ElementInstancer; + using Rml::ElementInstancerElement; + using Rml::ElementInstancerGeneric; + using Rml::ElementInstancerText; + using Rml::ElementList; + using Rml::ElementMeta; + using Rml::ElementProgress; + using Rml::ElementPtr; + using Rml::ElementScroll; + using Rml::ElementStyle; + using Rml::ElementTabSet; + using Rml::ElementText; + using Rml::ElementUtilities; + using Rml::EnableObserverPtr; + using Rml::Event; + using Rml::EventDispatcher; + using Rml::EventId; + using Rml::EventInstancer; + using Rml::EventListener; + using Rml::EventListenerInstancer; + using Rml::EventPhase; + using Rml::EventPtr; + using Rml::EventSpecification; + using Rml::Factory; + using Rml::Family; + using Rml::FamilyBase; + using Rml::FamilyId; + using Rml::FileHandle; + using Rml::FileInterface; + using Rml::Filter; + using Rml::FilterDeclaration; + using Rml::FilterDeclarationList; + using Rml::FilterHandleList; + using Rml::FilterInstancer; + using Rml::FilterOperation; + using Rml::FiltersPtr; + using Rml::FocusFlag; + using Rml::FontEffect; + using Rml::FontEffectInstancer; + using Rml::FontEffectList; + using Rml::FontEffects; + using Rml::FontEffectsHandle; + using Rml::FontEffectsPtr; + using Rml::FontEngineInterface; + using Rml::FontFaceHandle; + using Rml::FontGlyph; + using Rml::FontGlyphMap; + using Rml::FontMetrics; + using Rml::FormatString; + using Rml::FromString; + using Rml::FuncDefinition; + using Rml::Function; + using Rml::Geometry; + using Rml::Get; + using Rml::GetContext; + using Rml::GetFileInterface; + using Rml::GetFontEngineInterface; + using Rml::GetIf; + using Rml::GetNumContexts; + using Rml::GetRenderInterface; + using Rml::GetSystemInterface; + using Rml::GetTextInputHandler; + using Rml::GetTextureSourceList; + using Rml::GetVersion; + using Rml::Hash; + using Rml::Initialise; + using Rml::InlineLevelBox; + using Rml::InputType; + using Rml::IsVoidMemberFunc; + using Rml::KeyframeBlock; + using Rml::Keyframes; + using Rml::KeyframesMap; + using Rml::LayerHandle; + using Rml::LayoutEngine; + using Rml::List; + using Rml::LoadFontFace; + using Rml::Log; + using Rml::MakeLiteralIntVariable; + using Rml::MakeMoveIterator; + using Rml::MakeShared; + using Rml::MakeUnique; + using Rml::Matrix4; + using Rml::Matrix4f; + using Rml::MatrixStorageBase; + using Rml::MediaBlock; + using Rml::MediaBlockList; + using Rml::MediaQueryId; + using Rml::MediaQueryModifier; + using Rml::MemberGetFunc; + using Rml::MemberGetFuncDefinition; + using Rml::MemberGetterFunc; + using Rml::MemberObjectDefinition; + using Rml::MemberScalarGetSetFuncDefinition; + using Rml::MemberSetFunc; + using Rml::MemberSetterFunc; + using Rml::Mesh; + using Rml::MeshUtilities; + using Rml::ModalFlag; + using Rml::NamedDecorator; + using Rml::NamedDecoratorMap; + using Rml::NavigationSearchDirection; + using Rml::NonCopyMoveable; + using Rml::NumericValue; + using Rml::ObserverPtr; + using Rml::OwnedElementList; + using Rml::Pair; + using Rml::ParameterMap; + using Rml::Plugin; + using Rml::PointerDefinition; + using Rml::PointerTraits; + using Rml::PropertiesIterator; + using Rml::PropertiesIteratorView; + using Rml::Property; + using Rml::PropertyDefinition; + using Rml::PropertyDictionary; + using Rml::PropertyId; + using Rml::PropertyIdNameMap; + using Rml::PropertyIdSet; + using Rml::PropertyIdSetIterator; + using Rml::PropertyMap; + using Rml::PropertyParser; + using Rml::PropertySource; + using Rml::PropertySpecification; + using Rml::Queue; + using Rml::Rectangle; + using Rml::Rectanglef; + using Rml::Rectanglei; + using Rml::RegisterEventType; + using Rml::RegisterPlugin; + using Rml::RelativeTarget; + using Rml::Releasable; + using Rml::ReleaseCompiledGeometry; + using Rml::ReleaseFontResources; + using Rml::ReleaseRenderManagers; + using Rml::ReleaseTexture; + using Rml::ReleaseTextures; + using Rml::Releaser; + using Rml::ReleaserBase; + using Rml::RemoveContext; + using Rml::RenderBox; + using Rml::RenderInterface; + using Rml::RenderManager; + using Rml::RenderManagerAccess; + using Rml::RenderState; + using Rml::ReplacedBox; + using Rml::ResolveValue; + using Rml::ResolveValueOr; + using Rml::RowMajorMatrix4f; + using Rml::RowMajorStorage; + using Rml::ScalarDefinition; + using Rml::ScalarFuncDefinition; + using Rml::ScriptInterface; + using Rml::ScriptInterfacePtr; + using Rml::ScriptObject; + using Rml::ScrollAlignment; + using Rml::ScrollBehavior; + using Rml::ScrollController; + using Rml::ScrollIntoViewOptions; + using Rml::ScrollParentage; + using Rml::SetFileInterface; + using Rml::SetFontEngineInterface; + using Rml::SetRenderInterface; + using Rml::SetSystemInterface; + using Rml::SetTextInputHandler; + using Rml::SharedPtr; + using Rml::ShorthandDefinition; + using Rml::ShorthandId; + using Rml::ShorthandIdNameMap; + using Rml::ShorthandType; + using Rml::Shutdown; + using Rml::SmallOrderedMap; + using Rml::SmallOrderedSet; + using Rml::SmallUnorderedMap; + using Rml::SmallUnorderedSet; + using Rml::Span; + using Rml::Sprite; + using Rml::SpriteDefinitionList; + using Rml::SpriteMap; + using Rml::Spritesheet; + using Rml::SpritesheetList; + using Rml::StableMap; + using Rml::StableUnorderedMap; + using Rml::StableVector; + using Rml::StableVectorIndex; + using Rml::Stack; + using Rml::StackingContextChild; + using Rml::Stream; + using Rml::String; + using Rml::StringIteratorU8; + using Rml::StringList; + using Rml::StringView; + using Rml::StructDefinition; + using Rml::StructHandle; + using Rml::StyleSheet; + using Rml::StyleSheetContainer; + using Rml::StyleSheetIndex; + using Rml::StyleSheetNode; + using Rml::StyleSheetParser; + using Rml::StyleSheetSpecification; + using Rml::SystemInterface; + using Rml::TextInputHandler; + using Rml::TextShapingContext; + using Rml::Texture; + using Rml::TextureDatabase; + using Rml::TextureFileIndex; + using Rml::TextureHandle; + using Rml::TextureSource; + using Rml::TexturedMesh; + using Rml::TexturedMeshList; + using Rml::ToString; + using Rml::Touch; + using Rml::TouchId; + using Rml::TouchList; + using Rml::Transform; + using Rml::TransformFuncRegister; + using Rml::TransformPrimitive; + using Rml::TransformPtr; + using Rml::TransformState; + using Rml::Transition; + using Rml::TransitionList; + using Rml::Tween; + using Rml::TypeConverter; + using Rml::URL; + using Rml::UniquePtr; + using Rml::UniqueReleaserPtr; + using Rml::UniqueRenderResource; + using Rml::Unit; + using Rml::Units; + using Rml::UnorderedMap; + using Rml::UnorderedMultimap; + using Rml::UnorderedSet; + using Rml::UnregisterPlugin; + using Rml::VariableDefinition; + using Rml::Variant; + using Rml::VariantList; + using Rml::Vector2; + using Rml::Vector2f; + using Rml::Vector2i; + using Rml::Vector3; + using Rml::Vector3f; + using Rml::Vector3i; + using Rml::Vector4; + using Rml::Vector4f; + using Rml::Vector4i; + using Rml::Vector; + using Rml::Vertex; + using Rml::VoidMemberFunc; + using Rml::WeakPtr; + using Rml::WidgetDropDown; + using Rml::WidgetScroll; + using Rml::WidgetTextInputMultiLine; + using Rml::XMLAttributes; + using Rml::XMLDataType; + using Rml::XMLNodeHandler; + using Rml::XMLParser; + using Rml::byte; + using Rml::is_builtin_data_scalar; + using Rml::voidPtr; + + namespace Input + { + using Rml::Input::KeyIdentifier; + using Rml::Input::KeyModifier; + } + + namespace Math + { + using Rml::Math::ACos; + using Rml::Math::ASin; + using Rml::Math::ATan2; + using Rml::Math::Absolute; + using Rml::Math::Clamp; + using Rml::Math::Cos; + using Rml::Math::DecomposeFractionalIntegral; + using Rml::Math::DegreesToRadians; + using Rml::Math::Exp; + using Rml::Math::ExpandToPixelGrid; + using Rml::Math::HexToDecimal; + using Rml::Math::IsCloseToZero; + using Rml::Math::Lerp; + using Rml::Math::Log2; + using Rml::Math::Max; + using Rml::Math::Min; + using Rml::Math::NormaliseAngle; + using Rml::Math::RadiansToDegrees; + using Rml::Math::RandomBool; + using Rml::Math::RandomInteger; + using Rml::Math::RandomReal; + using Rml::Math::Round; + using Rml::Math::RoundDown; + using Rml::Math::RoundDownToInteger; + using Rml::Math::RoundToInteger; + using Rml::Math::RoundUp; + using Rml::Math::RoundUpToInteger; + using Rml::Math::RoundedLerp; + using Rml::Math::Sin; + using Rml::Math::SnapToPixelGrid; + using Rml::Math::SquareRoot; + using Rml::Math::Tan; + using Rml::Math::ToPowerOfTwo; + } + + namespace StringUtilities + { + using Rml::StringUtilities::BytesUTF8; + using Rml::StringUtilities::ConvertByteOffsetToCharacterOffset; + using Rml::StringUtilities::ConvertCharacterOffsetToByteOffset; + using Rml::StringUtilities::DecodeRml; + using Rml::StringUtilities::EncodeRml; + using Rml::StringUtilities::EndsWith; + using Rml::StringUtilities::ExpandString; + using Rml::StringUtilities::IsWhitespace; + using Rml::StringUtilities::JoinString; + using Rml::StringUtilities::LengthUTF8; + using Rml::StringUtilities::Replace; + using Rml::StringUtilities::SeekBackwardUTF8; + using Rml::StringUtilities::SeekForwardUTF8; + using Rml::StringUtilities::StartsWith; + using Rml::StringUtilities::StringCompareCaseInsensitive; + using Rml::StringUtilities::StripWhitespace; + using Rml::StringUtilities::ToCharacter; + using Rml::StringUtilities::ToLower; + using Rml::StringUtilities::ToUTF8; + using Rml::StringUtilities::ToUpper; + using Rml::StringUtilities::TrimTrailingDotZeros; + } + + namespace Style + { + using Rml::Style::AlignContent; + using Rml::Style::AlignItems; + using Rml::Style::AlignSelf; + using Rml::Style::Bottom; + using Rml::Style::BoxSizing; + using Rml::Style::Clear; + using Rml::Style::Clip; + using Rml::Style::CommonValues; + using Rml::Style::ComputedValues; + using Rml::Style::Direction; + using Rml::Style::Display; + using Rml::Style::Drag; + using Rml::Style::FlexBasis; + using Rml::Style::FlexDirection; + using Rml::Style::FlexWrap; + using Rml::Style::Float; + using Rml::Style::Focus; + using Rml::Style::FontKerning; + using Rml::Style::FontStyle; + using Rml::Style::FontWeight; + using Rml::Style::Height; + using Rml::Style::InheritedValues; + using Rml::Style::JustifyContent; + using Rml::Style::Left; + using Rml::Style::LengthPercentage; + using Rml::Style::LengthPercentageAuto; + using Rml::Style::LineHeight; + using Rml::Style::Margin; + using Rml::Style::MaxHeight; + using Rml::Style::MaxWidth; + using Rml::Style::MinHeight; + using Rml::Style::MinWidth; + using Rml::Style::Nav; + using Rml::Style::NumberAuto; + using Rml::Style::OriginX; + using Rml::Style::OriginY; + using Rml::Style::Overflow; + using Rml::Style::OverscrollBehavior; + using Rml::Style::Padding; + using Rml::Style::PerspectiveOrigin; + using Rml::Style::PointerEvents; + using Rml::Style::Position; + using Rml::Style::RareValues; + using Rml::Style::Right; + using Rml::Style::TabIndex; + using Rml::Style::TextAlign; + using Rml::Style::TextDecoration; + using Rml::Style::TextOverflow; + using Rml::Style::TextTransform; + using Rml::Style::Top; + using Rml::Style::TransformOrigin; + using Rml::Style::VerticalAlign; + using Rml::Style::Visibility; + using Rml::Style::WhiteSpace; + using Rml::Style::Width; + using Rml::Style::WordBreak; + using Rml::Style::ZIndex; + } + + namespace Transforms + { + using Rml::Transforms::DecomposedMatrix4; + using Rml::Transforms::Matrix2D; + using Rml::Transforms::Matrix3D; + using Rml::Transforms::Perspective; + using Rml::Transforms::ResolvedPrimitive; + using Rml::Transforms::Rotate2D; + using Rml::Transforms::Rotate3D; + using Rml::Transforms::RotateX; + using Rml::Transforms::RotateY; + using Rml::Transforms::RotateZ; + using Rml::Transforms::Scale2D; + using Rml::Transforms::Scale3D; + using Rml::Transforms::ScaleX; + using Rml::Transforms::ScaleY; + using Rml::Transforms::ScaleZ; + using Rml::Transforms::Skew2D; + using Rml::Transforms::SkewX; + using Rml::Transforms::SkewY; + using Rml::Transforms::Translate2D; + using Rml::Transforms::Translate3D; + using Rml::Transforms::TranslateX; + using Rml::Transforms::TranslateY; + using Rml::Transforms::TranslateZ; + using Rml::Transforms::UnresolvedPrimitive; + } + + namespace Utilities + { + using Rml::Utilities::HashCombine; + } + } + + using ::rmlui_dynamic_cast; + using ::rmlui_static_cast; + using ::rmlui_type_name; +} diff --git a/src/wrappers/rmlui/debugger.cppm b/src/wrappers/rmlui/debugger.cppm new file mode 100644 index 0000000..7dd319a --- /dev/null +++ b/src/wrappers/rmlui/debugger.cppm @@ -0,0 +1,14 @@ +module; + +#include + +export module wrappers.rmlui.debugger; + +export namespace Rml::Debugger +{ + using Rml::Debugger::Initialise; + using Rml::Debugger::IsVisible; + using Rml::Debugger::SetContext; + using Rml::Debugger::SetVisible; + using Rml::Debugger::Shutdown; +} diff --git a/src/wrappers/sdl.cppm b/src/wrappers/sdl.cppm new file mode 100644 index 0000000..78f7c72 --- /dev/null +++ b/src/wrappers/sdl.cppm @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..0b12725 --- /dev/null +++ b/src/wrappers/sdl/error.cppm @@ -0,0 +1,24 @@ +module; + +#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). + // SDL_GetError() is used in the constructor to get the error message unless specified explicitly. + class SDLException final : public std::runtime_error + { + public: + explicit SDLException(const std::string& sdl_func, const std::string& sdl_error) + : runtime_error(std::format("Error in SDL function {}: {}", sdl_func, sdl_error)) + {} + + explicit SDLException(const std::string& sdl_func) + : SDLException(sdl_func, SDL_GetError()) + {} + }; +} diff --git a/src/wrappers/sdl/events.cppm b/src/wrappers/sdl/events.cppm new file mode 100644 index 0000000..8ef341a --- /dev/null +++ b/src/wrappers/sdl/events.cppm @@ -0,0 +1,47 @@ +module; + +#include + +export module wrappers.sdl.events; + +export namespace sdl +{ + // Simple alias for SDL_Event union + using Event = SDL_Event; + + // Alias for EventType enum + using EventType = SDL_EventType; + + /** + * Wrapper for the SDL_EventType enum. + * + * 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 EventTypes + { + // 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 new file mode 100644 index 0000000..f673011 --- /dev/null +++ b/src/wrappers/sdl/init.cppm @@ -0,0 +1,62 @@ +module; + +#include + +export module wrappers.sdl.init; + +import std; +import wrappers.sdl.error; + +export namespace sdl +{ + using InitFlags_t = SDL_InitFlags; + + /** + * Wrapper for the SDL_InitFlags enum/constants. + * + * We're using a namespace here to emulate an enum-like interface. If we used an actual enum, we would have to + * explicitly define bit-wise operations for it (as well as for every other kind of bit flag enum. + * (Maybe in the future this can be replaced with a more elegant template-based solution or something.) + */ + namespace InitFlags + { + constexpr InitFlags_t Audio = SDL_INIT_AUDIO; + constexpr InitFlags_t Video = SDL_INIT_VIDEO; + constexpr InitFlags_t Joystick = SDL_INIT_JOYSTICK; + constexpr InitFlags_t Haptic = SDL_INIT_HAPTIC; + constexpr InitFlags_t Gamepad = SDL_INIT_GAMEPAD; + constexpr InitFlags_t Events = SDL_INIT_EVENTS; + constexpr InitFlags_t Sensor = SDL_INIT_SENSOR; + constexpr InitFlags_t Camera = SDL_INIT_CAMERA; + } + + /** + * Wrapper for the SDL_AppResult enum. + */ + enum class AppResult : std::underlying_type_t + { + Continue = SDL_APP_CONTINUE, + Success = SDL_APP_SUCCESS, + Failure = SDL_APP_FAILURE, + }; + + /** + * Wrapper for SDL_Init(). + */ + constexpr void initialize_sdl(const InitFlags_t flags) + { + if (!SDL_Init(flags)) { + throw SDLException("SDL_Init"); + } + } + + /** + * Wrapper for SDL_SetAppMetadataProperty(). + */ + 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 new file mode 100644 index 0000000..6e5dd32 --- /dev/null +++ b/src/wrappers/sdl/rect.cppm @@ -0,0 +1,42 @@ +module; + +#include + +export module wrappers.sdl.rect; + +export namespace sdl +{ + // Wrappers around SDL_Point and SDL_FPoint (change to wrapper classes when needed) + using FPoint = SDL_FPoint; + using Point = SDL_Point; + + /** + * Wrapper for SDL_FRect using class inheritance. + */ + class FRect : public SDL_FRect + { + /** + * Wrapper for SDL_RectEmptyFloat(). + * @return True if the floating point rectangle takes no space. + */ + bool is_empty() const + { + return SDL_RectEmptyFloat(this); + } + }; + + /** + * Wrapper for SDL_Rect using class inheritance. + */ + class Rect : public SDL_Rect + { + /** + * Wrapper for SDL_RectEmpty(). + * @return True if the rectangle takes no space. + */ + bool is_empty() const + { + return SDL_RectEmpty(this); + } + }; +} diff --git a/src/wrappers/sdl/render.cppm b/src/wrappers/sdl/render.cppm new file mode 100644 index 0000000..a556c3f --- /dev/null +++ b/src/wrappers/sdl/render.cppm @@ -0,0 +1,194 @@ +module; + +#include +#include + +export module wrappers.sdl.render; + +import std; +import utils.memory; +import wrappers.sdl.error; +import wrappers.sdl.rect; +import wrappers.sdl.video; + +export namespace sdl +{ + using Vertex = SDL_Vertex; + + /** + * Wrapper for SDL_Texture that manages its lifecycle with a unique_ptr. + */ + class Texture + { + std::unique_ptr< + SDL_Texture, + utils::FuncDeleter + > raw_texture_; + + public: + Texture() = delete; + + explicit Texture(SDL_Texture* raw_texture) + : raw_texture_{raw_texture} + {} + + constexpr SDL_Texture* get_raw() const + { + assert(raw_texture_); + return raw_texture_.get(); + } + + constexpr int get_width() const + { + return raw_texture_->w; + } + + constexpr int get_height() const + { + return raw_texture_->h; + } + + constexpr FRect get_boundaries() const + { + return {0, 0, static_cast(raw_texture_->w), static_cast(raw_texture_->h)}; + } + }; + + /** + * Wrapper for SDL_Renderer that manages its lifecycle with a unique_ptr. + */ + class Renderer + { + std::unique_ptr< + SDL_Renderer, + utils::FuncDeleter + > raw_renderer_; + + public: + Renderer() = delete; + + explicit Renderer(SDL_Renderer* raw_renderer) + : raw_renderer_{raw_renderer} + {} + + constexpr SDL_Renderer* get_raw() const + { + assert(raw_renderer_); + return raw_renderer_.get(); + } + + /** + * 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 + { + // TODO: These functions probably should never fail? Change this to a runtime exception if necessary. + if (!SDL_RenderClear(get_raw())) { + assert(!"SDL_RenderClear returned false"); + } + } + + /** + * Wrapper for SDL_RenderPresent(). + * Uses assert to verify the function returned true. + */ + constexpr void present() const + { + if (!SDL_RenderPresent(get_raw())) { + assert(!"SDL_RenderPresent returned false"); + } + } + + /** + * Wrapper for SDL_RenderTexture(). + * Uses assert to verify the function returned true. + */ + constexpr void render_texture( + const Texture& texture, + const FRect* src_rect, + const FRect* dest_rect + ) const + { + if (!SDL_RenderTexture(get_raw(), texture.get_raw(), src_rect, dest_rect)) { + 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 for SDL_CreateWindowAndRenderer(). + * Returns a pair of a Window and a Renderer object. + * Throws an SDLException on failure. + */ + std::pair create_window_and_renderer( + const std::string& title, + const int width, + const int height, + const SDL_WindowFlags window_flags + ) + { + SDL_Window* raw_window = nullptr; + SDL_Renderer* raw_renderer = nullptr; + + if (!SDL_CreateWindowAndRenderer( + title.c_str(), width, height, window_flags, &raw_window, &raw_renderer + )) { + throw SDLException("SDL_CreateWindowAndRenderer"); + } + + return {Window{raw_window}, Renderer{raw_renderer}}; + } +} 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 new file mode 100644 index 0000000..f393a43 --- /dev/null +++ b/src/wrappers/sdl/video.cppm @@ -0,0 +1,101 @@ +module; + +#include +#include + +export module wrappers.sdl.video; + +import std; +import utils.memory; +import wrappers.sdl.error; +import wrappers.sdl.rect; + +export namespace sdl +{ + /** + * Wrapper for SDL_Window that manages its lifecycle with a unique_ptr. + */ + class Window + { + std::unique_ptr< + SDL_Window, + utils::FuncDeleter + > raw_window_; + + public: + Window() = delete; + + explicit Window(SDL_Window* raw_window) + : raw_window_{raw_window} + {} + + constexpr SDL_Window* get_raw() const + { + 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 new file mode 100644 index 0000000..7312e9c --- /dev/null +++ b/src/wrappers/sdl_image.cppm @@ -0,0 +1,43 @@ +module; + +#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 for IMG_Load(). + */ + sdl::Surface load(const std::string& filename) + { + SDL_Surface* raw_surface = IMG_Load(filename.c_str()); + + 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{raw_texture}; + } +} diff --git a/tools/shuriken.py b/tools/shuriken.py index e1ae8aa..b7f38a7 100755 --- a/tools/shuriken.py +++ b/tools/shuriken.py @@ -3,14 +3,13 @@ ############################################ # 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 ############################################ import argparse -import copy import json import shlex import subprocess @@ -18,7 +17,7 @@ import sys from collections import namedtuple from dataclasses import asdict, dataclass, field from pathlib import Path -from typing import Any +from typing import Any, Self import yaml @@ -60,10 +59,43 @@ class TargetConfig: return f"{self.linker_args or ''} {self.linker_args_extra or ''}".strip() +@dataclass(kw_only=True) +class MergedTargetConfig(TargetConfig): + cpp_compiler: str = '' + cpp_standard: str = '' + cpp_flags: str = '' + cpp_flags_extra: str = '' + + linker_flags: str = '' + linker_flags_extra: str = '' + linker_args: str = '' + linker_args_extra: str = '' + + output_file: str = '' + run_command: str = '' + + @classmethod + def create_from(cls, target_defaults: TargetConfig, target_config: TargetConfig) -> Self: + merged_config = cls() + merged_config.update(asdict(target_defaults)) + merged_config.update(asdict(target_config)) + return merged_config + + def validate(self) -> None: + # Validate that all required fields are set + if not self.cpp_compiler: + raise ConfigValidationException('cpp_compiler must be set') + if not self.cpp_standard: + raise ConfigValidationException('cpp_standard must be set') + if not self.output_file: + raise ConfigValidationException('output_file must be set') + + @dataclass(kw_only=True) 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') @@ -71,14 +103,16 @@ class ShurikenConfig: defaults: TargetConfig = field(default_factory=lambda: TargetConfig( cpp_compiler='clang++', cpp_standard='c++20', - cpp_flags='', - linker_flags='', - linker_args='', )) + enable_import_std: bool = False + default_target: str = '' targets: dict[str, TargetConfig] = field(default_factory=dict) + # Merged target configs, generated from defaults and targets + _merged_target_configs = {} + def update(self, data: dict) -> None: for key, value in data.items(): # Ignore unknown config keys @@ -86,13 +120,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,11 +139,18 @@ 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)) + # Generate merged target configs (we generate all of them so that we can validate the full config) + for target_name, target_config in self.targets.items(): + self._merged_target_configs[target_name] = MergedTargetConfig.create_from(self.defaults, target_config) + def update_from_yaml(self, file_path: Path) -> None: with file_path.open('r') as file: self.update(yaml.safe_load(file)) @@ -131,23 +172,18 @@ class ShurikenConfig: raise ConfigValidationException('default_target is not defined in targets') # Validate individual build targets - for target_name, target_config in self.targets.items(): - if not target_config.cpp_compiler and not self.defaults.cpp_compiler: - raise ConfigValidationException(f'Target "{target_name}": cpp_compiler must be set') - if not target_config.cpp_standard and not self.defaults.cpp_standard: - raise ConfigValidationException(f'Target "{target_name}": cpp_standard must be set') - if not target_config.output_file and not self.defaults.output_file: - raise ConfigValidationException(f'Target "{target_name}": output_file must be set') + for target_name, merged_target_config in self._merged_target_configs.items(): + try: + merged_target_config.validate() + except ConfigValidationException as exc: + raise ConfigValidationException(f'Target "{target_name}": {str(exc)}') def get_ninja_file_path(self, target: str) -> Path: return self.build_dir / f'build.{target}.ninja' - def get_merged_target_config(self, target: str) -> TargetConfig: + def get_merged_target_config(self, target: str) -> MergedTargetConfig: assert target in self.targets, f'Target "{target}" not defined!' - - merged_config = copy.deepcopy(self.defaults) - merged_config.update(asdict(self.targets[target])) - return merged_config + return self._merged_target_configs[target] class ShurikenArgumentParser: @@ -186,8 +222,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 +232,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 +247,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 +341,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 +459,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 +520,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 +537,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 +555,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 +568,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 @@ -518,7 +600,25 @@ class NinjaFileGenerator: config = self.config target = self.target target_config = self.target_config - build_dir = self.config.build_dir + build_dir = config.build_dir + ninja_file_path = config.get_ninja_file_path(config.default_target) + cpp_compiler = target_config.cpp_compiler + + # Generate list of all source input files to be used as implicit dependencies for the dyndep file + dyndep_file_implicit_inputs = ' '.join(sorted( + t.src for t in self.compile_targets_cpp + self.compile_targets_cppm + )) + + # Generate list of all .dep files that are generated implicitly during dyndep file generation. + # This is mostly necessary so that Ninja can create the directories for these files beforehand. + dyndep_file_implicit_outputs = ' '.join(sorted( + f'{t.obj}.dep' for t in self.compile_targets_cpp + self.compile_targets_cppm + )) + + # Generate list of generated source files (currently just the copied std module file) + generated_source_files = ' '.join(sorted( + t.generated_src for t in self.compile_targets_std_module + )) # Generate Ninja file return f""" @@ -530,27 +630,39 @@ 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 + depfile = $out.dep + command = {cpp_compiler} $cpp_flags -MMD -MF $out.dep -c $in -o $out # Rule to compile a .cppm file (C++20 module) to a .o file ($out) and a .pcm file ($pcm_out) rule cppm - command = {target_config.cpp_compiler} $cpp_flags -c $in -o $out -fmodule-output=$pcm_out + depfile = $out.dep + command = {cpp_compiler} $cpp_flags -MMD -MF $out.dep -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 = {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 + command = {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,13 +672,13 @@ 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 {ninja_file_path} | generate_source_files # Shortcut alias to generate compilation database build compdb: phony $compdb_file # Generate Ninja dyndep file from compilation database -build $dyndep_file: generate_dyndeps $compdb_file +build $dyndep_file | {dyndep_file_implicit_outputs}: generate_dyndeps $compdb_file | {dyndep_file_implicit_inputs} # -- GENERATED BUILD TARGETS @@ -575,15 +687,19 @@ 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 {generated_source_files} + {self.render_compile_targets()} # End of build.{target}.ninja """.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: @@ -600,23 +716,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 +751,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 +771,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 +```