// // // Undefine stupid windows API defines that get included by the prefix headers #ifdef GetNextSibling #undef GetNextSibling #endif #ifdef GetFirstChild #undef GetFirstChild #endif #include "scpui/rocket_ui.h" #include "cfile/cfile.h" #include "io/mouse.h" #include "mod_table/mod_table.h" #include "osapi/osapi.h" #include "scpui/IncludeNodeHandler.h" #include "scpui/RocketDecoratorsInstancer.h" #include "scpui/RocketFileInterface.h" #include "scpui/RocketLuaSystemInterface.h" #include "scpui/RocketRenderingInterface.h" #include "scpui/RocketSystemInterface.h" #include "scpui/SoundPlugin.h" #include "scpui/elements/AnimationElement.h" #include "scpui/elements/ScrollingTextElement.h" #include "scripting/scripting.h" // Our Assert conflicts with the definitions inside libRocket #pragma push_macro("Assert") #undef Assert #include #include #include #include #include #pragma pop_macro("Assert") #include #include using namespace Rocket::Core; namespace { bool rocket_initialized = false; std::unique_ptr rendering_interface; std::unique_ptr system_interface; std::unique_ptr file_interface; Context* input_context = nullptr; vec2d render_offset = {0.f, 0.f}; bool debugger_initialized = false; /** * @brief Retrieves the current cursor position in render-target pixels * * @details SDL reports event coordinates in window pixels, but a libRocket context is sized from * gr_screen.max_w/max_h and renders through gr_set_2d_matrix, which is also render space. Rather * than duplicating the conversion here we let mouse_get_real_pos do it. * * Note that this is the cursor position as of right now rather than the one carried by the event, * so if several motion events are queued in a single os_poll libRocket sees the latest position * for all of them. That is what it wants for hover state anyway. */ vec2d get_render_space_mouse_pos() { int x = 0; int y = 0; mouse_get_real_pos(&x, &y); return {i2fl(x), i2fl(y)}; } bool transform_to_rocket(vec2d& in) { in = {in.x - render_offset.x, in.y - render_offset.y}; if (in.x < 0.f || in.y < 0.f) { // Out of bounds return false; } auto dims = input_context->GetDimensions(); if (in.x > dims.x || in.y > dims.y) { return false; } return true; } int get_modifier_state() { int mods = 0; auto sdl_mods = SDL_GetModState(); if (sdl_mods & SDL_KMOD_CTRL) { mods |= Input::KM_CTRL; } if (sdl_mods & SDL_KMOD_SHIFT) { mods |= Input::KM_SHIFT; } if (sdl_mods & SDL_KMOD_ALT) { mods |= Input::KM_ALT; } if (sdl_mods & SDL_KMOD_GUI) { mods |= Input::KM_META; } if (sdl_mods & SDL_KMOD_CAPS) { mods |= Input::KM_CAPSLOCK; } if (sdl_mods & SDL_KMOD_NUM) { mods |= Input::KM_NUMLOCK; } auto states = SDL_GetKeyboardState(nullptr); if (states[SDL_SCANCODE_SCROLLLOCK]) { mods |= Input::KM_SCROLLLOCK; } return mods; } void load_fonts() { SCP_vector files; cf_get_file_list(files, CF_TYPE_FONT, "*.ttf"); for (auto& name : files) { // The file extension gets removed by CFile FontDatabase::LoadFontFace((name + ".ttf").c_str()); } files.clear(); cf_get_file_list(files, CF_TYPE_FONT, "*.otf"); for (auto& name : files) { // The file extension gets removed by CFile FontDatabase::LoadFontFace((name + ".otf").c_str()); } } bool mouse_motion_handler(const SDL_Event& evt) { if (!os::events::isWindowEvent(evt, os::getSDLMainWindow())) { // Ignore events that are not relevant to the main window return false; } if (input_context == nullptr) { // No active input context return false; } vec2d evt_pos = get_render_space_mouse_pos(); if (!transform_to_rocket(evt_pos)) { // Out of bounds return false; } input_context->ProcessMouseMove((int)evt_pos.x, (int)evt_pos.y, get_modifier_state()); // Don't consume motion: the engine's own handler still has to track the cursor position, which // mouse_get_pos() and thereby e.g. the cursor drawn into the VR headset image depend on. return false; } bool mouse_button_handler(const SDL_Event& evt) { if (!os::events::isWindowEvent(evt, os::getSDLMainWindow())) { // Ignore events that are not relevant to the main window return false; } if (input_context == nullptr) { // No active input context return false; } vec2d evt_pos = get_render_space_mouse_pos(); if (!transform_to_rocket(evt_pos)) { // Out of bounds return false; } int button; switch (evt.button.button) { case SDL_BUTTON_LEFT: button = 0; break; case SDL_BUTTON_MIDDLE: button = 2; break; case SDL_BUTTON_RIGHT: button = 1; break; case SDL_BUTTON_X1: button = 3; break; case SDL_BUTTON_X2: button = 4; break; default: // Unknown SDL button value mprintf(("Unknown SDL button value %d in libRocket mouse handler!\n", evt.button.button)); return false; } if (evt.type == SDL_EVENT_MOUSE_BUTTON_UP) { // Up input_context->ProcessMouseButtonUp(button, get_modifier_state()); } else { // Down input_context->ProcessMouseButtonDown(button, get_modifier_state()); } return true; } bool mouse_wheel_handler(const SDL_Event& evt) { if (!os::events::isWindowEvent(evt, os::getSDLMainWindow())) { // Ignore events that are not relevant to the main window return false; } if (input_context == nullptr) { // No active input context return false; } int wheel_value = fl2i(evt.wheel.y); if (evt.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) { wheel_value *= -1; } // libRocket expects a different direction than what SDL delivers input_context->ProcessMouseWheel(-wheel_value, get_modifier_state()); return true; } Input::KeyIdentifier translateKey(SDL_Keycode Key) { switch (Key) { case SDLK_A: return Rocket::Core::Input::KI_A; case SDLK_B: return Rocket::Core::Input::KI_B; case SDLK_C: return Rocket::Core::Input::KI_C; case SDLK_D: return Rocket::Core::Input::KI_D; case SDLK_E: return Rocket::Core::Input::KI_E; case SDLK_F: return Rocket::Core::Input::KI_F; case SDLK_G: return Rocket::Core::Input::KI_G; case SDLK_H: return Rocket::Core::Input::KI_H; case SDLK_I: return Rocket::Core::Input::KI_I; case SDLK_J: return Rocket::Core::Input::KI_J; case SDLK_K: return Rocket::Core::Input::KI_K; case SDLK_L: return Rocket::Core::Input::KI_L; case SDLK_M: return Rocket::Core::Input::KI_M; case SDLK_N: return Rocket::Core::Input::KI_N; case SDLK_O: return Rocket::Core::Input::KI_O; case SDLK_P: return Rocket::Core::Input::KI_P; case SDLK_Q: return Rocket::Core::Input::KI_Q; case SDLK_R: return Rocket::Core::Input::KI_R; case SDLK_S: return Rocket::Core::Input::KI_S; case SDLK_T: return Rocket::Core::Input::KI_T; case SDLK_U: return Rocket::Core::Input::KI_U; case SDLK_V: return Rocket::Core::Input::KI_V; case SDLK_W: return Rocket::Core::Input::KI_W; case SDLK_X: return Rocket::Core::Input::KI_X; case SDLK_Y: return Rocket::Core::Input::KI_Y; case SDLK_Z: return Rocket::Core::Input::KI_Z; case SDLK_0: return Rocket::Core::Input::KI_0; case SDLK_1: return Rocket::Core::Input::KI_1; case SDLK_2: return Rocket::Core::Input::KI_2; case SDLK_3: return Rocket::Core::Input::KI_3; case SDLK_4: return Rocket::Core::Input::KI_4; case SDLK_5: return Rocket::Core::Input::KI_5; case SDLK_6: return Rocket::Core::Input::KI_6; case SDLK_7: return Rocket::Core::Input::KI_7; case SDLK_8: return Rocket::Core::Input::KI_8; case SDLK_9: return Rocket::Core::Input::KI_9; case SDLK_KP_0: return Rocket::Core::Input::KI_NUMPAD0; case SDLK_KP_1: return Rocket::Core::Input::KI_NUMPAD1; case SDLK_KP_2: return Rocket::Core::Input::KI_NUMPAD2; case SDLK_KP_3: return Rocket::Core::Input::KI_NUMPAD3; case SDLK_KP_4: return Rocket::Core::Input::KI_NUMPAD4; case SDLK_KP_5: return Rocket::Core::Input::KI_NUMPAD5; case SDLK_KP_6: return Rocket::Core::Input::KI_NUMPAD6; case SDLK_KP_7: return Rocket::Core::Input::KI_NUMPAD7; case SDLK_KP_8: return Rocket::Core::Input::KI_NUMPAD8; case SDLK_KP_9: return Rocket::Core::Input::KI_NUMPAD9; case SDLK_LEFT: return Rocket::Core::Input::KI_LEFT; case SDLK_RIGHT: return Rocket::Core::Input::KI_RIGHT; case SDLK_UP: return Rocket::Core::Input::KI_UP; case SDLK_DOWN: return Rocket::Core::Input::KI_DOWN; case SDLK_KP_PLUS: return Rocket::Core::Input::KI_ADD; case SDLK_KP_MINUS: return Rocket::Core::Input::KI_SUBTRACT; case SDLK_KP_MULTIPLY: return Rocket::Core::Input::KI_MULTIPLY; case SDLK_KP_DIVIDE: return Rocket::Core::Input::KI_DIVIDE; case SDLK_KP_DECIMAL: return Rocket::Core::Input::KI_DECIMAL; case SDLK_KP_ENTER: return Rocket::Core::Input::KI_NUMPADENTER; case SDLK_MINUS: return Rocket::Core::Input::KI_OEM_MINUS; case SDLK_EQUALS: return Rocket::Core::Input::KI_OEM_PLUS; case SDLK_BACKSPACE: return Rocket::Core::Input::KI_BACK; case SDLK_LEFTBRACKET: return Rocket::Core::Input::KI_OEM_4; case SDLK_RIGHTBRACKET: return Rocket::Core::Input::KI_OEM_6; case SDLK_BACKSLASH: return Rocket::Core::Input::KI_OEM_5; case SDLK_SEMICOLON: return Rocket::Core::Input::KI_OEM_1; case SDLK_APOSTROPHE: return Rocket::Core::Input::KI_OEM_7; case SDLK_COMMA: return Rocket::Core::Input::KI_OEM_COMMA; case SDLK_PERIOD: return Rocket::Core::Input::KI_OEM_PERIOD; case SDLK_SLASH: return Rocket::Core::Input::KI_OEM_2; case SDLK_INSERT: return Rocket::Core::Input::KI_INSERT; case SDLK_HOME: return Rocket::Core::Input::KI_HOME; case SDLK_PAGEUP: return Rocket::Core::Input::KI_PRIOR; case SDLK_DELETE: return Rocket::Core::Input::KI_DELETE; case SDLK_END: return Rocket::Core::Input::KI_END; case SDLK_PAGEDOWN: return Rocket::Core::Input::KI_NEXT; case SDLK_F1: return Rocket::Core::Input::KI_F1; case SDLK_F2: return Rocket::Core::Input::KI_F2; case SDLK_F3: return Rocket::Core::Input::KI_F3; case SDLK_F4: return Rocket::Core::Input::KI_F4; case SDLK_F5: return Rocket::Core::Input::KI_F5; case SDLK_F6: return Rocket::Core::Input::KI_F6; case SDLK_F7: return Rocket::Core::Input::KI_F7; case SDLK_F8: return Rocket::Core::Input::KI_F8; case SDLK_F9: return Rocket::Core::Input::KI_F9; case SDLK_F10: return Rocket::Core::Input::KI_F10; case SDLK_F11: return Rocket::Core::Input::KI_F11; case SDLK_F12: return Rocket::Core::Input::KI_F12; case SDLK_F13: return Rocket::Core::Input::KI_F13; case SDLK_F14: return Rocket::Core::Input::KI_F14; case SDLK_F15: return Rocket::Core::Input::KI_F15; case SDLK_ESCAPE: return Rocket::Core::Input::KI_ESCAPE; case SDLK_LCTRL: return Rocket::Core::Input::KI_LCONTROL; case SDLK_RCTRL: return Rocket::Core::Input::KI_RCONTROL; case SDLK_LALT: return Rocket::Core::Input::KI_LMETA; case SDLK_RALT: return Rocket::Core::Input::KI_RMETA; case SDLK_CAPSLOCK: return Rocket::Core::Input::KI_CAPITAL; case SDLK_PAUSE: return Rocket::Core::Input::KI_PAUSE; case SDLK_RETURN: return Rocket::Core::Input::KI_RETURN; case SDLK_LSHIFT: return Rocket::Core::Input::KI_LSHIFT; case SDLK_RSHIFT: return Rocket::Core::Input::KI_RSHIFT; case SDLK_SPACE: return Rocket::Core::Input::KI_SPACE; case SDLK_TAB: return Rocket::Core::Input::KI_TAB; }; return Rocket::Core::Input::KI_UNKNOWN; } bool key_event_handler(const SDL_Event& evt) { if (!os::events::isWindowEvent(evt, os::getSDLMainWindow())) { // Ignore events that are not relevant to the main window return false; } if (input_context == nullptr) { // No active input context return false; } // Check for special key combinations auto sdl_mods = SDL_GetModState(); auto shift_down = (sdl_mods & SDL_KMOD_SHIFT) != 0; if (evt.key.key == SDLK_ESCAPE && shift_down) { // Ignore this combination to still enable quitting return false; } if (evt.key.key == SDLK_PRINTSCREEN) { // Ignore print screen buttons to still allow the default engine actions return false; } if (evt.key.key == SDLK_LSHIFT || evt.key.key == SDLK_RSHIFT || evt.key.key == SDLK_LCTRL || evt.key.key == SDLK_RCTRL||evt.key.key == SDLK_LALT||evt.key.key == SDLK_RALT) { // Ignore simple key events of modifiers since they are not meaningful to libRocket return false; } if (evt.type == SDL_EVENT_KEY_UP) { auto ctrl_down = (sdl_mods & SDL_KMOD_CTRL) != 0; if (evt.key.key == SDLK_R && ctrl_down && shift_down) { // Ctrl+Shift+R reloads everything scpui::reloadAllContexts(); return true; } else if (evt.key.key == SDLK_D && ctrl_down && shift_down) { // Ctrl+Shift+D toggles the debugger if (!debugger_initialized) { // First we need to initialize the debugger. We use the current context for that, should work in // most cases if (!Rocket::Debugger::Initialise(input_context)) { Warning(LOCATION, "Failed to initialize debugger!"); return true; } debugger_initialized = true; } Rocket::Debugger::SetContext(input_context); // Toggle the debugger state Rocket::Debugger::SetVisible(!Rocket::Debugger::IsVisible()); return true; } } if (evt.type == SDL_EVENT_KEY_DOWN) { return input_context->ProcessKeyDown(translateKey(evt.key.key), get_modifier_state()); } else { return input_context->ProcessKeyUp(translateKey(evt.key.key), get_modifier_state()); } } bool text_input_handler(const SDL_Event& evt) { if (!os::events::isWindowEvent(evt, os::getSDLMainWindow())) { // Ignore events that are not relevant to the main window return false; } if (input_context == nullptr) { // No active input context return false; } // libRocket expects UCS-2 so we first need to convert to that // NOTE: not using SDL_iconv_utf8_ucs2() macro here due to Linux compatiblity issue #if SDL_BYTEORDER == SDL_BIG_ENDIAN const char *tocode = "UCS-2BE"; #else const char *tocode = "UCS-2LE"; #endif auto text = reinterpret_cast(SDL_iconv_string(tocode, "UTF-8", evt.text.text, SDL_strlen(evt.text.text)+1)); if ( !text ) { // encoding failed return false; } auto ucs2String = std::u16string(text); SDL_free(text); bool consumed = true; for (auto& c : ucs2String) { consumed = consumed && input_context->ProcessTextInput(c); } return consumed; } } // namespace namespace scpui { void initialize() { if (!Unicode_text_mode) { mprintf(("NOTE: libRocket is disabled since unicode text mode is not enabled!\n")); return; } rendering_interface.reset(new RocketRenderingInterface()); system_interface.reset(new RocketSystemInterface()); file_interface.reset(new RocketFileInterface()); rendering_interface->setRenderOffset(render_offset); // Setup our custom interfaces Rocket::Core::SetRenderInterface(rendering_interface.get()); Rocket::Core::SetSystemInterface(system_interface.get()); Rocket::Core::SetFileInterface(file_interface.get()); Rocket::Core::Initialise(); // Initialise the Rocket Controls library. Rocket::Controls::Initialise(); Rocket::Core::Factory::RegisterElementInstancer("ani", new ElementInstancerGeneric()) ->RemoveReference(); Rocket::Core::Factory::RegisterElementInstancer("scrollingText", new ElementInstancerGeneric()) ->RemoveReference(); XMLParser::RegisterNodeHandler("include", new IncludeNodeHandler())->RemoveReference(); // Register custom underline decorator with its own instancer Rocket::Core::DecoratorInstancer* underline_instancer = new scpui::decorators::UnderlineDecoratorInstancer(); Rocket::Core::Factory::RegisterDecoratorInstancer("underline", underline_instancer); // Register custom corner-borders decorator with its own instancer Rocket::Core::DecoratorInstancer* corner_borders_instancer = new scpui::decorators::BorderDecoratorInstancer(); Rocket::Core::Factory::RegisterDecoratorInstancer("corner-borders", corner_borders_instancer); // Decrease the reference counts (as it is managed by libRocket after registration) underline_instancer->RemoveReference(); corner_borders_instancer->RemoveReference(); // Setup the plugin a style sheet properties for the sound support Rocket::Core::RegisterPlugin(new SoundPlugin()); Rocket::Core::StyleSheetSpecification::RegisterParser("sound", new SoundPropertyParser()); for (auto& type : SoundPlugin::getEventTypes()) { auto prop_name = type + "-sound"; Rocket::Core::StyleSheetSpecification::RegisterProperty(prop_name.c_str(), "-1", false).AddParser("sound"); } // Initialise the Lua interface Rocket::Core::Lua::Interpreter::Initialise(Script_system.GetLuaSession(), std::unique_ptr(new RocketLuaSystemInterface())); Rocket::Controls::Lua::RegisterTypes(Rocket::Core::Lua::Interpreter::GetLuaState()); load_fonts(); // initialize events os::events::addEventListener(SDL_EVENT_MOUSE_MOTION, os::events::DEFAULT_LISTENER_WEIGHT - 10, mouse_motion_handler); os::events::addEventListener(SDL_EVENT_MOUSE_BUTTON_UP, os::events::DEFAULT_LISTENER_WEIGHT - 10, mouse_button_handler); os::events::addEventListener(SDL_EVENT_MOUSE_BUTTON_DOWN, os::events::DEFAULT_LISTENER_WEIGHT - 10, mouse_button_handler); os::events::addEventListener(SDL_EVENT_MOUSE_WHEEL, os::events::DEFAULT_LISTENER_WEIGHT - 10, mouse_wheel_handler); os::events::addEventListener(SDL_EVENT_KEY_UP, os::events::DEFAULT_LISTENER_WEIGHT - 10, key_event_handler); os::events::addEventListener(SDL_EVENT_KEY_DOWN, os::events::DEFAULT_LISTENER_WEIGHT - 10, key_event_handler); os::events::addEventListener(SDL_EVENT_TEXT_INPUT, os::events::DEFAULT_LISTENER_WEIGHT - 10, text_input_handler); rocket_initialized = true; } void setOffset(float x, float y) { rendering_interface->setRenderOffset({x, y}); } void shutdown_scripting() { if (!rocket_initialized) { return; } Rocket::Core::Lua::Interpreter::Shutdown(false); } void shutdown() { if (!rocket_initialized) { return; } if (input_context != nullptr) { // If this is still here then the user forgot to disable input mprintf(("Warning: The input context is still active! Disabling input...\n")); disableInput(); } // Shutdown Rocket. Rocket::Core::Shutdown(); rendering_interface.reset(); system_interface.reset(); file_interface.reset(); rocket_initialized = false; } void reloadContext(Rocket::Core::Context* context) { struct DocumentStatus { ElementDocument* original = nullptr; String file_name; bool visible = true; bool focus = false; }; // Save the current status of the documents in this context SCP_vector documents; for (auto i = 0; i < context->GetNumDocuments(); ++i) { ElementDocument* doc = context->GetDocument(i); if (doc->GetSourceURL() == "") { // In-memory documents can't be reloaded continue; } DocumentStatus status; status.original = doc; status.file_name = doc->GetSourceURL(); status.visible = doc->IsVisible(); status.focus = doc->GetFocusLeafNode() != nullptr; documents.push_back(std::move(status)); } Factory::ClearStyleSheetCache(); Factory::ClearTemplateCache(); // And readd them all for (auto& status : documents) { // Selectively unload the old documents. // We can't just unload all since that destroys the debugger context->UnloadDocument(status.original); auto doc = context->LoadDocument(status.file_name); if (status.visible) { doc->Show(status.focus ? ElementDocument::FOCUS : ElementDocument::NONE); } // We don't own this reference doc->RemoveReference(); } } void reloadAllContexts() { for (auto i = 0; i < Rocket::Core::GetNumContexts(); ++i) { reloadContext(Rocket::Core::GetContext(i)); } } Rocket::Core::Context* getContext() { return input_context; } void enableInput(Rocket::Core::Context* main_ctx) { Assertion(main_ctx != nullptr, "Invalid context pointer detected!"); if (input_context != nullptr) { // If we still have an active context, disable the input on the previous context disableInput(); } input_context = main_ctx; input_context->AddReference(); } void disableInput() { if (input_context == nullptr) { return; } input_context->RemoveReference(); input_context = nullptr; } } // namespace scpui