// IAEngine: 2D Game Engine by IA // Copyright (C) 2025 IASoft (PVT) LTD (oss@iasoft.dev) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . #include #include #include #include #include #include namespace ia::iae { Rml::Context *g_context{}; BOOL g_debuggerEnabled{false}; Rml::ElementDocument *g_document{}; String g_markup; Rml::SharedPtr g_styleSheetContainer; class RmlUIRenderInterface : public Rml::RenderInterface { public: Rml::CompiledGeometryHandle CompileGeometry(Rml::Span vertices, Rml::Span indices); void RenderGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle texture); void ReleaseGeometry(Rml::CompiledGeometryHandle geometry); Rml::TextureHandle LoadTexture(Rml::Vector2i &texture_dimensions, const Rml::String &source); Rml::TextureHandle GenerateTexture(Rml::Span source, Rml::Vector2i source_dimensions); void ReleaseTexture(Rml::TextureHandle texture); void EnableScissorRegion(bool enable); void SetScissorRegion(Rml::Rectanglei region); } g_rmlUIRenderInterface{}; class RmlEventListener : public Rml::EventListener { public: RmlEventListener() { } VOID AddClickListener(IN Rml::Element *element, IN std::function callback) { element->AddEventListener(Rml::EventId::Click, this); m_clickCallbacks[element->GetId().c_str()] = callback; } VOID AddPointerDownListener(IN Rml::Element *element, IN std::function callback) { element->AddEventListener(Rml::EventId::Mousedown, this); m_pointerDownCallbacks[element->GetId().c_str()] = callback; } VOID AddPointerUpListener(IN Rml::Element *element, IN std::function callback) { element->AddEventListener(Rml::EventId::Mouseup, this); m_pointerUpCallbacks[element->GetId().c_str()] = callback; } VOID AddPointerEnterListener(IN Rml::Element *element, IN std::function callback) { element->AddEventListener(Rml::EventId::Mouseover, this); m_hoverEnterCallbacks[element->GetId().c_str()] = callback; } VOID AddPointerExitListener(IN Rml::Element *element, IN std::function callback) { element->AddEventListener(Rml::EventId::Mouseout, this); m_hoverExitCallbacks[element->GetId().c_str()] = callback; } VOID ProcessEvent(IN Rml::Event &event) { switch (event.GetId()) { case Rml::EventId::Click: m_clickCallbacks[event.GetTargetElement()->GetId().c_str()](); break; case Rml::EventId::Mouseover: m_hoverEnterCallbacks[event.GetTargetElement()->GetId().c_str()](); break; case Rml::EventId::Mouseout: m_hoverExitCallbacks[event.GetTargetElement()->GetId().c_str()](); break; case Rml::EventId::Mousedown: { m_pointerDownCallbacks[event.GetTargetElement()->GetId().c_str()](); } break; case Rml::EventId::Mouseup: { m_pointerUpCallbacks[event.GetTargetElement()->GetId().c_str()](); } break; default: break; } } private: Map> m_clickCallbacks; Map> m_hoverEnterCallbacks; Map> m_hoverExitCallbacks; Map> m_pointerDownCallbacks; Map> m_pointerUpCallbacks; } g_eventListener{}; /* Taken from https://github.com/mikke89/RmlUi/blob/master/Backends/RmlUi_Platform_SDL.cpp */ INT32 GetKeyModifierState() { SDL_Keymod sdl_mods = SDL_GetModState(); INT32 keymods = 0; if (sdl_mods & SDL_KMOD_CTRL) keymods |= Rml::Input::KM_CTRL; if (sdl_mods & SDL_KMOD_SHIFT) keymods |= Rml::Input::KM_SHIFT; if (sdl_mods & SDL_KMOD_ALT) keymods |= Rml::Input::KM_ALT; if (sdl_mods & SDL_KMOD_NUM) keymods |= Rml::Input::KM_NUMLOCK; if (sdl_mods & SDL_KMOD_CAPS) keymods |= Rml::Input::KM_CAPSLOCK; return keymods; } /* Taken from https://github.com/mikke89/RmlUi/blob/master/Backends/RmlUi_Platform_SDL.cpp */ INT32 SDLMouseButtonToRml(IN INT32 button) { switch (button) { case SDL_BUTTON_LEFT: return 0; case SDL_BUTTON_RIGHT: return 1; case SDL_BUTTON_MIDDLE: return 2; default: break; } return 3; } /* Taken from https://github.com/mikke89/RmlUi/blob/master/Backends/RmlUi_Platform_SDL.cpp */ Rml::Input::KeyIdentifier SDLKeyToRml(int sdlkey) { switch (sdlkey) { case SDLK_UNKNOWN: return Rml::Input::KI_UNKNOWN; case SDLK_ESCAPE: return Rml::Input::KI_ESCAPE; case SDLK_SPACE: return Rml::Input::KI_SPACE; case SDLK_0: return Rml::Input::KI_0; case SDLK_1: return Rml::Input::KI_1; case SDLK_2: return Rml::Input::KI_2; case SDLK_3: return Rml::Input::KI_3; case SDLK_4: return Rml::Input::KI_4; case SDLK_5: return Rml::Input::KI_5; case SDLK_6: return Rml::Input::KI_6; case SDLK_7: return Rml::Input::KI_7; case SDLK_8: return Rml::Input::KI_8; case SDLK_9: return Rml::Input::KI_9; case SDLK_A: return Rml::Input::KI_A; case SDLK_B: return Rml::Input::KI_B; case SDLK_C: return Rml::Input::KI_C; case SDLK_D: return Rml::Input::KI_D; case SDLK_E: return Rml::Input::KI_E; case SDLK_F: return Rml::Input::KI_F; case SDLK_G: return Rml::Input::KI_G; case SDLK_H: return Rml::Input::KI_H; case SDLK_I: return Rml::Input::KI_I; case SDLK_J: return Rml::Input::KI_J; case SDLK_K: return Rml::Input::KI_K; case SDLK_L: return Rml::Input::KI_L; case SDLK_M: return Rml::Input::KI_M; case SDLK_N: return Rml::Input::KI_N; case SDLK_O: return Rml::Input::KI_O; case SDLK_P: return Rml::Input::KI_P; case SDLK_Q: return Rml::Input::KI_Q; case SDLK_R: return Rml::Input::KI_R; case SDLK_S: return Rml::Input::KI_S; case SDLK_T: return Rml::Input::KI_T; case SDLK_U: return Rml::Input::KI_U; case SDLK_V: return Rml::Input::KI_V; case SDLK_W: return Rml::Input::KI_W; case SDLK_X: return Rml::Input::KI_X; case SDLK_Y: return Rml::Input::KI_Y; case SDLK_Z: return Rml::Input::KI_Z; case SDLK_SEMICOLON: return Rml::Input::KI_OEM_1; case SDLK_PLUS: return Rml::Input::KI_OEM_PLUS; case SDLK_COMMA: return Rml::Input::KI_OEM_COMMA; case SDLK_MINUS: return Rml::Input::KI_OEM_MINUS; case SDLK_PERIOD: return Rml::Input::KI_OEM_PERIOD; case SDLK_SLASH: return Rml::Input::KI_OEM_2; case SDLK_GRAVE: return Rml::Input::KI_OEM_3; case SDLK_LEFTBRACKET: return Rml::Input::KI_OEM_4; case SDLK_BACKSLASH: return Rml::Input::KI_OEM_5; case SDLK_RIGHTBRACKET: return Rml::Input::KI_OEM_6; case SDLK_DBLAPOSTROPHE: return Rml::Input::KI_OEM_7; case SDLK_KP_0: return Rml::Input::KI_NUMPAD0; case SDLK_KP_1: return Rml::Input::KI_NUMPAD1; case SDLK_KP_2: return Rml::Input::KI_NUMPAD2; case SDLK_KP_3: return Rml::Input::KI_NUMPAD3; case SDLK_KP_4: return Rml::Input::KI_NUMPAD4; case SDLK_KP_5: return Rml::Input::KI_NUMPAD5; case SDLK_KP_6: return Rml::Input::KI_NUMPAD6; case SDLK_KP_7: return Rml::Input::KI_NUMPAD7; case SDLK_KP_8: return Rml::Input::KI_NUMPAD8; case SDLK_KP_9: return Rml::Input::KI_NUMPAD9; case SDLK_KP_ENTER: return Rml::Input::KI_NUMPADENTER; case SDLK_KP_MULTIPLY: return Rml::Input::KI_MULTIPLY; case SDLK_KP_PLUS: return Rml::Input::KI_ADD; case SDLK_KP_MINUS: return Rml::Input::KI_SUBTRACT; case SDLK_KP_PERIOD: return Rml::Input::KI_DECIMAL; case SDLK_KP_DIVIDE: return Rml::Input::KI_DIVIDE; case SDLK_KP_EQUALS: return Rml::Input::KI_OEM_NEC_EQUAL; case SDLK_BACKSPACE: return Rml::Input::KI_BACK; case SDLK_TAB: return Rml::Input::KI_TAB; case SDLK_CLEAR: return Rml::Input::KI_CLEAR; case SDLK_RETURN: return Rml::Input::KI_RETURN; case SDLK_PAUSE: return Rml::Input::KI_PAUSE; case SDLK_CAPSLOCK: return Rml::Input::KI_CAPITAL; case SDLK_PAGEUP: return Rml::Input::KI_PRIOR; case SDLK_PAGEDOWN: return Rml::Input::KI_NEXT; case SDLK_END: return Rml::Input::KI_END; case SDLK_HOME: return Rml::Input::KI_HOME; case SDLK_LEFT: return Rml::Input::KI_LEFT; case SDLK_UP: return Rml::Input::KI_UP; case SDLK_RIGHT: return Rml::Input::KI_RIGHT; case SDLK_DOWN: return Rml::Input::KI_DOWN; case SDLK_INSERT: return Rml::Input::KI_INSERT; case SDLK_DELETE: return Rml::Input::KI_DELETE; case SDLK_HELP: return Rml::Input::KI_HELP; case SDLK_F1: return Rml::Input::KI_F1; case SDLK_F2: return Rml::Input::KI_F2; case SDLK_F3: return Rml::Input::KI_F3; case SDLK_F4: return Rml::Input::KI_F4; case SDLK_F5: return Rml::Input::KI_F5; case SDLK_F6: return Rml::Input::KI_F6; case SDLK_F7: return Rml::Input::KI_F7; case SDLK_F8: return Rml::Input::KI_F8; case SDLK_F9: return Rml::Input::KI_F9; case SDLK_F10: return Rml::Input::KI_F10; case SDLK_F11: return Rml::Input::KI_F11; case SDLK_F12: return Rml::Input::KI_F12; case SDLK_F13: return Rml::Input::KI_F13; case SDLK_F14: return Rml::Input::KI_F14; case SDLK_F15: return Rml::Input::KI_F15; case SDLK_NUMLOCKCLEAR: return Rml::Input::KI_NUMLOCK; case SDLK_SCROLLLOCK: return Rml::Input::KI_SCROLL; case SDLK_LSHIFT: return Rml::Input::KI_LSHIFT; case SDLK_RSHIFT: return Rml::Input::KI_RSHIFT; case SDLK_LCTRL: return Rml::Input::KI_LCONTROL; case SDLK_RCTRL: return Rml::Input::KI_RCONTROL; case SDLK_LALT: return Rml::Input::KI_LMENU; case SDLK_RALT: return Rml::Input::KI_RMENU; case SDLK_LGUI: return Rml::Input::KI_LMETA; case SDLK_RGUI: return Rml::Input::KI_RMETA; default: break; } return Rml::Input::KI_UNKNOWN; } } // namespace ia::iae namespace ia::iae { INT64 UI::s_numericVariables[MAX_VARIABLE_COUNT]; std::string UI::s_stringVariables[MAX_VARIABLE_COUNT]; Rml::DataModelConstructor g_dataModel; VOID UI::Initialize() { Rml::SetRenderInterface(&g_rmlUIRenderInterface); Rml::Initialise(); Rml::LoadFontFace("Resources/Fonts/Roboto-Black.ttf"); const auto displayExtent = Engine::GetDisplayExtent(); g_context = Rml::CreateContext("main", Rml::Vector2i(displayExtent.x, displayExtent.y)); Rml::Debugger::Initialise(g_context); Rml::Debugger::SetVisible(false); g_document = g_context->CreateDocument(); g_dataModel = g_context->CreateDataModel("ui_data"); for (INT32 i = 0; i < MAX_VARIABLE_COUNT; i++) g_dataModel.Bind(BuildString("NumVar", i).c_str(), &s_numericVariables[i]); for (INT32 i = 0; i < MAX_VARIABLE_COUNT; i++) g_dataModel.Bind(BuildString("StrVar", i).c_str(), &s_stringVariables[i]); } VOID UI::Terminate() { Rml::Shutdown(); } VOID UI::Update() { if (Engine::Input_WasKeyPressed(InputKey::F8)) Rml::Debugger::SetVisible(g_debuggerEnabled = !g_debuggerEnabled); g_context->Update(); } VOID UI::Draw() { g_context->Render(); } static Rml::TouchList TouchEventToTouchList(SDL_Event &ev, Rml::Context *context, SDL_FingerID finger_id) { const Rml::Vector2f position = Rml::Vector2f{ev.tfinger.x, ev.tfinger.y} * Rml::Vector2f{context->GetDimensions()}; return {Rml::Touch{static_cast(finger_id), position}}; } VOID UI::OnSDLEvent(IN PVOID _event) { auto GetFingerId = [](const SDL_Event &event) { return event.tfinger.fingerID; }; const auto keymods = GetKeyModifierState(); const auto event = (SDL_Event *) _event; switch (event->type) { case SDL_EventType::SDL_EVENT_MOUSE_MOTION: g_context->ProcessMouseMove(event->motion.x, event->motion.y, keymods); break; case SDL_EventType::SDL_EVENT_MOUSE_BUTTON_UP: g_context->ProcessMouseButtonUp(SDLMouseButtonToRml(event->button.button), keymods); break; case SDL_EventType::SDL_EVENT_MOUSE_BUTTON_DOWN: g_context->ProcessMouseButtonDown(SDLMouseButtonToRml(event->button.button), keymods); break; case SDL_EventType::SDL_EVENT_KEY_DOWN: g_context->ProcessKeyDown(SDLKeyToRml(event->key.key), keymods); break; case SDL_EventType::SDL_EVENT_KEY_UP: g_context->ProcessKeyUp(SDLKeyToRml(event->key.key), keymods); break; case SDL_EventType::SDL_EVENT_TEXT_INPUT: g_context->ProcessTextInput(Rml::String(&event->text.text[0])); break; case SDL_EventType::SDL_EVENT_FINGER_DOWN: { const Rml::TouchList touches = TouchEventToTouchList(*event, g_context, GetFingerId(*event)); g_context->ProcessTouchStart(touches, GetKeyModifierState()); } break; case SDL_EventType::SDL_EVENT_FINGER_MOTION: { const Rml::TouchList touches = TouchEventToTouchList(*event, g_context, GetFingerId(*event)); g_context->ProcessTouchMove(touches, GetKeyModifierState()); } break; case SDL_EventType::SDL_EVENT_FINGER_UP: { const Rml::TouchList touches = TouchEventToTouchList(*event, g_context, GetFingerId(*event)); g_context->ProcessTouchEnd(touches, GetKeyModifierState()); } } } VOID UI::OnScreenResize(IN INT32 newWidth, IN INT32 newHeight) { g_context->SetDimensions(Rml::Vector2i{newWidth, newHeight}); } VOID UI::AddFontFromFile(IN CONST String &path) { Rml::LoadFontFace(path.c_str()); } VOID UI::SetMarkup(IN CONST String &markup, IN CONST String &styles) { g_styleSheetContainer = Rml::Factory::InstanceStyleSheetString(styles.c_str()); g_markup = BuildString("
", markup, "
"); g_document->SetStyleSheetContainer(g_styleSheetContainer); g_document->SetInnerRML(g_markup.c_str()); } VOID UI::AddClickEvent(IN PCCHAR elementId, IN std::function callback) { g_eventListener.AddClickListener(g_document->GetElementById(elementId), callback); } VOID UI::AddPointerUpEvent(IN PCCHAR elementId, IN std::function callback) { g_eventListener.AddPointerUpListener(g_document->GetElementById(elementId), callback); } VOID UI::AddPointerDownEvent(IN PCCHAR elementId, IN std::function callback) { g_eventListener.AddPointerDownListener(g_document->GetElementById(elementId), callback); } VOID UI::AddPointerExitEvent(IN PCCHAR elementId, IN std::function callback) { g_eventListener.AddPointerExitListener(g_document->GetElementById(elementId), callback); } VOID UI::AddPointerEnterEvent(IN PCCHAR elementId, IN std::function callback) { g_eventListener.AddPointerEnterListener(g_document->GetElementById(elementId), callback); } VOID UI::SetNumericVariable(IN UINT8 index, IN INT64 value) { s_numericVariables[index] = value; } VOID UI::SetStringVariable(IN UINT8 index, IN CONST String &value) { s_stringVariables[index] = value.c_str(); } INT64 UI::GetNumericVariable(IN UINT8 index) { return s_numericVariables[index]; } String UI::GetStringVariable(IN UINT8 index) { return s_stringVariables[index].c_str(); } VOID UI::SetElementWidth(IN CONST String &elementId, IN INT32 value) { g_document->GetElementById(elementId.c_str())->SetProperty("width", BuildString(value, "px").c_str()); } INT32 UI::GetElementWidth(IN CONST String &elementId) { return g_document->GetElementById(elementId.c_str())->GetClientWidth(); } VOID UI::SetElementHeight(IN CONST String &elementId, IN INT32 value) { g_document->GetElementById(elementId.c_str())->SetProperty("height", BuildString(value, "px").c_str()); } INT32 UI::GetElementHeight(IN CONST String &elementId) { return g_document->GetElementById(elementId.c_str())->GetClientHeight(); } } // namespace ia::iae namespace ia::iae { Rml::CompiledGeometryHandle RmlUIRenderInterface::CompileGeometry(Rml::Span vertices, Rml::Span indices) { Vector _vertices; Vector _indices; for (const auto &v : vertices) _vertices.pushBack({Vec2{v.position.x, v.position.y}, Vec2{v.tex_coord.x, v.tex_coord.y}, Vec4{v.colour.red / 255.0f, v.colour.green / 255.0f, v.colour.blue / 255.0f, v.colour.alpha / 255.0f}}); for (const auto &v : indices) _indices.pushBack(v); return (Rml::CompiledGeometryHandle) Engine::CreateGeometry(_vertices, _indices); } void RmlUIRenderInterface::RenderGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle texture) { Engine::SetRenderState_FlippedH(false); Engine::SetRenderState_FlippedV(false); Engine::SetRenderState_ColorOverlay({255, 255, 255, 255}); Engine::SetRenderState_TextureOffset({0, 0}); Engine::SetRenderState_CameraRelative(false); Engine::DrawGeometry((Handle) geometry, (Handle) texture, {translation.x, translation.y}, {1.0f, 1.0f}, 0, 0xFF, 0); } void RmlUIRenderInterface::ReleaseGeometry(Rml::CompiledGeometryHandle geometry) { Engine::DestroyGeometry((Handle) geometry); } Rml::TextureHandle RmlUIRenderInterface::LoadTexture(Rml::Vector2i &texture_dimensions, const Rml::String &source) { Handle result; if (source.starts_with("$H$")) result = (Handle) std::stoi(source.substr(3)); else result = Engine::CreateImageFromFile(Engine::GetUniqueResourceName(), source.c_str()); const auto extent = Engine::GetImageExtent(result); texture_dimensions = {extent.x, extent.y}; return result; } Rml::TextureHandle RmlUIRenderInterface::GenerateTexture(Rml::Span source, Rml::Vector2i source_dimensions) { return Engine::CreateImage(Engine::GetUniqueResourceName(), (PCUINT8) source.data(), source_dimensions.x, source_dimensions.y); } void RmlUIRenderInterface::ReleaseTexture(Rml::TextureHandle texture) { Engine::DestroyImage((Handle) texture); } void RmlUIRenderInterface::EnableScissorRegion(bool enable) { if (!enable) Engine::SetRenderState_Scissor({}); } void RmlUIRenderInterface::SetScissorRegion(Rml::Rectanglei region) { Engine::SetRenderState_Scissor({region.Left(), region.Top(), region.Right(), region.Bottom()}); } } // namespace ia::iae