Compare commits

..

No commits in common. "engine-prototyping" and "main" have entirely different histories.

44 changed files with 241 additions and 3502 deletions

4
.gitignore vendored
View file

@ -14,7 +14,3 @@ __pycache__/
/build /build
/build.ninja /build.ninja
/shuriken.override.yaml /shuriken.override.yaml
# Dependencies
/vendor
!/vendor/README.md

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 622 B

View file

@ -1,88 +0,0 @@
/* 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;
}

View file

@ -1,16 +0,0 @@
<rml>
<head>
<link type="text/rcss" href="base.rcss"/>
<style>
body {
font-family: LatoLatin;
font-size: 20dp;
color: white;
background-color: #330033;
}
</style>
</head>
<body>
<p>Meow!</p>
</body>
</rml>

View file

@ -1,22 +1,10 @@
defaults: defaults:
cpp_standard: c++23 cpp_standard: c++23
cpp_flags: >- cpp_flags: -Wall -Wextra -Werror -pedantic
-Wall linker_args: -lSDL3 -lSDL3_image
-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 default_target: linux
targets: targets:
linux: linux:
output_file: rutile_game output_file: rutile_game
# TODO: In a release build, set -DNDEBUG

View file

@ -1,30 +1,27 @@
import rutile.app; import sdl_app;
#define SDL_MAIN_USE_CALLBACKS #define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL_main.h> #include <SDL3/SDL_main.h>
SDL_AppResult SDL_AppInit(void** appstate, const int /*argc*/, char** /*argv*/) SDL_AppResult SDL_AppInit(void** appstate, int /*argc*/, char** /*argv*/)
{ {
auto* app = new rutile::App; *appstate = new AppState;
*appstate = app; return sdl_app_init(static_cast<AppState*>(*appstate));
return static_cast<SDL_AppResult>(app->initialize());
}
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{
auto* app = static_cast<rutile::App*>(appstate);
return static_cast<SDL_AppResult>(app->handle_event(*event));
} }
SDL_AppResult SDL_AppIterate(void* appstate) SDL_AppResult SDL_AppIterate(void* appstate)
{ {
auto* app = static_cast<rutile::App*>(appstate); return sdl_app_iterate(static_cast<AppState*>(appstate));
return static_cast<SDL_AppResult>(app->iterate());
} }
void SDL_AppQuit(void* appstate, const SDL_AppResult /*result*/) SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{ {
const auto* app = static_cast<rutile::App*>(appstate); return sdl_app_event(static_cast<AppState*>(appstate), event);
app->shutdown(); }
delete app;
void SDL_AppQuit(void* appstate, SDL_AppResult /*result*/)
{
const auto* game_app_state = static_cast<AppState*>(appstate);
sdl_app_shutdown(game_app_state);
delete game_app_state;
} }

View file

@ -1,86 +0,0 @@
module;
#include <SDL3/SDL_init.h>
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> engine_{nullptr};
std::unique_ptr<Game> 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();
}
};
}

View file

@ -1,36 +0,0 @@
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);
}
}
}

View file

@ -1,82 +0,0 @@
module;
#include <cassert>
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<sdl::FRect> 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);
}
};
}

View file

@ -1,108 +0,0 @@
module;
#include <cassert>
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<TileID[]> 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<TileID[]>(
static_cast<std::size_t>(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<float>(tile_size_),
static_cast<float>(tile_size_),
};
render_server.render_texture(
tile_set_texture,
&tile_atlas_src_rect,
&tile_dest_rect
);
}
}
}
};
}

View file

@ -1,92 +0,0 @@
module;
#include <cassert>
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<TileDefinition> tile_definitions_;
public:
TileSet() = delete;
explicit TileSet(
const TextureID texture_id,
std::vector<TileDefinition> 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<sdl::Point>& 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<TileDefinition> 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<float>(x * tile_size),
static_cast<float>(y * tile_size),
static_cast<float>(tile_size),
static_cast<float>(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);
}
};
}

View file

@ -1,118 +0,0 @@
module;
#include <cassert>
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<Engine> 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<Engine>{
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;
}

