Added raylib+rmlui template

This commit is contained in:
2026-08-23 20:57:32 -05:00
commit 1a378aebf0
15 changed files with 685 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
build/
build-wasm/
.envrc
.direnv/
.vscode/
result
.cache/
compile_commands.json

69
CMakeLists.txt Normal file
View File

@@ -0,0 +1,69 @@
cmake_minimum_required(VERSION 3.20)
project(gamejam LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
# ---------------------------------------------------------------------------
# Dependencies
#
# Native build: raylib + RmlUi come from nixpkgs / the `rmlui` derivation,
# found on the default system paths set up by the dev shell.
#
# Wasm build: raylib + RmlUi come from the `raylibWasm` / `rmluiWasm`
# derivations, located via -DCMAKE_PREFIX_PATH set by devShells.wasm.
# ---------------------------------------------------------------------------
find_package(raylib REQUIRED)
find_package(RmlUi REQUIRED)
add_executable(gamejam
src/main.cpp
src/Application.cpp
src/backend/RaylibSystemInterface.cpp
src/backend/RaylibRenderInterface.cpp
src/backend/RaylibInput.cpp
)
target_link_libraries(gamejam PRIVATE
raylib
RmlUi::Core
RmlUi::Debugger
)
target_include_directories(gamejam PRIVATE ${CMAKE_SOURCE_DIR}/src)
set(ASSET_DIR ${CMAKE_SOURCE_DIR}/assets)
if(EMSCRIPTEN)
# -----------------------------------------------------------------------
# Wasm-specific settings
# -----------------------------------------------------------------------
set(CMAKE_EXECUTABLE_SUFFIX ".html")
target_link_options(gamejam PRIVATE
"-sUSE_GLFW=3"
"-sASYNCIFY"
"-sALLOW_MEMORY_GROWTH=1"
"-sFORCE_FILESYSTEM=1"
"--preload-file=${ASSET_DIR}@assets"
# "--shell-file=${CMAKE_SOURCE_DIR}/shell.html"
)
else()
add_custom_command(TARGET gamejam POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${ASSET_DIR} $<TARGET_FILE_DIR:gamejam>/assets
COMMENT "Copying assets to build output"
)
endif()
install(TARGETS gamejam RUNTIME DESTINATION bin)
if(NOT EMSCRIPTEN)
install(DIRECTORY ${ASSET_DIR} DESTINATION bin)
endif()

39
assets/main.rcss Normal file
View File

@@ -0,0 +1,39 @@
body {
font-family: rmlui-debugger-font;
font-size: 16px;
color: white;
}
#panel {
position: absolute;
top: 40px;
left: 40px;
width: 300px;
padding: 20px;
background-color: rgba(20, 20, 20, 220);
border: 2px #888;
}
h1 {
font-size: 24px;
color: #ffd35c;
margin-bottom: 10px;
}
p {
margin-bottom: 15px;
}
#start-btn {
display: block;
width: 100px;
padding: 8px;
text-align: center;
background-color: #3a6ea5;
color: white;
border: 1px #5a8ec5;
}
#start-btn:hover {
background-color: #4a7eb5;
}

13
assets/main.rml Normal file
View File

@@ -0,0 +1,13 @@
<rml>
<head>
<title>gamejam</title>
<link type="text/rcss" href="main.rcss"/>
</head>
<body>
<div id="panel">
<h1>gamejam</h1>
<p>RmlUi + raylib scaffold is alive.</p>
<button id="start-btn">Start</button>
</div>
</body>
</rml>

61
flake.lock generated Normal file
View File

@@ -0,0 +1,61 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1787360063,
"narHash": "sha256-dt4WdcvsA8/RCe+VZZwqU0X+XMM3wBbGCWA0/sFWzGo=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2c423e03bbafcff28bfadc6781a4a8257f205cb5",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

195
flake.nix Normal file
View File

