Compare commits

...

7 commits

Author SHA1 Message Date
d5d480d854
Replace stdlib headers with import std 2026-05-16 01:58:45 +02:00
5a53457817
shuriken: Copy source file to build dir for "import std"
Problem with the previous approach: CLion wouldn't recognize the std module, unless it's included in the compdb (but then /usr/include/c++/.../bits is listed as part of the project -.-)
2026-05-16 01:17:43 +02:00
ae1df9bc8b
shuriken: Add support for "import std" 2026-05-15 01:49:53 +02:00
d256de19c5
shuriken: Add lint command 2026-05-11 01:41:25 +02:00
7b8aac69fb
Refactoring: Rename SDL wrapper funcs to snake_case 2026-05-10 23:34:17 +02:00
7f37be386d
Implement UIs using RmlUi 2026-05-10 23:23:40 +02:00
8039b1276e
Refactor: Move code to rutile module and namespace 2026-04-28 23:28:53 +02:00
35 changed files with 1723 additions and 176 deletions

4
.gitignore vendored
View file

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

Binary file not shown.

88
assets/ui/base.rcss Normal file
View file

@ -0,0 +1,88 @@
/* Base style sheet based on: https://mikke89.github.io/RmlUiDoc/pages/rml/html4_style_sheet.html */
body, div, h1, h2, h3, h4, h5, h6, p, hr, pre {
display: block;
}
h1 {
font-size: 2em;
margin: .67em 0;
}
h2 {
font-size: 1.5em;
margin: .75em 0;
}
h3 {
font-size: 1.17em;
margin: .83em 0;
}
h4 {
margin: 1.12em 0;
}
h5 {
font-size: .83em;
margin: 1.5em 0;
}
h6 {
font-size: .75em;
margin: 1.67em 0;
}
h1, h2, h3, h4, h5, h6, strong {
font-weight: bold;
}
em {
font-style: italic;
}
pre {
white-space: pre;
}
hr {
border-width: 1px;
}
table {
box-sizing: border-box;
display: table;
}
tr {
box-sizing: border-box;
display: table-row;
}
td {
box-sizing: border-box;
display: table-cell;
}
col {
box-sizing: border-box;
display: table-column;
}
colgroup {
display: table-column-group;
}
thead, tbody, tfoot {
display: table-row-group;
}
tabset tabs {
display: block;
}
input {
background-color: white;
color: black;
cursor: text;
}

16
assets/ui/main_ui.rml Normal file
View file

@ -0,0 +1,16 @@
<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,7 +1,18 @@
defaults: defaults:
cpp_standard: c++23 cpp_standard: c++23
cpp_flags: -Wall -Wextra -Werror -pedantic cpp_flags: >-
linker_args: -lSDL3 -lSDL3_image -Wall
-Wextra
-pedantic
-Ivendor/RmlUi/Include
linker_args: >-
-lSDL3
-lSDL3_image
-lfreetype
vendor/RmlUi/Build/librmlui_debugger.a
vendor/RmlUi/Build/librmlui.a
enable_import_std: true
default_target: linux default_target: linux

View file

@ -1,30 +1,30 @@
import app; import rutile.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, const int /*argc*/, char** /*argv*/)
{ {
auto* app = new App; auto* app = new rutile::App;
*appstate = app; *appstate = app;
return static_cast<SDL_AppResult>(app->initialize()); return static_cast<SDL_AppResult>(app->initialize());
} }
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{ {
auto* app = static_cast<App*>(appstate); auto* app = static_cast<rutile::App*>(appstate);
return static_cast<SDL_AppResult>(app->handle_event(event)); 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<App*>(appstate); auto* app = static_cast<rutile::App*>(appstate);
return static_cast<SDL_AppResult>(app->iterate()); return static_cast<SDL_AppResult>(app->iterate());
} }
void SDL_AppQuit(void* appstate, const SDL_AppResult /*result*/) void SDL_AppQuit(void* appstate, const SDL_AppResult /*result*/)
{ {
const auto* app = static_cast<App*>(appstate); const auto* app = static_cast<rutile::App*>(appstate);
app->shutdown(); app->shutdown();
delete app; delete app;
} }

View file