View file

@ -1,89 +0,0 @@
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<TextureID, sdl::Texture, TextureLoader>;
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);
}
};
}

View file

@ -1,120 +0,0 @@
module;
#include <cassert>
export module rutile.core.resource_manager;
import std;
export namespace rutile
{
template <typename ResourceLoaderType, typename ResourceType>
concept IsResourceLoader = requires(ResourceLoaderType loader, const std::string& name)
{
{ loader.load_resource(name) } -> std::same_as<ResourceType>;
};
template <
typename ResourceIDType,
typename ResourceType,
IsResourceLoader<ResourceType> ResourceLoaderType
>
class ResourceManager
{
// Resource loader
ResourceLoaderType& resource_loader_;
// Registry of loaded resources, array index is resource ID
std::vector<ResourceType> resource_registry_;
// Mapping of all loaded resources from (file) name to resource ID
std::map<std::string, ResourceIDType> 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<ResourceIDType>(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<ResourceIDType> 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];
}
};
}

View file

@ -1,34 +0,0 @@
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);
}
};
}

View file

@ -1,275 +0,0 @@
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<Rml::TouchId>(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<int>(ev.motion.x * pixel_density),
static_cast<int>(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;
}
};
}

View file

@ -1,198 +0,0 @@
// RmlUi RenderInterface implementation for SDL using SDLrenderer.
// Based on RmlUi example code: vendor/RmlUi/Backends/RmlUi_Renderer_SDL.{cpp,h}
module;
#include <cassert>
#include <SDL3/SDL.h>
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<const Rml::Vertex> vertices;
Rml::Span<const int> 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<const Rml::Vertex> vertices,
const Rml::Span<const int> 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<Rml::CompiledGeometryHandle>(data);
}
void ReleaseGeometry(const Rml::CompiledGeometryHandle geometry) override
{
delete reinterpret_cast<GeometryView*>(geometry);
}
void RenderGeometry(
const Rml::CompiledGeometryHandle handle,
const Rml::Vector2f translation,
const Rml::TextureHandle texture
) override
{
const GeometryView* geometry = reinterpret_cast<GeometryView*>(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<sdl::Vertex[]>(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<SDL_Texture*>(texture),
sdl_vertices.get(),
static_cast<int>(num_vertices),
indices,
static_cast<int>(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<Rml::byte*>(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<Rml::byte>(
static_cast<int>(pixels[i + j]) * static_cast<int>(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<Rml::TextureHandle>(texture);
}
Rml::TextureHandle GenerateTexture(
const Rml::Span<const Rml::byte> source,
const Rml::Vector2i source_dimensions
) override
{
assert(
source.data() && source.size() == static_cast<size_t>(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<Rml::byte*>(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<Rml::TextureHandle>(texture);
}
void ReleaseTexture(const Rml::TextureHandle texture_handle) override
{
SDL_DestroyTexture(reinterpret_cast<SDL_Texture*>(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_);
}
}
};
}

View file

@ -1,117 +0,0 @@
// RmlUi SystemInterface implementation for SDL.
// Based on RmlUi example code: vendor/RmlUi/Backends/RmlUi_Platform_SDL.{cpp,h}
module;
#include <SDL3/SDL.h>
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<SDL_Cursor, utils::FuncDeleter<SDL_DestroyCursor>>;
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<double>(SDL_GetPerformanceFrequency());
return static_cast<double>(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<int>(caret_position.x),
static_cast<int>(caret_position.y),
1,
static_cast<int>(line_height),
};
window_.set_text_input_area(&rect, 0);
window_.start_text_input();
}
void DeactivateKeyboard() override
{
window_.stop_text_input();
}
};
}

View file

@ -1,124 +0,0 @@
module;
#include <cassert>
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;
}

View file

@ -1,161 +0,0 @@
module;
#include <cassert>
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<Sprite> 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<Game> create(Engine& engine)
{
// Prevent the class from being instantiated multiple times
assert(!instantiated_);
return std::unique_ptr<Game>{
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;
}

95
src/sdl_app.cppm Normal file
View file

@ -0,0 +1,95 @@
module;
#include <iostream>
#include <string>
#include <SDL3/SDL.h>
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);
}
}

81
src/sprite.cppm Normal file
View file

@ -0,0 +1,81 @@
module;
#include <stdexcept>
#include <string>
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
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<float>(width);
dest_rect.h = static_cast<float>(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);
}
};

