Files
fs2open.github.com/code/scpui/rocket_ui.cpp
T
BMagnu 88669e4518 Fix VR cursor issues and warn about crash hazard (#7868)
* Add warning for OpenXR toolkit

* Properly handle mouse position

* GCC 9 issue
2026-10-15 00:48:46 +00:00

758 lines
20 KiB
C++

//
//
// 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 <Rocket/Controls.h>
#include <Rocket/Core.h>
#include <Rocket/Debugger.h>
#include <Rocket/Controls/Lua/Controls.h>
#include <Rocket/Core/Lua/Interpreter.h>
#pragma pop_macro("Assert")
#include <codecvt>
#include <locale>
using namespace Rocket::Core;
namespace {
bool rocket_initialized = false;
std::unique_ptr<scpui::RocketRenderingInterface> rendering_interface;
std::unique_ptr<scpui::RocketSystemInterface> system_interface;
std::unique_ptr<scpui::RocketFileInterface> 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<SCP_string> 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<char16_t *>(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<elements::AnimationElement>())
->RemoveReference();
Rocket::Core::Factory::RegisterElementInstancer("scrollingText",
new ElementInstancerGeneric<elements::ScrollingTextElement>())
->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<Rocket::Core::Lua::LuaSystemInterface>(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<DocumentStatus> 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