@ -1,22 +1,21 @@
module; module;
#include <iostream> #include <SDL3/SDL_init.h>
#include <memory>
#include <SDL3/SDL.h>
export module app; export module rutile.app;
import config; import std;
import core.engine; import rutile.config;
import game.game; import rutile.core.engine;
import rutile.game.game;
import wrappers.sdl; import wrappers.sdl;
export export namespace rutile
{ {
class App class App
{ {
std::unique_ptr<core::Engine> engine_{nullptr}; std::unique_ptr<Engine> engine_{nullptr};
std::unique_ptr<game::Game> game_{nullptr}; std::unique_ptr<Game> game_{nullptr};
public: public:
App() = default; App() = default;
@ -25,20 +24,20 @@ export
{ {
try { try {
// Set SDL application metadata // Set SDL application metadata
sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING, config::app_name); sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_NAME_STRING, config::app_name);
sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_VERSION_STRING, config::app_version); sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_VERSION_STRING, config::app_version);
sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_IDENTIFIER_STRING, config::app_identifier); sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_IDENTIFIER_STRING, config::app_identifier);
sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_CREATOR_STRING, config::app_creator); sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_CREATOR_STRING, config::app_creator);
sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_COPYRIGHT_STRING, config::app_copyright); sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_COPYRIGHT_STRING, config::app_copyright);
sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_URL_STRING, config::app_url); sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_URL_STRING, config::app_url);
sdl::SetAppMetadataProperty(SDL_PROP_APP_METADATA_TYPE_STRING, "game"); sdl::set_app_metadata_property(SDL_PROP_APP_METADATA_TYPE_STRING, "game");
// Initialize SDL subsystems // Initialize SDL subsystems
sdl::Init(sdl::InitFlags::Video | sdl::InitFlags::Events); sdl::initialize_sdl(sdl::InitFlags::Video | sdl::InitFlags::Events);
// Create engine (includes window and renderer) and game state // Create engine (includes window and renderer) and game state
engine_ = core::Engine::create(); engine_ = Engine::create();
game_ = game::Game::create(*engine_); game_ = Game::create(*engine_);
} }
catch (const std::runtime_error& e) { catch (const std::runtime_error& e) {
std::cerr << "Unhandled exception during initialization: " << e.what() << '\n'; std::cerr << "Unhandled exception during initialization: " << e.what() << '\n';
@ -48,7 +47,7 @@ export
return sdl::AppResult::Continue; return sdl::AppResult::Continue;
} }
sdl::AppResult handle_event(const sdl::Event* event) sdl::AppResult handle_event(const sdl::Event& event)
{ {
try { try {
if (!engine_->handle_event(event)) { if (!engine_->handle_event(event)) {

View file

@ -1,16 +1,16 @@
module; module;
#include <format>
#ifdef NDEBUG #ifdef NDEBUG
#define DEBUG_BOOL false #define DEBUG_BOOL false
#else #else
#define DEBUG_BOOL true #define DEBUG_BOOL true
#endif #endif
export module config; export module rutile.config;
export namespace config import std;
export namespace rutile::config
{ {
constexpr auto debug = DEBUG_BOOL; constexpr auto debug = DEBUG_BOOL;

View file

@ -1,15 +1,14 @@
module; module;
#include <cassert> #include <cassert>
#include <optional>
#include <string>
export module core.drawing.sprite; export module rutile.core.drawing.sprite;
import core.render_server; import std;
import rutile.core.render_server;
import wrappers.sdl; import wrappers.sdl;
export namespace core export namespace rutile
{ {
class Sprite class Sprite
{ {

View file

@ -1,19 +1,18 @@
module; module;
#include <cassert> #include <cassert>
#include <cstddef>
#include <memory>
export module core.drawing.tile_map; export module rutile.core.drawing.tile_map;
import core.drawing.tile_set; import std;
import core.render_server; import rutile.core.drawing.tile_set;
import rutile.core.render_server;
import wrappers.sdl; import wrappers.sdl;
export namespace core export namespace rutile
{ {
// Restrict map size to 16 bit per dimension, so that width*height still fits within size_t // Restrict map size to 16 bit per dimension, so that width*height still fits within size_t
using MapCoord = uint16_t; using MapCoord = std::uint16_t;
class TileMap class TileMap
{ {

View file

@ -1,16 +1,14 @@
module; module;
#include <cassert> #include <cassert>
#include <string>
#include <utility>
#include <vector>
export module core.drawing.tile_set; export module rutile.core.drawing.tile_set;
import core.render_server; import std;
import rutile.core.render_server;
import wrappers.sdl; import wrappers.sdl;
export namespace core export namespace rutile
{ {
using TileSetID = unsigned int; using TileSetID = unsigned int;
using TileID = unsigned int; using TileID = unsigned int;

View file

@ -1,15 +1,16 @@
module; module;
#include <cassert> #include <cassert>
#include <memory>
export module core.engine; export module rutile.core.engine;
import config; import std;
import core.render_server; import rutile.config;
import rutile.core.render_server;
import rutile.core.ui.ui_server;
import wrappers.sdl; import wrappers.sdl;
export namespace core export namespace rutile
{ {
class Engine class Engine
{ {
@ -21,11 +22,13 @@ export namespace core
sdl::Window window_; sdl::Window window_;
RenderServer render_server_; RenderServer render_server_;
UIServer ui_server_;
// Private constructor // Private constructor
Engine(sdl::Window&& window, sdl::Renderer&& renderer) Engine(sdl::Window&& window, sdl::Renderer&& renderer)
: window_{std::move(window)}, : window_{std::move(window)},
render_server_{std::move(renderer)} render_server_{std::move(renderer)},
ui_server_{render_server_.get_renderer(), window_}
{ {
instantiated_ = true; instantiated_ = true;
} }
@ -49,19 +52,21 @@ export namespace core
// Prevent the class from being instantiated multiple times // Prevent the class from being instantiated multiple times
assert(!instantiated_); assert(!instantiated_);
auto [sdl_window, sdl_renderer] = sdl::CreateWindowAndRenderer( auto [sdl_window, sdl_renderer] = sdl::create_window_and_renderer(
config::get_window_title(), config::get_window_title(),
config::window_width, config::window_width,
config::window_height, config::window_height,
0 0
); );
// NOLINTBEGIN: "Allocated memory is leaked"
return std::unique_ptr<Engine>{ return std::unique_ptr<Engine>{
new Engine{ new Engine{
std::move(sdl_window), std::move(sdl_window),
std::move(sdl_renderer) std::move(sdl_renderer)
} }
}; };
// NOLINTEND
} }
bool keep_running() const bool keep_running() const
@ -79,15 +84,24 @@ export namespace core
return render_server_; return render_server_;
} }
// Handles an SDL event. Returns true if the event has been handled. UIServer& get_ui_server()
bool handle_event(const sdl::Event* event)
{ {
if (event->type == sdl::EventType::Quit) { return ui_server_;
}
// Handles an SDL event. Returns true if the event has been handled.
bool handle_event(const sdl::Event& event)
{
if (event.type == sdl::EventTypes::Quit) {
// Exit the application // Exit the application
keep_running_ = false; keep_running_ = false;
return true; return true;
} }
if (ui_server_.handle_event(event)) {
return true;
}
return false; return false;
} }

View file

@ -1,15 +1,13 @@
module; module;
#include <string> export module rutile.core.render_server;
#include <utility>
export module core.render_server; import std;
import rutile.core.resource_manager;
import core.resource_manager; import rutile.core.texture_loader;
import core.texture_loader;
import wrappers.sdl; import wrappers.sdl;
export namespace core export namespace rutile
{ {
using TextureID = unsigned int; using TextureID = unsigned int;
using TextureManager = ResourceManager<TextureID, sdl::Texture, TextureLoader>; using TextureManager = ResourceManager<TextureID, sdl::Texture, TextureLoader>;
@ -27,7 +25,9 @@ export namespace core
: renderer_{std::move(renderer)}, : renderer_{std::move(renderer)},
texture_loader_{renderer_}, texture_loader_{renderer_},
texture_manager_{texture_loader_} texture_manager_{texture_loader_}
{} {
renderer_.set_vsync(1);
}
// No copy or move operations // No copy or move operations
RenderServer(const RenderServer&) = delete; RenderServer(const RenderServer&) = delete;

View file

@ -1,14 +1,12 @@
module; module;
#include <cassert> #include <cassert>
#include <map>
#include <optional>
#include <string>
#include <vector>
export module core.resource_manager; export module rutile.core.resource_manager;
export namespace core import std;
export namespace rutile
{ {
template <typename ResourceLoaderType, typename ResourceType> template <typename ResourceLoaderType, typename ResourceType>
concept IsResourceLoader = requires(ResourceLoaderType loader, const std::string& name) concept IsResourceLoader = requires(ResourceLoaderType loader, const std::string& name)

View file

@ -1,14 +1,13 @@
module; module;
#include <string> export module rutile.core.texture_loader;
export module core.texture_loader; import std;
import rutile.core.resource_manager;
import core.resource_manager;
import wrappers.sdl; import wrappers.sdl;
import wrappers.sdl_image; import wrappers.sdl_image;
export namespace core export namespace rutile
{ {
class TextureLoader class TextureLoader
{ {
@ -29,7 +28,7 @@ export namespace core
sdl::Texture load_resource(const std::string& filename) const sdl::Texture load_resource(const std::string& filename) const
{ {
return sdl_image::LoadTexture(renderer_, filename); return sdl_image::load_texture(renderer_, filename);
} }
}; };
} }

View file

@ -0,0 +1,278 @@
module;
#include <RmlUi/Core/Context.h>
#include <RmlUi/Core/Input.h>
#include <RmlUi/Core/Types.h>
export module rutile.core.ui.backend.rmlui_input_handler;
import wrappers.sdl;
export namespace rutile
{
class RmlUiInputHandler
{
sdl::Window& window_;
public:
explicit RmlUiInputHandler(sdl::Window& window)
: window_{window}
{}
static Rml::Input::KeyIdentifier convert_key(const int sdl_key)
{
namespace Key = sdl::Key;
// clang-format off
switch (sdl_key) {
case Key::Unknown: return Rml::Input::KI_UNKNOWN;
case Key::Escape: return Rml::Input::KI_ESCAPE;
case Key::Space: return Rml::Input::KI_SPACE;
case Key::Num0: return Rml::Input::KI_0;
case Key::Num1: return Rml::Input::KI_1;
case Key::Num2: return Rml::Input::KI_2;
case Key::Num3: return Rml::Input::KI_3;
case Key::Num4: return Rml::Input::KI_4;
case Key::Num5: return Rml::Input::KI_5;
case Key::Num6: return Rml::Input::KI_6;
case Key::Num7: return Rml::Input::KI_7;
case Key::Num8: return Rml::Input::KI_8;
case Key::Num9: return Rml::Input::KI_9;
case Key::A: return Rml::Input::KI_A;
case Key::B: return Rml::Input::KI_B;
case Key::C: return Rml::Input::KI_C;
case Key::D: return Rml::Input::KI_D;
case Key::E: return Rml::Input::KI_E;
case Key::F: return Rml::Input::KI_F;
case Key::G: return Rml::Input::KI_G;
case Key::H: return Rml::Input::KI_H;
case Key::I: return Rml::Input::KI_I;
case Key::J: return Rml::Input::KI_J;
case Key::K: return Rml::Input::KI_K;
case Key::L: return Rml::Input::KI_L;
case Key::M: return Rml::Input::KI_M;
case Key::N: return Rml::Input::KI_N;
case Key::O: return Rml::Input::KI_O;
case Key::P: return Rml::Input::KI_P;
case Key::Q: return Rml::Input::KI_Q;
case Key::R: return Rml::Input::KI_R;
case Key::S: return Rml::Input::KI_S;
case Key::T: return Rml::Input::KI_T;
case Key::U: return Rml::Input::KI_U;
case Key::V: return Rml::Input::KI_V;
case Key::W: return Rml::Input::KI_W;
case Key::X: return Rml::Input::KI_X;
case Key::Y: return Rml::Input::KI_Y;
case Key::Z: return Rml::Input::KI_Z;
case Key::Semicolon: return Rml::Input::KI_OEM_1;
case Key::Plus: return Rml::Input::KI_OEM_PLUS;
case Key::Comma: return Rml::Input::KI_OEM_COMMA;
case Key::Minus: return Rml::Input::KI_OEM_MINUS;
case Key::Period: return Rml::Input::KI_OEM_PERIOD;
case Key::Slash: return Rml::Input::KI_OEM_2;
case Key::Grave: return Rml::Input::KI_OEM_3;
case Key::LeftBracket: return Rml::Input::KI_OEM_4;
case Key::Backslash: return Rml::Input::KI_OEM_5;
case Key::RightBracket: return Rml::Input::KI_OEM_6;
case Key::DblApostrophe: return Rml::Input::KI_OEM_7;
case Key::Numpad0: return Rml::Input::KI_NUMPAD0;
case Key::Numpad1: return Rml::Input::KI_NUMPAD1;
case Key::Numpad2: return Rml::Input::KI_NUMPAD2;
case Key::Numpad3: return Rml::Input::KI_NUMPAD3;
case Key::Numpad4: return Rml::Input::KI_NUMPAD4;
case Key::Numpad5: return Rml::Input::KI_NUMPAD5;
case Key::Numpad6: return Rml::Input::KI_NUMPAD6;
case Key::Numpad7: return Rml::Input::KI_NUMPAD7;
case Key::Numpad8: return Rml::Input::KI_NUMPAD8;
case Key::Numpad9: return Rml::Input::KI_NUMPAD9;
case Key::NumpadEnter: return Rml::Input::KI_NUMPADENTER;
case Key::NumpadMultiply: return Rml::Input::KI_MULTIPLY;
case Key::NumpadPlus: return Rml::Input::KI_ADD;
case Key::NumpadMinus: return Rml::Input::KI_SUBTRACT;
case Key::NumpadPeriod: return Rml::Input::KI_DECIMAL;
case Key::NumpadDivide: return Rml::Input::KI_DIVIDE;
case Key::NumpadEquals: return Rml::Input::KI_OEM_NEC_EQUAL;
case Key::Backspace: return Rml::Input::KI_BACK;
case Key::Tab: return Rml::Input::KI_TAB;
case Key::Clear: return Rml::Input::KI_CLEAR;
case Key::Return: return Rml::Input::KI_RETURN;
case Key::Pause: return Rml::Input::KI_PAUSE;
case Key::CapsLock: return Rml::Input::KI_CAPITAL;
case Key::PageUp: return Rml::Input::KI_PRIOR;
case Key::PageDown: return Rml::Input::KI_NEXT;
case Key::End: return Rml::Input::KI_END;
case Key::Home: return Rml::Input::KI_HOME;
case Key::Left: return Rml::Input::KI_LEFT;
case Key::Up: return Rml::Input::KI_UP;
case Key::Right: return Rml::Input::KI_RIGHT;
case Key::Down: return Rml::Input::KI_DOWN;
case Key::Insert: return Rml::Input::KI_INSERT;
case Key::Delete: return Rml::Input::KI_DELETE;
case Key::Help: return Rml::Input::KI_HELP;
case Key::F1: return Rml::Input::KI_F1;
case Key::F2: return Rml::Input::KI_F2;
case Key::F3: return Rml::Input::KI_F3;
case Key::F4: return Rml::Input::KI_F4;
case Key::F5: return Rml::Input::KI_F5;
case Key::F6: return Rml::Input::KI_F6;
case Key::F7: return Rml::Input::KI_F7;
case Key::F8: return Rml::Input::KI_F8;
case Key::F9: return Rml::Input::KI_F9;
case Key::F10: return Rml::Input::KI_F10;
case Key::F11: return Rml::Input::KI_F11;
case Key::F12: return Rml::Input::KI_F12;
case Key::F13: return Rml::Input::KI_F13;
case Key::F14: return Rml::Input::KI_F14;
case Key::F15: return Rml::Input::KI_F15;
case Key::NumLockClear: return Rml::Input::KI_NUMLOCK;
case Key::ScrollLock: return Rml::Input::KI_SCROLL;
case Key::LShift: return Rml::Input::KI_LSHIFT;
case Key::RShift: return Rml::Input::KI_RSHIFT;
case Key::LCtrl: return Rml::Input::KI_LCONTROL;
case Key::RCtrl: return Rml::Input::KI_RCONTROL;
case Key::LAlt: return Rml::Input::KI_LMENU;
case Key::RAlt: return Rml::Input::KI_RMENU;
case Key::LGui: return Rml::Input::KI_LMETA;
case Key::RGui: return Rml::Input::KI_RMETA;
default: return Rml::Input::KI_UNKNOWN;
}
// clang-format on
}
static int convert_mouse_button(const sdl::MouseButtonIndex sdl_mouse_button)
{
switch (sdl_mouse_button) {
case sdl::MouseButtons::Left: return 0;
case sdl::MouseButtons::Right: return 1;
case sdl::MouseButtons::Middle: return 2;
default: return 3;
}
}
static int get_key_modifiers()
{
const sdl::KeyModState sdl_mods = sdl::get_key_mod_state();
int retval = 0;
if (sdl_mods & sdl::KeyMods::Ctrl) {
retval |= Rml::Input::KM_CTRL;
}
if (sdl_mods & sdl::KeyMods::Shift) {
retval |= Rml::Input::KM_SHIFT;
}
if (sdl_mods & sdl::KeyMods::Alt) {
retval |= Rml::Input::KM_ALT;
}
if (sdl_mods & sdl::KeyMods::NumLock) {
retval |= Rml::Input::KM_NUMLOCK;
}
if (sdl_mods & sdl::KeyMods::CapsLock) {
retval |= Rml::Input::KM_CAPSLOCK;
}
return retval;
}
static Rml::TouchList convert_touch_event(const Rml::Context& context, const sdl::Event& ev)
{
const Rml::Vector2f position = Rml::Vector2f{ev.tfinger.x, ev.tfinger.y}
* Rml::Vector2f{context.GetDimensions()};
return {Rml::Touch{static_cast<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

@ -0,0 +1,200 @@
// RmlUi RenderInterface implementation for SDL using SDLrenderer.
// Based on RmlUi example code: vendor/RmlUi/Backends/RmlUi_Renderer_SDL.{cpp,h}
module;
#include <RmlUi/Core/Core.h>
#include <RmlUi/Core/FileInterface.h>
#include <RmlUi/Core/RenderInterface.h>
#include <RmlUi/Core/Types.h>
#include <SDL3/SDL.h>
export module rutile.core.ui.backend.rmlui_render_interface;
import std;
import wrappers.sdl;
import wrappers.sdl_image;
export namespace rutile
{
class RmlUiRenderInterface final : public Rml::RenderInterface
{
struct GeometryView
{
Rml::Span<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
{
RMLUI_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

@ -0,0 +1,118 @@
// RmlUi SystemInterface implementation for SDL.
// Based on RmlUi example code: vendor/RmlUi/Backends/RmlUi_Platform_SDL.{cpp,h}
module;
#include <RmlUi/Core/StringUtilities.h>
#include <RmlUi/Core/SystemInterface.h>
#include <RmlUi/Core/Types.h>
#include <SDL3/SDL.h>
export module rutile.core.ui.backend.rmlui_system_interface;
import utils.memory;
import wrappers.sdl;
// Shorthand for a unique pointer to an SDL_Cursor
// TODO: Build actual wrapper sdl::Cursor at some point?
using SDL_CursorPtr = std::unique_ptr<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

@ -0,0 +1,124 @@
module;
#include <cassert>
#include <RmlUi/Core.h>
#include <RmlUi/Debugger.h>
export module rutile.core.ui.ui_server;
import std;
import rutile.core.ui.backend.rmlui_render_interface;
import rutile.core.ui.backend.rmlui_system_interface;
import rutile.core.ui.backend.rmlui_input_handler;
import wrappers.sdl;
export namespace rutile
{
class UIServer
{
// Whether this class is currently instantiated (to prevent multiple instances because we need to do
// Rml::Initialise() and Rml::Shutdown()).
static bool instantiated_;
RmlUiSystemInterface system_interface_;
RmlUiRenderInterface render_interface_;
RmlUiInputHandler input_handler_;
// RmlUi context (non-owning pointer, lifetime managed by library)
Rml::Context* context_;
public:
explicit UIServer(sdl::Renderer& renderer, sdl::Window& window)
: system_interface_{window},
render_interface_{renderer},
input_handler_{window}
{
// Prevent the class from being instantiated multiple times
assert(!instantiated_);
Rml::SetSystemInterface(&system_interface_);
Rml::SetRenderInterface(&render_interface_);
Rml::Initialise();
// Create the main RmlUi context
auto [window_width, window_height] = window.get_size();
context_ = Rml::CreateContext("main", Rml::Vector2i(window_width, window_height));
if (!context_) {
throw std::runtime_error("RmlUi: Couldn't create context");
}
// TODO: Implement some kind of switch to disable debugger (just use config::debug?)
Rml::Debugger::Initialise(context_);
instantiated_ = true;
}
// No copy or move operations
UIServer(const UIServer&) = delete;
UIServer& operator=(const UIServer&) = delete;
UIServer(UIServer&&) = delete;
UIServer& operator=(UIServer&&) = delete;
~UIServer()
{
Rml::Shutdown();
instantiated_ = false;
}
constexpr Rml::Context& get_context() const
{
// TODO: Can we assume context_ is always non-nullptr?
assert(context_);
return *context_;
}
// Handles an SDL event. Returns true if the event has been handled.
bool handle_event(const sdl::Event& event) const
{
// Toggle RmlUi debugger with F12
if (event.type == sdl::EventTypes::KeyDown && event.key.key == sdl::Key::F12) {
Rml::Debugger::SetVisible(!Rml::Debugger::IsVisible());
return true;
}
// Let RmlUi handle relevant events
if (input_handler_.handle_input_event(get_context(), event)) {
return true;
}
return false;
}
void update() const
{
get_context().Update();
}
void render() const
{
render_interface_.prepare_render();
get_context().Render();
}
static void load_font_face(const std::string& file_path)
{
if (!Rml::LoadFontFace(file_path)) {
throw std::runtime_error(std::format("RmlUi: Couldn't load font face \"{}\"", file_path));
}
}
// TODO: Can/should we avoid the (non-owning) raw pointers here?
Rml::ElementDocument* load_document(const std::string& file_path) const
{
Rml::ElementDocument* document = get_context().LoadDocument(file_path);
if (!document) {
throw std::runtime_error(std::format("RmlUi: Couldn't load document \"{}\"", file_path));
}
return document;
}
};
bool UIServer::instantiated_ = false;
}

View file

@ -1,21 +1,20 @@
module; module;
#include <cassert> #include <cassert>
#include <memory> #include <RmlUi/Core/ElementDocument.h>
#include <optional>
#include <vector>
export module game.game; export module rutile.game.game;
import core.drawing.sprite; import std;
import core.drawing.tile_map; import rutile.core.drawing.sprite;
import core.drawing.tile_set; import rutile.core.drawing.tile_map;
import core.engine; import rutile.core.drawing.tile_set;
import core.render_server; import rutile.core.engine;
import rutile.core.render_server;
import wrappers.sdl; import wrappers.sdl;
import wrappers.sdl_image; import wrappers.sdl_image;
export namespace game export namespace rutile
{ {
class Game class Game
{ {
@ -23,21 +22,21 @@ export namespace game
static bool instantiated_; static bool instantiated_;
// Reference to the engine // Reference to the engine
core::Engine& engine_; Engine& engine_;
// Sprites for testing // Sprites for testing
core::Sprite player_sprite_; Sprite player_sprite_;
std::vector<core::Sprite> sprites_; std::vector<Sprite> sprites_;
// Tile set and tile map (TODO: tile set should be moved to resource manager in engine) // Tile set and tile map (TODO: tile set should be moved to resource manager in engine)
core::TileSet tile_set_; TileSet tile_set_;
core::TileMap tile_map_; TileMap tile_map_;
// Private constructor // Private constructor
explicit Game(core::Engine& engine) explicit Game(Engine& engine)
: engine_(engine), : engine_(engine),
player_sprite_{ player_sprite_{
core::Sprite::create_from_texture( Sprite::create_from_texture(
engine_.get_render_server(), engine_.get_render_server(),
"assets/sprites/neocat_64.png", "assets/sprites/neocat_64.png",
std::nullopt, std::nullopt,
@ -46,7 +45,7 @@ export namespace game
) )
}, },
tile_set_{ tile_set_{
core::TileSet::create_from_texture( TileSet::create_from_texture(
engine_.get_render_server(), engine_.get_render_server(),
"assets/tilesets/terrain.png", "assets/tilesets/terrain.png",
32, 32,
@ -63,6 +62,12 @@ export namespace game
}, },
tile_map_(0, 32, 16, 12, {50, 50}) 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 // Initialize tile map with example data
for (auto x = 0; x < 10; x++) { for (auto x = 0; x < 10; x++) {
for (auto y = 0; y < 10; y++) { for (auto y = 0; y < 10; y++) {
@ -70,6 +75,8 @@ export namespace game
tile_map_.set_tile(x, y, tile_index); tile_map_.set_tile(x, y, tile_index);
} }
} }
instantiated_ = true;
} }
public: public:
@ -86,7 +93,7 @@ export namespace game
instantiated_ = false; instantiated_ = false;
} }
static std::unique_ptr<Game> create(core::Engine& engine) static std::unique_ptr<Game> create(Engine& engine)
{ {
// Prevent the class from being instantiated multiple times // Prevent the class from being instantiated multiple times
assert(!instantiated_); assert(!instantiated_);
@ -96,20 +103,20 @@ export namespace game
} }
// Handles an SDL event. Returns true if the event has been handled. // Handles an SDL event. Returns true if the event has been handled.
bool handle_event(const sdl::Event* event) bool handle_event(const sdl::Event& event)
{ {
if (event->type == sdl::EventType::MouseMotion) { if (event.type == sdl::EventTypes::MouseMotion) {
player_sprite_.set_position({event->motion.x, event->motion.y}); player_sprite_.set_position({event.motion.x, event.motion.y});
return true; return true;
} }
if (event->type == sdl::EventType::MouseButtonUp) { if (event.type == sdl::EventTypes::MouseButtonUp) {
sprites_.push_back( sprites_.push_back(
core::Sprite::create_from_texture( Sprite::create_from_texture(
engine_.get_render_server(), engine_.get_render_server(),
"assets/sprites/neofox_64.png", "assets/sprites/neofox_64.png",
std::nullopt, std::nullopt,
sdl::FPoint{event->motion.x, event->motion.y}, sdl::FPoint{event.motion.x, event.motion.y},
sdl::FPoint{-32, -32} sdl::FPoint{-32, -32}
) )
); );
@ -121,6 +128,8 @@ export namespace game
void update() void update()
{ {
// TODO: Move this to Engine
engine_.get_ui_server().update();
} }
void render() const void render() const
@ -132,11 +141,14 @@ export namespace game
tile_map_.draw(render_server, tile_set_); tile_map_.draw(render_server, tile_set_);
// Render sprites // Render sprites
for (const core::Sprite& sprite : sprites_) { for (const Sprite& sprite : sprites_) {
sprite.draw(render_server); sprite.draw(render_server);
} }
player_sprite_.draw(render_server); player_sprite_.draw(render_server);
// TODO: Move this to Engine
engine_.get_ui_server().render();
render_server.finish_frame(); render_server.finish_frame();
} }

View file

@ -3,9 +3,14 @@ module;
export module wrappers.sdl; export module wrappers.sdl;
// Export submodules // Export submodules
export import wrappers.sdl.clipboard;
export import wrappers.sdl.error; export import wrappers.sdl.error;
export import wrappers.sdl.events; export import wrappers.sdl.events;
export import wrappers.sdl.init; export import wrappers.sdl.init;
export import wrappers.sdl.keyboard;
export import wrappers.sdl.mouse;
export import wrappers.sdl.rect; export import wrappers.sdl.rect;
export import wrappers.sdl.render; export import wrappers.sdl.render;
export import wrappers.sdl.surface;
export import wrappers.sdl.utils;
export import wrappers.sdl.video; export import wrappers.sdl.video;

View file

@ -0,0 +1,38 @@
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,11 +1,11 @@
module; module;
#include <format> #include <SDL3/SDL_error.h>
#include <stdexcept>
#include <SDL3/SDL.h>
export module wrappers.sdl.error; export module wrappers.sdl.error;
import std;
export namespace sdl export namespace sdl
{ {
// Exception that wraps an SDL error (which SDL function caused the error, what's the error). // Exception that wraps an SDL error (which SDL function caused the error, what's the error).

View file

@ -1,6 +1,6 @@
module; module;
#include <SDL3/SDL.h> #include <SDL3/SDL_events.h>
export module wrappers.sdl.events; export module wrappers.sdl.events;
@ -10,7 +10,7 @@ export namespace sdl
using Event = SDL_Event; using Event = SDL_Event;
// Alias for EventType enum // Alias for EventType enum
using EventType_t = SDL_EventType; using EventType = SDL_EventType;
/** /**
* Wrapper for the SDL_EventType enum. * Wrapper for the SDL_EventType enum.
@ -18,13 +18,30 @@ export namespace sdl
* We're using a namespace here to emulate an enum-like interface, without having to copy the entire enum. * 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. * More constants can be added on demand.
*/ */
namespace EventType namespace EventTypes
{ {
constexpr EventType_t Quit = SDL_EVENT_QUIT; // Application events
constexpr EventType_t KeyDown = SDL_EVENT_KEY_DOWN; constexpr EventType Quit = SDL_EVENT_QUIT;
constexpr EventType_t KeyUp = SDL_EVENT_KEY_UP;
constexpr EventType_t MouseMotion = SDL_EVENT_MOUSE_MOTION; // Window events
constexpr EventType_t MouseButtonUp = SDL_EVENT_MOUSE_BUTTON_UP; constexpr EventType WindowMouseLeave = SDL_EVENT_WINDOW_MOUSE_LEAVE;
constexpr EventType_t MouseButtonDown = SDL_EVENT_MOUSE_BUTTON_DOWN; 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,10 +1,10 @@
module; module;
#include <type_traits> #include <SDL3/SDL_init.h>
#include <SDL3/SDL.h>
export module wrappers.sdl.init; export module wrappers.sdl.init;
import std;
import wrappers.sdl.error; import wrappers.sdl.error;
export namespace sdl export namespace sdl
@ -31,7 +31,7 @@ export namespace sdl
} }
/** /**
* Wrapper around the SDL_AppResult enum. * Wrapper for the SDL_AppResult enum.
*/ */
enum class AppResult : std::underlying_type_t<SDL_AppResult> enum class AppResult : std::underlying_type_t<SDL_AppResult>
{ {
@ -41,9 +41,9 @@ export namespace sdl
}; };
/** /**
* Wrapper around SDL_Init(). * Wrapper for SDL_Init().
*/ */
constexpr void Init(const InitFlags_t flags) constexpr void initialize_sdl(const InitFlags_t flags)
{ {
if (!SDL_Init(flags)) { if (!SDL_Init(flags)) {
throw SDLException("SDL_Init"); throw SDLException("SDL_Init");
@ -51,9 +51,9 @@ export namespace sdl
} }
/** /**
* Wrapper around SDL_SetAppMetadataProperty(). * Wrapper for SDL_SetAppMetadataProperty().
*/ */
constexpr void SetAppMetadataProperty(const char* property_name, const char* value) constexpr void set_app_metadata_property(const char* property_name, const char* value)
{ {
if (!SDL_SetAppMetadataProperty(property_name, value)) { if (!SDL_SetAppMetadataProperty(property_name, value)) {
throw SDLException("SDL_SetAppMetadataProperty"); throw SDLException("SDL_SetAppMetadataProperty");

View file

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

@ -0,0 +1,29 @@
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,6 +1,6 @@
module; module;
#include <SDL3/SDL.h> #include <SDL3/SDL_rect.h>
export module wrappers.sdl.rect; export module wrappers.sdl.rect;
@ -11,30 +11,30 @@ export namespace sdl
using Point = SDL_Point; using Point = SDL_Point;
/** /**
* Wrapper around SDL_FRect using class inheritance. * Wrapper for SDL_FRect using class inheritance.
*/ */
class FRect : public SDL_FRect class FRect : public SDL_FRect
{ {
/** /**
* Wrapper around SDL_RectEmptyFloat(). * Wrapper for SDL_RectEmptyFloat().
* @return True if the floating point rectangle takes no space. * @return True if the floating point rectangle takes no space.
*/ */
constexpr bool is_empty() const bool is_empty() const
{ {
return SDL_RectEmptyFloat(this); return SDL_RectEmptyFloat(this);
} }
}; };
/** /**
* Wrapper around SDL_Rect using class inheritance. * Wrapper for SDL_Rect using class inheritance.
*/ */
class Rect : public SDL_Rect class Rect : public SDL_Rect
{ {
/** /**
* Wrapper around SDL_RectEmpty(). * Wrapper for SDL_RectEmpty().
* @return True if the rectangle takes no space. * @return True if the rectangle takes no space.
*/ */
constexpr bool is_empty() const bool is_empty() const
{ {
return SDL_RectEmpty(this); return SDL_RectEmpty(this);
} }

View file

@ -1,11 +1,11 @@
module; module;
#include <cassert> #include <cassert>
#include <memory>
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
export module wrappers.sdl.render; export module wrappers.sdl.render;
import std;
import utils.memory; import utils.memory;
import wrappers.sdl.error; import wrappers.sdl.error;
import wrappers.sdl.rect; import wrappers.sdl.rect;
@ -13,8 +13,10 @@ import wrappers.sdl.video;
export namespace sdl export namespace sdl
{ {
using Vertex = SDL_Vertex;
/** /**
* Wrapper around SDL_Texture that manages its lifecycle with a unique_ptr. * Wrapper for SDL_Texture that manages its lifecycle with a unique_ptr.
*/ */
class Texture class Texture
{ {
@ -53,7 +55,7 @@ export namespace sdl
}; };
/** /**
* Wrapper around SDL_Renderer that manages its lifecycle with a unique_ptr. * Wrapper for SDL_Renderer that manages its lifecycle with a unique_ptr.
*/ */
class Renderer class Renderer
{ {
@ -76,7 +78,41 @@ export namespace sdl
} }
/** /**
* Wrapper around SDL_RenderClear(). * Wrapper for SDL_SetRenderClipRect().
* Uses assert to verify the function returned true.
*/
constexpr void set_clip_rect(const Rect* rect) const
{
if (!SDL_SetRenderClipRect(get_raw(), rect)) {
assert(!"SDL_SetRenderClipRect returned false");
}
}
/**
* Wrapper for SDL_SetRenderDrawBlendMode().
* Uses assert to verify the function returned true.
*/
constexpr void set_draw_blend_mode(SDL_BlendMode blend_mode) const
{
if (!SDL_SetRenderDrawBlendMode(get_raw(), blend_mode)) {
assert(!"SDL_SetRenderDrawBlendMode returned false");
}
}
/**
* Wrapper for SDL_SetRenderVSync().
* Uses assert to verify the function returned true.
*/
constexpr void set_vsync(const int vsync) const
{
// TODO: These functions probably should never fail? Change this to a runtime exception if necessary.
if (!SDL_SetRenderVSync(get_raw(), vsync)) {
assert(!"SDL_SetRenderVSync returned false");
}
}
/**
* Wrapper for SDL_RenderClear().
* Uses assert to verify the function returned true. * Uses assert to verify the function returned true.
*/ */
constexpr void clear() const constexpr void clear() const
@ -88,7 +124,7 @@ export namespace sdl
} }
/** /**
* Wrapper around SDL_RenderPresent(). * Wrapper for SDL_RenderPresent().
* Uses assert to verify the function returned true. * Uses assert to verify the function returned true.
*/ */
constexpr void present() const constexpr void present() const
@ -99,7 +135,7 @@ export namespace sdl
} }
/** /**
* Wrapper around SDL_RenderTexture(). * Wrapper for SDL_RenderTexture().
* Uses assert to verify the function returned true. * Uses assert to verify the function returned true.
*/ */
constexpr void render_texture( constexpr void render_texture(
@ -112,14 +148,32 @@ export namespace sdl
assert(!"SDL_RenderTexture returned false"); assert(!"SDL_RenderTexture returned false");
} }
} }
/**
* Wrapper for SDL_RenderGeometry().
* Uses assert to verify the function returned true.
*/
// TODO: Function uses raw SDL_Texture pointer. Add overload for sdl::Texture& when needed.
constexpr void render_geometry(
SDL_Texture* texture,
const Vertex* vertices,
const int num_vertices,
const int* indices,
const int num_indices
) const
{
if (!SDL_RenderGeometry(get_raw(), texture, vertices, num_vertices, indices, num_indices)) {
assert(!"SDL_RenderGeometry returned false");
}
}
}; };
/** /**
* Wrapper around SDL_CreateWindowAndRenderer(). * Wrapper for SDL_CreateWindowAndRenderer().
* Returns a pair of a Window and a Renderer object. * Returns a pair of a Window and a Renderer object.
* Throws an SDLException on failure. * Throws an SDLException on failure.
*/ */
std::pair<Window, Renderer> CreateWindowAndRenderer( std::pair<Window, Renderer> create_window_and_renderer(
const std::string& title, const std::string& title,
const int width, const int width,
const int height, const int height,

View file

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

@ -0,0 +1,23 @@
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,17 +1,19 @@
module; module;
#include <cassert> #include <cassert>
#include <memory>
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
export module wrappers.sdl.video; export module wrappers.sdl.video;
import std;
import utils.memory; import utils.memory;
import wrappers.sdl.error;
import wrappers.sdl.rect;
export namespace sdl export namespace sdl
{ {
/** /**
* Wrapper around SDL_Window that manages its lifecycle with a unique_ptr. * Wrapper for SDL_Window that manages its lifecycle with a unique_ptr.
*/ */
class Window class Window
{ {
@ -32,5 +34,68 @@ export namespace sdl
assert(raw_window_); assert(raw_window_);
return raw_window_.get(); 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,28 +1,43 @@
module; module;
#include <string>
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
export module wrappers.sdl_image; export module wrappers.sdl_image;
import std;
import utils.memory; import utils.memory;
import wrappers.sdl.error; import wrappers.sdl.error;
import wrappers.sdl.render; import wrappers.sdl.render;
import wrappers.sdl.surface;
export namespace sdl_image export namespace sdl_image
{ {
/** /**
* Wrapper around IMG_LoadTexture(). * Wrapper for IMG_Load().
*/ */
sdl::Texture LoadTexture(const sdl::Renderer& renderer, const std::string& filename) sdl::Surface load(const std::string& filename)
{ {
SDL_Texture* sdl_texture = IMG_LoadTexture(renderer.get_raw(), filename.c_str()); SDL_Surface* raw_surface = IMG_Load(filename.c_str());
if (sdl_texture == nullptr) { if (raw_surface == nullptr) {
throw sdl::SDLException("IMG_Load");
}
return sdl::Surface{raw_surface};
}
/**
* Wrapper for IMG_LoadTexture().
*/
sdl::Texture load_texture(const sdl::Renderer& renderer, const std::string& filename)
{
SDL_Texture* raw_texture = IMG_LoadTexture(renderer.get_raw(), filename.c_str());
if (raw_texture == nullptr) {
throw sdl::SDLException("IMG_LoadTexture"); throw sdl::SDLException("IMG_LoadTexture");
} }
return sdl::Texture{sdl_texture}; return sdl::Texture{raw_texture};
} }
} }

View file

@ -3,7 +3,7 @@
############################################ ############################################
# shuriken - A ninja-based C++ build tool # shuriken - A ninja-based C++ build tool
# ------------------------------------------ # ------------------------------------------
# Version: 0.1.0 # Version: 0.1.0 (modified)
# Author: binaryDiv # Author: binaryDiv
# License: MIT License # License: MIT License
# https://git.0xbd.space/binaryDiv/shuriken # https://git.0xbd.space/binaryDiv/shuriken
@ -64,6 +64,7 @@ class TargetConfig:
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')
@ -76,6 +77,8 @@ class ShurikenConfig:
linker_args='', 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)
@ -86,13 +89,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] == '.':
@ -105,8 +108,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))
@ -186,8 +192,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(
@ -196,6 +202,13 @@ 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',
@ -204,11 +217,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',
) )
@ -298,6 +311,9 @@ 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')
@ -413,6 +429,26 @@ 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)
@ -454,13 +490,14 @@ 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(deps)}') print(f'build {out}: dyndep | {" ".join(str(dep) for dep in 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:
@ -470,6 +507,7 @@ 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):
@ -487,6 +525,7 @@ 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'):
@ -499,12 +538,25 @@ 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/{'.'.join(relative_name.with_suffix('.pcm').parts)}", pcm=f"$builddir/pcm/{module_name}",
)) ))
# Compile targets for standard library modules ("import std")
if self.config.enable_import_std:
# NOTE: Add std.compat to the list (here and a few other places) if we ever need it. For now, std is enough.
std_module_source_finder = StdModuleSourceFinder(config=self.config, target=self.target)
std_module_sources = std_module_source_finder.find_module_sources(['std'])
for module_name, module_src_path in std_module_sources.items():
self.compile_targets_std_module.append(CompileTargetStdModule(
original_src=str(module_src_path),
generated_src=f"$builddir/generated/stdlib/{module_name}.cppm",
pcm=f"$builddir/pcm/{module_name}.pcm",
))
# Link target for output file (executable): gather all object files # 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
@ -530,12 +582,18 @@ compdb_file = {build_dir}/compile_commands.json
dyndep_file = $builddir/.ninja_dyndep dyndep_file = $builddir/.ninja_dyndep
# Compiler flags # Compiler flags
cpp_flags = -std={target_config.cpp_standard} -fprebuilt-module-path=$builddir/pcm/ {target_config.merged_cpp_flags} cpp_base_flags = -std={target_config.cpp_standard} -fprebuilt-module-path=$builddir/pcm/
cpp_flags = $cpp_base_flags {target_config.merged_cpp_flags}
cpp_std_module_flags = $cpp_base_flags -Wno-reserved-module-identifier -x c++-module
linker_flags = {target_config.merged_linker_flags} linker_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
command = {target_config.cpp_compiler} $cpp_flags -c $in -o $out command = {target_config.cpp_compiler} $cpp_flags -c $in -o $out
@ -544,13 +602,17 @@ rule cpp
rule cppm rule cppm
command = {target_config.cpp_compiler} $cpp_flags -c $in -o $out -fmodule-output=$pcm_out command = {target_config.cpp_compiler} $cpp_flags -c $in -o $out -fmodule-output=$pcm_out
# Rule to compile a standard library module to a .pcm file (e.g. "import std")
rule std_module
command = {target_config.cpp_compiler} $cpp_std_module_flags --precompile $in -o $out
# Rule to link several .o files to an executable # Rule to link several .o files to an executable
rule link rule link
command = {target_config.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 > $out command = ninja -f $in -t compdb cpp cppm std_module > $out
# Rule to generate a Ninja dyndep file for C++ module dependencies based on a compilation database # Rule to generate a Ninja dyndep file for C++ module dependencies based on a compilation database
rule generate_dyndeps rule generate_dyndeps
@ -560,7 +622,7 @@ 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 {self.config.get_ninja_file_path(config.default_target)} build $compdb_file: generate_compdb {self.config.get_ninja_file_path(config.default_target)} | generate_source_files
# Shortcut alias to generate compilation database # Shortcut alias to generate compilation database
build compdb: phony $compdb_file build compdb: phony $compdb_file
@ -575,15 +637,23 @@ build $dyndep_file: generate_dyndeps $compdb_file
build $builddir/{target_config.output_file}: link {' '.join(self.link_target_obj_files)} 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 {self.list_generated_source_files()}
{self.render_compile_targets()} {self.render_compile_targets()}
# End of build.{target}.ninja # End of build.{target}.ninja
""".strip() """.strip()
def list_generated_source_files(self) -> str:
generated_files = [target.generated_src for target in self.compile_targets_std_module]
return ' '.join(generated_files)
def render_compile_targets(self) -> str: def render_compile_targets(self) -> str:
rendered_targets_cpp = [self.render_compile_target_cpp(target) for target in self.compile_targets_cpp] rendered_targets = list(map(self.render_compile_target_std_module, self.compile_targets_std_module))
rendered_targets_cppm = [self.render_compile_target_cppm(target) for target in self.compile_targets_cppm] rendered_targets += list(map(self.render_compile_target_cpp, self.compile_targets_cpp))
return '\n\n'.join(rendered_targets_cpp + rendered_targets_cppm) rendered_targets += list(map(self.render_compile_target_cppm, self.compile_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:
@ -600,23 +670,34 @@ 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[str, list[str]]: def parse_build_dependencies(self, parsed_p1689: dict[str, Any]) -> dict[Path, list[Path]]:
# 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:
@ -624,15 +705,19 @@ 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}'
expected_source_path = str(Path(source_dir, module_name.replace('.', '/') + '.cppm')) if module_name != 'std':
assert provides[0]['source-path'] == expected_source_path, f"Module name does not match source path: {rule}" expected_source_path = str(Path(source_dir, module_name.replace('.', '/') + '.cppm'))
assert provides[0]['source-path'] == expected_source_path, \
f"Module name does not match source path: {rule}"
module_map[module_name] = str(Path(target_build_dir, 'pcm', module_name + '.pcm')) module_map[module_name] = Path(target_build_dir, 'pcm', module_name + '.pcm')
# Collect all module dependencies to generate dyndeps
build_dependencies: dict[Path, list[Path]] = {}
# Parse rules and get module dependencies # 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)
@ -640,11 +725,65 @@ 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 Normal file
View file

@ -0,0 +1,30 @@
# Vendor dependencies
TODO: This should be automated with shuriken at some point. For now, manual instructions.
## System dependencies
For now, SDL3 and SDL3_image need to be installed on the system. They should be vendored at some point.
Same for freetype, which is required by RmlUi.
## RmlUi
[RmlUi](https://github.com/mikke89/RmlUi) is a UI library.
Within the `vendor` directory, run:
```shell
# Either clone git repository or download and unpack archive (version 6.2)
git clone git@github.com:mikke89/RmlUi.git RmlUi --branch 6.2
cd RmlUi
# Configure and build with cmake (use samples or standalone preset)
cmake -G Ninja -B Build --preset samples -DBUILD_SHARED_LIBS=false -DRMLUI_BACKEND=SDL_SDLrenderer
cmake --build Build
# Optionally test by running a sample
./Build/rmlui_sample_demo
```