View file

@ -1,21 +0,0 @@
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 <auto delete_func>
struct FuncDeleter
{
template <typename T>
constexpr void operator()(T* ptr) const noexcept
{
delete_func(ptr);
}
};
}

View file

@ -1,530 +0,0 @@
module;
#include <RmlUi/Core.h>
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;
}

View file

@ -1,14 +0,0 @@
module;
#include <RmlUi/Debugger.h>
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;
}

View file

@ -1,16 +0,0 @@
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;

View file

@ -1,38 +0,0 @@
module;
#include <SDL3/SDL_clipboard.h>
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());
}
}

View file

@ -1,24 +0,0 @@
module;
#include <SDL3/SDL_error.h>
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())
{}
};
}

View file

@ -1,47 +0,0 @@
module;
#include <SDL3/SDL_events.h>
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;
}
}

View file

@ -1,62 +0,0 @@
module;
#include <SDL3/SDL_init.h>
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<SDL_AppResult>
{
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");
}
}
}

View file

@ -1,177 +0,0 @@
module;
#include <SDL3/SDL_keyboard.h>
#include <SDL3/SDL_keycode.h>
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();
}
}

View file

@ -1,29 +0,0 @@
module;
#include <cassert>
#include <SDL3/SDL.h>
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);
}
}

View file

@ -1,42 +0,0 @@
module;
#include <SDL3/SDL_rect.h>
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);
}
};
}

View file

@ -1,194 +0,0 @@
module;
#include <cassert>
#include <SDL3/SDL.h>
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<SDL_DestroyTexture>
> 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<float>(raw_texture_->w), static_cast<float>(raw_texture_->h)};
}
};
/**
* Wrapper for SDL_Renderer that manages its lifecycle with a unique_ptr.
*/
class Renderer
{
std::unique_ptr<
SDL_Renderer,
utils::FuncDeleter<SDL_DestroyRenderer>
> 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<Window, Renderer> 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}};
}
}

View file

@ -1,98 +0,0 @@
module;
#include <cassert>
#include <SDL3/SDL.h>
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<SDL_DestroySurface>
> 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);
}
};
}

View file

@ -1,23 +0,0 @@
module;
#include <SDL3/SDL_stdinc.h>
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;
}
}

View file

@ -1,101 +0,0 @@
module;
#include <cassert>
#include <SDL3/SDL.h>
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<SDL_DestroyWindow>
> 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<int, int> 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");
}
}
};
}

View file

@ -1,43 +0,0 @@
module;
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
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};
}
}

View file