@@ -0,0 +1,195 @@
{
description = "Raylib + RmlUI gamejam dev environment (desktop + wasm)";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, utils }:
utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
rmluiSrc = pkgs.fetchFromGitHub {
owner = "mikke89";
repo = "RmlUi";
rev = "9c74c4fadc2ad7f1496239e560bab19ca2fe5072";
hash = "sha256-YZz41ViE3+a0VPypnbJ1Orlf5Hyxd65se5yzyHIY7ZI=";
};
rmluiCmakeFlags = [
"-DRMLUI_LUA_BINDINGS=OFF"
"-DRMLUI_SVG_PLUGIN=OFF"
"-DBUILD_SAMPLES=OFF"
"-DBUILD_TESTING=OFF"
];
# Emscripten's toolchain file restricts find_library/find_path/find_package
# to CMAKE_FIND_ROOT_PATH (its own sysroot) by default. Our externally-built
# wasm deps (freetypeWasm, raylibWasm, rmluiWasm) live in the Nix store,
# outside that sysroot, so CMAKE_PREFIX_PATH alone is silently ignored
# unless we relax these three modes to BOTH.
wasmFindOverrides = [
"-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH"
"-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH"
"-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH"
];
# ---- native RmlUi (shared lib) --------------------------------------
rmlui = pkgs.stdenv.mkDerivation {
pname = "rmlui";
version = "master";
src = rmluiSrc;
nativeBuildInputs = [ pkgs.cmake pkgs.pkg-config ];
buildInputs = [ pkgs.freetype ];
cmakeFlags = rmluiCmakeFlags ++ [ "-DBUILD_SHARED_LIBS=ON" ];
};
# ---- WASM Freetype (static, minimal deps) ---------------------------
freetypeWasm = pkgs.stdenv.mkDerivation {
pname = "freetype-wasm";
version = pkgs.freetype.version;
src = pkgs.freetype.src;
nativeBuildInputs = [ pkgs.cmake pkgs.emscripten ];
dontFixup = true;
configurePhase = ''
emcmake cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DFT_DISABLE_ZLIB=TRUE \
-DFT_DISABLE_BZIP2=TRUE \
-DFT_DISABLE_PNG=TRUE \
-DFT_DISABLE_HARFBUZZ=TRUE \
-DFT_DISABLE_BROTLI=TRUE \
-DCMAKE_INSTALL_PREFIX=$out
'';
buildPhase = "emmake make -C build -j$NIX_BUILD_CORES";
installPhase = "emmake make -C build install";
};
# ---- WASM RmlUi (static) ---------------------------------------------
rmluiWasm = pkgs.stdenv.mkDerivation {
pname = "rmlui-wasm";
version = "master";
src = rmluiSrc;
nativeBuildInputs = [ pkgs.cmake pkgs.emscripten ];
dontFixup = true;
configurePhase = ''
emcmake cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
${pkgs.lib.concatStringsSep " " rmluiCmakeFlags} \
${pkgs.lib.concatStringsSep " " wasmFindOverrides} \
-DCMAKE_PREFIX_PATH="${freetypeWasm}" \
-DCMAKE_INSTALL_PREFIX=$out
'';
buildPhase = "emmake make -C build -j$NIX_BUILD_CORES";
installPhase = "emmake make -C build install";
};
# ---- WASM raylib (PLATFORM=Web) ---------------------------------------
raylibWasm = pkgs.stdenv.mkDerivation {
pname = "raylib-wasm";
version = pkgs.raylib.version;
src = pkgs.raylib.src;
nativeBuildInputs = [ pkgs.cmake pkgs.emscripten ];
dontFixup = true;
configurePhase = ''
emcmake cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DPLATFORM=Web \
-DBUILD_EXAMPLES=OFF \
-DCMAKE_INSTALL_PREFIX=$out
'';
buildPhase = "emmake make -C build -j$NIX_BUILD_CORES";
installPhase = "emmake make -C build install";
};
# ---- native toolchain / runtime ---------------------------------------
nativeBuildTools = with pkgs; [
cmake
pkg-config
clang-tools
];
nativeRuntimeDeps = with pkgs; [
raylib
libGL
libX11
libXcursor
libXrandr
libXinerama
libXi
wayland
libxkbcommon
rmlui
freetype
];
in
{
devShells.default = pkgs.mkShell {
nativeBuildInputs = nativeBuildTools;
buildInputs = nativeRuntimeDeps;
shellHook = ''
export C_INCLUDE_PATH="${rmlui}/include:${pkgs.raylib}/include"
export CPLUS_INCLUDE_PATH="$C_INCLUDE_PATH"
export LIBRARY_PATH="${pkgs.lib.makeLibraryPath nativeRuntimeDeps}"
export LD_LIBRARY_PATH="$LIBRARY_PATH:$LD_LIBRARY_PATH"
echo "native dev shell ready (raylib + rmlui)"
'';
};
devShells.wasm = pkgs.mkShell {
nativeBuildInputs = [ pkgs.cmake pkgs.emscripten pkgs.python3 ];
shellHook = ''
export RAYLIB_WASM="${raylibWasm}"
export RMLUI_WASM="${rmluiWasm}"
export FREETYPE_WASM="${freetypeWasm}"
echo "wasm dev shell ready configure with:"
echo " emcmake cmake -B build-wasm -DPLATFORM=Web \\"
echo " -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH \\"
echo " -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH \\"
echo " -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH \\"
echo " -DCMAKE_PREFIX_PATH=\"\$RAYLIB_WASM;\$RMLUI_WASM;\$FREETYPE_WASM\""
echo " emmake make -C build-wasm"
'';
};
packages.default = pkgs.stdenv.mkDerivation {
pname = "gamejam";
version = "0.1.0";
src = ./.;
nativeBuildInputs = nativeBuildTools ++ [ pkgs.makeWrapper ];
buildInputs = nativeRuntimeDeps;
cmakeFlags = [ "-DCMAKE_BUILD_TYPE=Release" ];
postInstall = ''
wrapProgram $out/bin/gamejam \
--prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath nativeRuntimeDeps}"
'';
};
packages.wasm = pkgs.stdenv.mkDerivation {
pname = "gamejam-wasm";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [ pkgs.cmake pkgs.emscripten ];
dontFixup = true;
configurePhase = ''
emcmake cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DPLATFORM=Web \
${pkgs.lib.concatStringsSep " " wasmFindOverrides} \
-DCMAKE_PREFIX_PATH="${raylibWasm};${rmluiWasm};${freetypeWasm}" \
-DCMAKE_INSTALL_PREFIX=$out
'';
buildPhase = "emmake make -C build -j$NIX_BUILD_CORES";
installPhase = ''
mkdir -p $out
cp -r build/*.html build/*.js build/*.wasm $out/ 2>/dev/null || true
emmake make -C build install || true
'';
};
}
);
}

51
src/Application.cpp Normal file
View File

@@ -0,0 +1,51 @@
#include "Application.h"
#include "backend/RaylibInput.h"
#include <raylib.h>
#include <RmlUi/Core.h>
#include <RmlUi/Debugger.h>
bool Application::Init(int width, int height, const char* title, const char* documentPath) {
InitWindow(width, height, title);
SetTargetFPS(60);
Rml::SetSystemInterface(&system_interface);
Rml::SetRenderInterface(&render_interface);
Rml::Initialise();
context = Rml::CreateContext("main", Rml::Vector2i(width, height));
Rml::Debugger::Initialise(context);
// TODO: load a real font
if (Rml::ElementDocument* document = context->LoadDocument(documentPath)) {
document->Show();
} else {
TraceLog(LOG_WARNING, "Failed to load %s", documentPath);
}
return true;
}
void Application::Frame() {
if (WindowShouldClose()) {
running = false;
return;
}
ForwardInputToRml(context);
context->Update();
BeginDrawing();
ClearBackground(DARKGRAY);
// --- game world rendering goes here, before the UI ---
context->Render();
EndDrawing();
}
void Application::Shutdown() {
Rml::Shutdown();
CloseWindow();
}

19
src/Application.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include <RmlUi/Core/Context.h>
#include "backend/RaylibSystemInterface.h"
#include "backend/RaylibRenderInterface.h"
class Application {
public:
bool Init(int width, int height, const char* title, const char* documentPath);
void Frame();
void Shutdown();
bool IsRunning() const { return running; }
private:
RaylibSystemInterface system_interface;
RaylibRenderInterface render_interface;
Rml::Context* context = nullptr;
bool running = true;
};

View File

@@ -0,0 +1,25 @@
#include "RaylibInput.h"
#include <raylib.h>
namespace {
int GetRmlKeyModifiers() {
int mods = 0;
if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) mods |= Rml::Input::KM_SHIFT;
if (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL)) mods |= Rml::Input::KM_CTRL;
if (IsKeyDown(KEY_LEFT_ALT) || IsKeyDown(KEY_RIGHT_ALT)) mods |= Rml::Input::KM_ALT;
return mods;
}
}
void ForwardInputToRml(Rml::Context* context) {
Vector2 mouse = GetMousePosition();
context->ProcessMouseMove((int)mouse.x, (int)mouse.y, GetRmlKeyModifiers());
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) context->ProcessMouseButtonDown(0, GetRmlKeyModifiers());
if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) context->ProcessMouseButtonUp(0, GetRmlKeyModifiers());
if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) context->ProcessMouseButtonDown(1, GetRmlKeyModifiers());
if (IsMouseButtonReleased(MOUSE_BUTTON_RIGHT)) context->ProcessMouseButtonUp(1, GetRmlKeyModifiers());
float wheel = GetMouseWheelMove();
if (wheel != 0.0f) context->ProcessMouseWheel(-wheel, GetRmlKeyModifiers());
}

View File

@@ -0,0 +1,4 @@
#pragma once
#include <RmlUi/Core/Context.h>
void ForwardInputToRml(Rml::Context* context);

View File

@@ -0,0 +1,109 @@
#include "RaylibRenderInterface.h"
#include <raylib.h>
#include <rlgl.h>
#include <vector>
#include <cstring>
namespace {
struct GeometryData {
std::vector<Rml::Vertex> vertices;
std::vector<int> indices;
};
}
Rml::CompiledGeometryHandle RaylibRenderInterface::CompileGeometry(
Rml::Span<const Rml::Vertex> vertices, Rml::Span<const int> indices) {
auto* geo = new GeometryData();
geo->vertices.assign(vertices.begin(), vertices.end());
geo->indices.assign(indices.begin(), indices.end());
return reinterpret_cast<Rml::CompiledGeometryHandle>(geo);
}
void RaylibRenderInterface::RenderGeometry(Rml::CompiledGeometryHandle handle,
Rml::Vector2f translation,
Rml::TextureHandle texture) {
auto* geo = reinterpret_cast<GeometryData*>(handle);
if (texture) {
Texture2D* tex = reinterpret_cast<Texture2D*>(texture);
rlSetTexture(tex->id);
} else {
rlSetTexture(rlGetTextureIdDefault());
}
rlPushMatrix();
rlTranslatef(translation.x, translation.y, 0.0f);
rlBegin(RL_TRIANGLES);
for (size_t i = 0; i + 2 < geo->indices.size(); i += 3) {
for (int j = 0; j < 3; ++j) {
const Rml::Vertex& v = geo->vertices[geo->indices[i + j]];
rlColor4ub(v.colour.red, v.colour.green, v.colour.blue, v.colour.alpha);
rlTexCoord2f(v.tex_coord.x, v.tex_coord.y);
rlVertex2f(v.position.x, v.position.y);
}
}
rlEnd();
rlPopMatrix();
rlSetTexture(0);
}
void RaylibRenderInterface::ReleaseGeometry(Rml::CompiledGeometryHandle handle) {
delete reinterpret_cast<GeometryData*>(handle);
}
Rml::TextureHandle RaylibRenderInterface::LoadTexture(Rml::Vector2i& texture_dimensions,
const Rml::String& source) {
Texture2D tex = ::LoadTexture(source.c_str());
if (tex.id == 0) {
TraceLog(LOG_WARNING, "[RmlUi] failed to load texture: %s", source.c_str());
return 0;
}
texture_dimensions = Rml::Vector2i(tex.width, tex.height);
return reinterpret_cast<Rml::TextureHandle>(new Texture2D(tex));
}
Rml::TextureHandle RaylibRenderInterface::GenerateTexture(Rml::Span<const Rml::byte> source_data,
Rml::Vector2i source_dimensions) {
Image img{};
img.width = source_dimensions.x;
img.height = source_dimensions.y;
img.mipmaps = 1;
img.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8;
size_t byte_count = static_cast<size_t>(img.width) * img.height * 4;
img.data = RL_MALLOC(byte_count);
std::memcpy(img.data, source_data.data(), byte_count);
Texture2D tex = LoadTextureFromImage(img);
UnloadImage(img); // frees img.data
return reinterpret_cast<Rml::TextureHandle>(new Texture2D(tex));
}
void RaylibRenderInterface::ReleaseTexture(Rml::TextureHandle handle) {
Texture2D* tex = reinterpret_cast<Texture2D*>(handle);
UnloadTexture(*tex);
delete tex;
}
void RaylibRenderInterface::EnableScissorRegion(bool enable) {
scissor_enabled = enable;
if (enable) ApplyScissor();
else EndScissorMode();
}
void RaylibRenderInterface::SetScissorRegion(Rml::Rectanglei region) {
scissor_region = region;
if (scissor_enabled) ApplyScissor();
}
void RaylibRenderInterface::ApplyScissor() {
BeginScissorMode(scissor_region.Left(), scissor_region.Top(),
scissor_region.Width(), scissor_region.Height());
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include <RmlUi/Core/RenderInterface.h>
// NOTE: RmlUi's RenderInterface signature has shifted across versions
// (raw pointer+count vs. Rml::Span<>). If this fails to compile against
// a different RmlUi commit, check RmlUi/Core/RenderInterface.h and adjust
// CompileGeometry/GenerateTexture's parameter types, internal logic
// stays the same.
class RaylibRenderInterface : public Rml::RenderInterface {
public:
Rml::CompiledGeometryHandle CompileGeometry(Rml::Span<const Rml::Vertex> vertices,
Rml::Span<const int> indices) override;
void RenderGeometry(Rml::CompiledGeometryHandle handle, Rml::Vector2f translation,
Rml::TextureHandle texture) override;
void ReleaseGeometry(Rml::CompiledGeometryHandle handle) override;
Rml::TextureHandle LoadTexture(Rml::Vector2i& texture_dimensions,
const Rml::String& source) override;
Rml::TextureHandle GenerateTexture(Rml::Span<const Rml::byte> source_data,
Rml::Vector2i source_dimensions) override;
void ReleaseTexture(Rml::TextureHandle handle) override;
void EnableScissorRegion(bool enable) override;
void SetScissorRegion(Rml::Rectanglei region) override;
private:
void ApplyScissor();
bool scissor_enabled = false;
Rml::Rectanglei scissor_region;
};

View File

@@ -0,0 +1,14 @@
#include "RaylibSystemInterface.h"
#include <raylib.h>
double RaylibSystemInterface::GetElapsedTime() {
return GetTime();
}
bool RaylibSystemInterface::LogMessage(Rml::Log::Type type, const Rml::String& message) {
TraceLogLevel level = LOG_INFO;
if (type == Rml::Log::LT_WARNING) level = LOG_WARNING;
if (type == Rml::Log::LT_ERROR || type == Rml::Log::LT_ASSERT) level = LOG_ERROR;
TraceLog(level, "[RmlUi] %s", message.c_str());
return true;
}

View File

@@ -0,0 +1,8 @@
#pragma once
#include <RmlUi/Core/SystemInterface.h>
class RaylibSystemInterface : public Rml::SystemInterface {
public:
double GetElapsedTime() override;
bool LogMessage(Rml::Log::Type type, const Rml::String& message) override;
};

33
src/main.cpp Normal file
View File

@@ -0,0 +1,33 @@
#include "Application.h"
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
namespace {
Application* g_app = nullptr;
void MainLoopStep() {
g_app->Frame();
}
} // namespace
int main() {
static Application app;
g_app = &app;
app.Init(1280, 720, "gamejam", "assets/main.rml");
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop(MainLoopStep, 0, 1);
#else
while (app.IsRunning()) {
app.Frame();
}
#endif
app.Shutdown();
return 0;
}