@ -3,13 +3,14 @@
############################################ ############################################
# shuriken - A ninja-based C++ build tool # shuriken - A ninja-based C++ build tool
# ------------------------------------------ # ------------------------------------------
# Version: 0.1.0 (modified) # Version: 0.1.0
# Author: binaryDiv # Author: binaryDiv
# License: MIT License # License: MIT License
# https://git.0xbd.space/binaryDiv/shuriken # https://git.0xbd.space/binaryDiv/shuriken
############################################ ############################################
import argparse import argparse
import copy
import json import json
import shlex import shlex
import subprocess import subprocess
@ -17,7 +18,7 @@ import sys
from collections import namedtuple from collections import namedtuple
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any, Self from typing import Any
import yaml import yaml
@ -59,43 +60,10 @@ class TargetConfig:
return f"{self.linker_args or ''} {self.linker_args_extra or ''}".strip() 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) @dataclass(kw_only=True)
class ShurikenConfig: class ShurikenConfig:
__path_fields = ['source_dir', 'build_dir'] __path_fields = ['source_dir', 'build_dir']
__list_fields = [] __list_fields = []
__bool_fields = ['enable_import_std']
source_dir: Path = Path('src') source_dir: Path = Path('src')
build_dir: Path = Path('build') build_dir: Path = Path('build')
@ -103,16 +71,14 @@ class ShurikenConfig:
defaults: TargetConfig = field(default_factory=lambda: TargetConfig( defaults: TargetConfig = field(default_factory=lambda: TargetConfig(
cpp_compiler='clang++', cpp_compiler='clang++',
cpp_standard='c++20', cpp_standard='c++20',
cpp_flags='',
linker_flags='',
linker_args='',
)) ))
enable_import_std: bool = False
default_target: str = '' default_target: str = ''
targets: dict[str, TargetConfig] = field(default_factory=dict) 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: def update(self, data: dict) -> None:
for key, value in data.items(): for key, value in data.items():
# Ignore unknown config keys # Ignore unknown config keys
@ -120,13 +86,13 @@ class ShurikenConfig:
continue continue
if key == 'defaults': if key == 'defaults':
assert type(value) is dict assert (type(value) is dict)
self.defaults.update(value) self.defaults.update(value)
elif key == 'targets': elif key == 'targets':
assert type(value) is dict assert (type(value) is dict)
for target_name, target_config in value.items(): for target_name, target_config in value.items():
assert type(target_name) is str and len(target_name) > 0 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_config) is dict or target_config is None)
# Ignore "hidden" targets starting with a dot (can be used for YAML anchors) # Ignore "hidden" targets starting with a dot (can be used for YAML anchors)
if target_name[0] == '.': if target_name[0] == '.':
@ -139,18 +105,11 @@ class ShurikenConfig:
elif key in self.__path_fields: elif key in self.__path_fields:
setattr(self, key, Path(value)) setattr(self, key, Path(value))
elif key in self.__list_fields: 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)) 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: else:
setattr(self, key, str(value)) 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: def update_from_yaml(self, file_path: Path) -> None:
with file_path.open('r') as file: with file_path.open('r') as file:
self.update(yaml.safe_load(file)) self.update(yaml.safe_load(file))
@ -172,18 +131,23 @@ class ShurikenConfig:
raise ConfigValidationException('default_target is not defined in targets') raise ConfigValidationException('default_target is not defined in targets')
# Validate individual build targets # Validate individual build targets
for target_name, merged_target_config in self._merged_target_configs.items(): for target_name, target_config in self.targets.items():
try: if not target_config.cpp_compiler and not self.defaults.cpp_compiler:
merged_target_config.validate() raise ConfigValidationException(f'Target "{target_name}": cpp_compiler must be set')
except ConfigValidationException as exc: if not target_config.cpp_standard and not self.defaults.cpp_standard:
raise ConfigValidationException(f'Target "{target_name}": {str(exc)}') 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')
def get_ninja_file_path(self, target: str) -> Path: def get_ninja_file_path(self, target: str) -> Path:
return self.build_dir / f'build.{target}.ninja' return self.build_dir / f'build.{target}.ninja'
def get_merged_target_config(self, target: str) -> MergedTargetConfig: def get_merged_target_config(self, target: str) -> TargetConfig:
assert target in self.targets, f'Target "{target}" not defined!' assert target in self.targets, f'Target "{target}" not defined!'
return self._merged_target_configs[target]
merged_config = copy.deepcopy(self.defaults)
merged_config.update(asdict(self.targets[target]))
return merged_config
class ShurikenArgumentParser: class ShurikenArgumentParser:
@ -222,8 +186,8 @@ class ShurikenArgumentParser:
help='Print generated Ninja file to stdout instead of writing to a file', 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_build = subparsers.add_parser('build', help='Build project (default command)')
_subparser_compdb = subparsers.add_parser('compdb', help='Generate compilation database') subparser_compdb = subparsers.add_parser('compdb', help='Generate compilation database')
subparser_run = subparsers.add_parser('run', help='Build and run project') subparser_run = subparsers.add_parser('run', help='Build and run project')
subparser_run.add_argument( subparser_run.add_argument(
@ -232,13 +196,6 @@ class ShurikenArgumentParser:
help='Arguments that are passed to the application', 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 = subparsers.add_parser('clean', help='Remove all build files of the current target')
subparser_clean.add_argument( subparser_clean.add_argument(
'--all', '--all',
@ -247,11 +204,11 @@ class ShurikenArgumentParser:
help='Remove all generated files from all targets', help='Remove all generated files from all targets',
) )
_subparser_dump_config = subparsers.add_parser( subparser_dump_config = subparsers.add_parser(
'dump-config', 'dump-config',
help='Dumps the parsed config as well as the effective build target config (for debugging)', 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', 'p1689-to-dyndeps',
help='(Internal command) Parse p1689 file and generate Ninja dyndeps', help='(Internal command) Parse p1689 file and generate Ninja dyndeps',
) )
@ -341,9 +298,6 @@ class ShurikenCli:
case 'run': case 'run':
self.run_project(selected_target) self.run_project(selected_target)
case 'lint':
self.lint_project(selected_target)
case 'clean': case 'clean':
if self.args.clean_all: if self.args.clean_all:
self.log_info('Cleaning up *all* build targets') self.log_info('Cleaning up *all* build targets')
@ -459,26 +413,6 @@ class ShurikenCli:
except KeyboardInterrupt: except KeyboardInterrupt:
self.log_info('Keyboard interrupt') 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: def clean_project(self, target: str) -> None:
ninja_file_path = self.config.get_ninja_file_path(target) ninja_file_path = self.config.get_ninja_file_path(target)
@ -520,14 +454,13 @@ class ShurikenCli:
print('ninja_dyndep_version = 1\n') print('ninja_dyndep_version = 1\n')
for out, deps in build_dependencies.items(): for out, deps in build_dependencies.items():
if deps: if deps:
print(f'build {out}: dyndep | {" ".join(str(dep) for dep in deps)}') print(f'build {out}: dyndep | {" ".join(deps)}')
else: else:
print(f'build {out}: dyndep') print(f'build {out}: dyndep')
CompileTargetCpp = namedtuple('CompileTargetCpp', ['src', 'obj']) CompileTargetCpp = namedtuple('CompileTargetCpp', ['src', 'obj'])
CompileTargetCppm = namedtuple('CompileTargetCppm', ['src', 'obj', 'pcm']) CompileTargetCppm = namedtuple('CompileTargetCppm', ['src', 'obj', 'pcm'])
CompileTargetStdModule = namedtuple('CompileTargetStdModule', ['original_src', 'generated_src', 'pcm'])
class NinjaFileGenerator: class NinjaFileGenerator:
@ -537,7 +470,6 @@ class NinjaFileGenerator:
compile_targets_cpp: list[CompileTargetCpp] compile_targets_cpp: list[CompileTargetCpp]
compile_targets_cppm: list[CompileTargetCppm] compile_targets_cppm: list[CompileTargetCppm]
compile_targets_std_module: list[CompileTargetStdModule]
link_target_obj_files: list[str] link_target_obj_files: list[str]
def __init__(self, *, config: ShurikenConfig, target: str): def __init__(self, *, config: ShurikenConfig, target: str):
@ -555,7 +487,6 @@ class NinjaFileGenerator:
def collect_compile_targets(self) -> None: def collect_compile_targets(self) -> None:
self.compile_targets_cpp = [] self.compile_targets_cpp = []
self.compile_targets_cppm = [] self.compile_targets_cppm = []
self.compile_targets_std_module = []
# Compile targets for .cpp files # Compile targets for .cpp files
for src_file in self.find_source_files('**/*.cpp'): for src_file in self.find_source_files('**/*.cpp'):
@ -568,25 +499,12 @@ class NinjaFileGenerator:
# Compile targets for .cppm files # Compile targets for .cppm files
for src_file in self.find_source_files('**/*.cppm'): for src_file in self.find_source_files('**/*.cppm'):
relative_name = src_file.relative_to(self.config.source_dir) 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( self.compile_targets_cppm.append(CompileTargetCppm(
src=str(src_file), src=str(src_file),
obj=f"$builddir/obj/{relative_name.with_suffix('.o')}", obj=f"$builddir/obj/{relative_name.with_suffix('.o')}",
pcm=f"$builddir/pcm/{module_name}", pcm=f"$builddir/pcm/{'.'.join(relative_name.with_suffix('.pcm').parts)}",
)) ))
# 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 # Link target for output file (executable): gather all object files
self.link_target_obj_files = sorted( self.link_target_obj_files = sorted(
target.obj for target in self.compile_targets_cpp + self.compile_targets_cppm target.obj for target in self.compile_targets_cpp + self.compile_targets_cppm
@ -600,25 +518,7 @@ class NinjaFileGenerator:
config = self.config config = self.config
target = self.target target = self.target
target_config = self.target_config target_config = self.target_config
build_dir = config.build_dir build_dir = self.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 # Generate Ninja file
return f""" return f"""
@ -630,39 +530,27 @@ compdb_file = {build_dir}/compile_commands.json
dyndep_file = $builddir/.ninja_dyndep dyndep_file = $builddir/.ninja_dyndep
# Compiler flags # Compiler flags
cpp_base_flags = -std={target_config.cpp_standard} -fprebuilt-module-path=$builddir/pcm/ cpp_flags = -std={target_config.cpp_standard} -fprebuilt-module-path=$builddir/pcm/ {target_config.merged_cpp_flags}
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_flags = {target_config.merged_linker_flags}
linker_args = {target_config.merged_linker_args} linker_args = {target_config.merged_linker_args}
# -- RULES # -- 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 to compile a .cpp file to a .o file
rule cpp rule cpp
depfile = $out.dep command = {target_config.cpp_compiler} $cpp_flags -c $in -o $out
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 to compile a .cppm file (C++20 module) to a .o file ($out) and a .pcm file ($pcm_out)
rule cppm rule cppm
depfile = $out.dep command = {target_config.cpp_compiler} $cpp_flags -c $in -o $out -fmodule-output=$pcm_out
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 to link several .o files to an executable
rule link rule link
command = {cpp_compiler} $linker_flags -o $out $in $linker_args command = {target_config.cpp_compiler} $linker_flags -o $out $in $linker_args
# Rule to generate a compilation database (JSON file) from the build targets defined in a Ninja file # Rule to generate a compilation database (JSON file) from the build targets defined in a Ninja file
rule generate_compdb rule generate_compdb
command = ninja -f $in -t compdb cpp cppm std_module > $out command = ninja -f $in -t compdb cpp cppm > $out
# Rule to generate a Ninja dyndep file for C++ module dependencies based on a compilation database # Rule to generate a Ninja dyndep file for C++ module dependencies based on a compilation database
rule generate_dyndeps rule generate_dyndeps
@ -672,13 +560,13 @@ rule generate_dyndeps
# -- STATIC BUILD TARGETS # -- STATIC BUILD TARGETS
# Generate compilation database from default target Ninja file # Generate compilation database from default target Ninja file
build $compdb_file: generate_compdb {ninja_file_path} | generate_source_files build $compdb_file: generate_compdb {self.config.get_ninja_file_path(config.default_target)}
# Shortcut alias to generate compilation database # Shortcut alias to generate compilation database
build compdb: phony $compdb_file build compdb: phony $compdb_file
# Generate Ninja dyndep file from compilation database # Generate Ninja dyndep file from compilation database
build $dyndep_file | {dyndep_file_implicit_outputs}: generate_dyndeps $compdb_file | {dyndep_file_implicit_inputs} build $dyndep_file: generate_dyndeps $compdb_file
# -- GENERATED BUILD TARGETS # -- GENERATED BUILD TARGETS
@ -687,19 +575,15 @@ build $dyndep_file | {dyndep_file_implicit_outputs}: generate_dyndeps $compdb_fi
build $builddir/{target_config.output_file}: link {' '.join(self.link_target_obj_files)} build $builddir/{target_config.output_file}: link {' '.join(self.link_target_obj_files)}
default $builddir/{target_config.output_file} 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()} {self.render_compile_targets()}
# End of build.{target}.ninja # End of build.{target}.ninja
""".strip() """.strip()
def render_compile_targets(self) -> str: def render_compile_targets(self) -> str:
rendered_targets = list(map(self.render_compile_target_std_module, self.compile_targets_std_module)) rendered_targets_cpp = [self.render_compile_target_cpp(target) for target in self.compile_targets_cpp]
rendered_targets += list(map(self.render_compile_target_cpp, self.compile_targets_cpp)) rendered_targets_cppm = [self.render_compile_target_cppm(target) for target in self.compile_targets_cppm]
rendered_targets += list(map(self.render_compile_target_cppm, self.compile_targets_cppm)) return '\n\n'.join(rendered_targets_cpp + rendered_targets_cppm)
return '\n\n'.join(rendered_targets)
@staticmethod @staticmethod
def render_compile_target_cpp(target: CompileTargetCpp) -> str: def render_compile_target_cpp(target: CompileTargetCpp) -> str:
@ -716,34 +600,23 @@ build {target.obj} | {target.pcm}: cppm {target.src} || $dyndep_file
pcm_out = {target.pcm} pcm_out = {target.pcm}
""".strip() """.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: class BuildDependencyParser:
config: ShurikenConfig config: ShurikenConfig
target: str target: str
def __init__(self, *, config: ShurikenConfig, target: str): def __init__(self, config: ShurikenConfig, target: str):
self.config = config self.config = config
self.target = target self.target = target
def parse_build_dependencies(self, parsed_p1689: dict[str, Any]) -> dict[Path, list[Path]]: def parse_build_dependencies(self, parsed_p1689: dict[str, Any]) -> dict[str, list[str]]:
# Shortcut variables # Shortcut variables
source_dir = self.config.source_dir source_dir = self.config.source_dir
target_build_dir = self.config.build_dir / self.target target_build_dir = self.config.build_dir / self.target
default_target_build_dir = self.config.build_dir / self.config.default_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 # Validate module name and path convention, construct map of module names to PCM file paths
module_map = {}
for rule in parsed_p1689['rules']: for rule in parsed_p1689['rules']:
provides = rule.get('provides', []) provides = rule.get('provides', [])
if provides: if provides:
@ -751,19 +624,15 @@ class BuildDependencyParser:
assert provides[0]['is-interface'] is True, f"Rule provides non-interface module: {rule}" assert provides[0]['is-interface'] is True, f"Rule provides non-interface module: {rule}"
module_name: str = provides[0]['logical-name'] 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}"
if module_name != 'std': expected_source_path = str(Path(source_dir, module_name.replace('.', '/') + '.cppm'))
expected_source_path = str(Path(source_dir, module_name.replace('.', '/') + '.cppm')) assert provides[0]['source-path'] == expected_source_path, f"Module name does not match source path: {rule}"
assert provides[0]['source-path'] == expected_source_path, \
f"Module name does not match source path: {rule}"
module_map[module_name] = Path(target_build_dir, 'pcm', module_name + '.pcm') module_map[module_name] = str(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 # Parse rules and get module dependencies
build_dependencies = {}
for rule in parsed_p1689['rules']: for rule in parsed_p1689['rules']:
# Compilation database contains "build/{default_target}" paths, we need to map them to "build/{target}" # 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) out = target_build_dir / Path(rule['primary-output']).relative_to(default_target_build_dir)
@ -771,65 +640,11 @@ class BuildDependencyParser:
build_dependencies[out] = [] build_dependencies[out] = []
for require in rule.get('requires', []): for require in rule.get('requires', []):
module_name = require['logical-name'] 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]) build_dependencies[out].append(module_map[module_name])
return build_dependencies 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__': if __name__ == '__main__':
ShurikenCli().run() ShurikenCli().run()

30
vendor/README.md vendored
View file

@ -1,30 +0,0 @@
# 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
```