From abfb319e9b19dc672b80ab978e0f37e91ae414ee Mon Sep 17 00:00:00 2001 From: v-rob Date: Wed, 24 Jan 2024 20:57:46 -0800 Subject: [PATCH] Create C++ backend UI code --- CMakeLists.txt | 11 + doc/compiling/README.md | 1 + src/CMakeLists.txt | 15 +- src/client/client.h | 1 + src/client/clientevent.h | 5 + src/client/game.cpp | 35 +- src/client/render/core.cpp | 3 +- src/client/render/core.h | 2 +- src/client/render/pipeline.h | 1 + src/client/render/plain.cpp | 25 ++ src/client/renderingengine.cpp | 4 +- src/client/renderingengine.h | 2 +- src/cmake_config.h.in | 1 + src/network/clientopcodes.cpp | 2 +- src/network/clientpackethandler.cpp | 11 + src/network/networkprotocol.cpp | 3 +- src/network/networkprotocol.h | 6 + src/network/serveropcodes.cpp | 2 +- src/script/lua_api/l_server.cpp | 14 + src/script/lua_api/l_server.h | 3 + src/server.cpp | 13 + src/server.h | 2 + src/ui/CMakeLists.txt | 9 + src/ui/box.cpp | 512 +++++++++++++++++++++++++++ src/ui/box.h | 102 ++++++ src/ui/elem.cpp | 105 ++++++ src/ui/elem.h | 79 +++++ src/ui/helpers.h | 515 ++++++++++++++++++++++++++++ src/ui/manager.cpp | 123 +++++++ src/ui/manager.h | 85 +++++ src/ui/static_elems.cpp | 33 ++ src/ui/static_elems.h | 34 ++ src/ui/style.cpp | 185 ++++++++++ src/ui/style.h | 126 +++++++ src/ui/window.cpp | 310 +++++++++++++++++ src/ui/window.h | 96 ++++++ 36 files changed, 2466 insertions(+), 10 deletions(-) create mode 100644 src/ui/CMakeLists.txt create mode 100644 src/ui/box.cpp create mode 100644 src/ui/box.h create mode 100644 src/ui/elem.cpp create mode 100644 src/ui/elem.h create mode 100644 src/ui/helpers.h create mode 100644 src/ui/manager.cpp create mode 100644 src/ui/manager.h create mode 100644 src/ui/static_elems.cpp create mode 100644 src/ui/static_elems.h create mode 100644 src/ui/style.cpp create mode 100644 src/ui/style.h create mode 100644 src/ui/window.cpp create mode 100644 src/ui/window.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 70a027f57..7f0afb3db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ set(BUILD_SERVER FALSE CACHE BOOL "Build server") set(BUILD_UNITTESTS TRUE CACHE BOOL "Build unittests") set(BUILD_BENCHMARKS FALSE CACHE BOOL "Build benchmarks") set(BUILD_DOCUMENTATION TRUE CACHE BOOL "Build documentation") +set(BUILD_UI FALSE CACHE BOOL "Build experimental UI API; requires BUILD_CLIENT and USE_SDL2") set(DEFAULT_ENABLE_LTO TRUE) # by default don't enable on Debug builds to get faster builds @@ -77,6 +78,7 @@ message(STATUS "BUILD_SERVER: " ${BUILD_SERVER}) message(STATUS "BUILD_UNITTESTS: " ${BUILD_UNITTESTS}) message(STATUS "BUILD_BENCHMARKS: " ${BUILD_BENCHMARKS}) message(STATUS "BUILD_DOCUMENTATION: " ${BUILD_DOCUMENTATION}) +message(STATUS "BUILD_UI: " ${BUILD_UI}) message(STATUS "RUN_IN_PLACE: " ${RUN_IN_PLACE}) set(WARN_ALL TRUE CACHE BOOL "Enable -Wall for Release build") @@ -398,3 +400,12 @@ if(BUILD_WITH_TRACY) FetchContent_MakeAvailable(tracy) message(STATUS "Fetching Tracy - done") endif() + +if(BUILD_UI) + if(NOT BUILD_CLIENT) + message(FATAL_ERROR "BUILD_UI requires BUILD_CLIENT") + endif() + if(NOT USE_SDL2) + message(FATAL_ERROR "BUILD_UI requires USE_SDL2") + endif() +endif() diff --git a/doc/compiling/README.md b/doc/compiling/README.md index 9ce8a800e..b3088ef66 100644 --- a/doc/compiling/README.md +++ b/doc/compiling/README.md @@ -14,6 +14,7 @@ General options and their default values: BUILD_UNITTESTS=TRUE - Build unittest sources BUILD_BENCHMARKS=FALSE - Build benchmark sources BUILD_DOCUMENTATION=TRUE - Build doxygen documentation + BUILD_UI=TRUE - Build experimental UI API; requires BUILD_CLIENT and USE_SDL2 CMAKE_BUILD_TYPE=Release - Type of build (Release vs. Debug) Release - Release build Debug - Debug build diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1231f49ba..07f2832ad 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -529,6 +529,10 @@ if (BUILD_CLIENT) add_subdirectory(client) add_subdirectory(gui) add_subdirectory(irrlicht_changes) + + if(BUILD_UI) + add_subdirectory(ui) + endif() endif(BUILD_CLIENT) list(APPEND client_SRCS @@ -542,10 +546,12 @@ list(APPEND client_SRCS if(BUILD_UNITTESTS) list(APPEND client_SRCS ${UNITTEST_CLIENT_SRCS}) endif() - if(BUILD_BENCHMARKS) list(APPEND client_SRCS ${BENCHMARK_CLIENT_SRCS}) endif() +if(BUILD_UI) + set(client_SRCS ${client_SRCS} ${ui_SRCS}) +endif() # Server sources # (nothing here because a client always comes with a server) @@ -587,6 +593,10 @@ if(BUILD_CLIENT) ${FREETYPE_INCLUDE_DIRS} ${SOUND_INCLUDE_DIRS} ) + + if(BUILD_UI) + include_directories(SYSTEM ${SDL2_INCLUDE_DIRS}) + endif() endif() if(USE_CURL) @@ -756,6 +766,9 @@ if(BUILD_CLIENT) if(BUILD_WITH_TRACY) target_link_libraries(${PROJECT_NAME} Tracy::TracyClient) endif() + if(BUILD_UI) + target_link_libraries(${PROJECT_NAME} ${SDL2_LIBRARIES}) + endif() if(PRECOMPILE_HEADERS) target_precompile_headers(${PROJECT_NAME} PRIVATE ${PRECOMPILED_HEADERS_LIST}) diff --git a/src/client/client.h b/src/client/client.h index 12625f24e..8ce4149b3 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -192,6 +192,7 @@ public: void handleCommand_InventoryFormSpec(NetworkPacket* pkt); void handleCommand_DetachedInventory(NetworkPacket* pkt); void handleCommand_ShowFormSpec(NetworkPacket* pkt); + void handleCommand_UiMessage(NetworkPacket* pkt); void handleCommand_SpawnParticle(NetworkPacket* pkt); void handleCommand_AddParticleSpawner(NetworkPacket* pkt); void handleCommand_DeleteParticleSpawner(NetworkPacket* pkt); diff --git a/src/client/clientevent.h b/src/client/clientevent.h index 3d627c934..e4c5fe973 100644 --- a/src/client/clientevent.h +++ b/src/client/clientevent.h @@ -24,6 +24,7 @@ enum ClientEventType : u8 CE_SHOW_FORMSPEC, CE_SHOW_CSM_FORMSPEC, CE_SHOW_PAUSE_MENU_FORMSPEC, + CE_UI_MESSAGE, CE_SPAWN_PARTICLE, CE_ADD_PARTICLESPAWNER, CE_DELETE_PARTICLESPAWNER, @@ -90,6 +91,10 @@ struct ClientEvent std::string *formspec; std::string *formname; } show_formspec; + struct + { + std::string *data; + } ui_message; ParticleParameters *spawn_particle; struct { diff --git a/src/client/game.cpp b/src/client/game.cpp index f9a52c43f..cfcc36e98 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -64,6 +64,9 @@ #if USE_SOUND #include "client/sound/sound_openal.h" #endif +#if BUILD_UI +#include "ui/manager.h" +#endif #include @@ -727,6 +730,7 @@ private: void handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam); void handleClientEvent_ShowCSMFormSpec(ClientEvent *event, CameraOrientation *cam); void handleClientEvent_ShowPauseMenuFormSpec(ClientEvent *event, CameraOrientation *cam); + void handleClientEvent_UiMessage(ClientEvent *event, CameraOrientation *cam); void handleClientEvent_HandleParticleEvent(ClientEvent *event, CameraOrientation *cam); void handleClientEvent_HudAdd(ClientEvent *event, CameraOrientation *cam); @@ -777,6 +781,9 @@ private: std::unique_ptr m_game_ui; irr_ptr gui_chat_console; +#if BUILD_UI + irr_ptr gui_manager_elem; +#endif MapDrawControl *draw_control = nullptr; Camera *camera = nullptr; irr_ptr clouds; @@ -1117,6 +1124,10 @@ void Game::run() void Game::shutdown() { // Delete text and menus first +#if BUILD_UI + ui::g_manager.reset(); +#endif + m_game_ui->clearText(); m_game_formspec.reset(); while (g_menumgr.menuCount() > 0) { @@ -1129,6 +1140,9 @@ void Game::shutdown() clouds.reset(); gui_chat_console.reset(); +#if BUILD_UI + gui_manager_elem.reset(); +#endif sky.reset(); @@ -1426,6 +1440,10 @@ bool Game::createClient(const GameStartData &start_data) if (mapper && client->modsLoaded()) client->getScript()->on_minimap_ready(mapper); +#if BUILD_UI + ui::g_manager.setClient(client); +#endif + return true; } @@ -1454,6 +1472,12 @@ bool Game::initGui() gui_chat_console = make_irr(guienv, guienv->getRootGUIElement(), -1, chat_backend, client, &g_menumgr); +#if BUILD_UI + // Thingy to draw UI manager after chat but before formspecs. + gui_manager_elem = make_irr( + guienv, guienv->getRootGUIElement(), -1); +#endif + if (shouldShowTouchControls()) g_touchcontrols = new TouchControls(device, texture_src); @@ -2653,6 +2677,7 @@ const ClientEventHandler Game::clientEventHandler[CLIENTEVENT_MAX] = { {&Game::handleClientEvent_ShowFormSpec}, {&Game::handleClientEvent_ShowCSMFormSpec}, {&Game::handleClientEvent_ShowPauseMenuFormSpec}, + {&Game::handleClientEvent_UiMessage}, {&Game::handleClientEvent_HandleParticleEvent}, {&Game::handleClientEvent_HandleParticleEvent}, {&Game::handleClientEvent_HandleParticleEvent}, @@ -2739,6 +2764,14 @@ void Game::handleClientEvent_ShowPauseMenuFormSpec(ClientEvent *event, CameraOri delete event->show_formspec.formname; } +void Game::handleClientEvent_UiMessage(ClientEvent *event, CameraOrientation *cam) +{ +#if BUILD_UI + ui::g_manager.receiveMessage(*event->ui_message.data); +#endif + delete event->ui_message.data; +} + void Game::handleClientEvent_HandleParticleEvent(ClientEvent *event, CameraOrientation *cam) { @@ -4151,7 +4184,7 @@ void Game::drawScene(ProfilerGraph *graph, RunStats *stats) draw_crosshair = false; this->m_rendering_engine->draw_scene(sky_color, this->m_game_ui->m_flags.show_hud, - draw_wield_tool, draw_crosshair); + this->m_game_ui->m_flags.show_chat, draw_wield_tool, draw_crosshair); /* Profiler graph diff --git a/src/client/render/core.cpp b/src/client/render/core.cpp index ef780269a..e380eebe9 100644 --- a/src/client/render/core.cpp +++ b/src/client/render/core.cpp @@ -21,7 +21,7 @@ RenderingCore::~RenderingCore() delete shadow_renderer; } -void RenderingCore::draw(video::SColor _skycolor, bool _show_hud, +void RenderingCore::draw(video::SColor _skycolor, bool _show_hud, bool _show_chat, bool _draw_wield_tool, bool _draw_crosshair) { v2u32 screensize = device->getVideoDriver()->getScreenSize(); @@ -31,6 +31,7 @@ void RenderingCore::draw(video::SColor _skycolor, bool _show_hud, context.draw_crosshair = _draw_crosshair; context.draw_wield_tool = _draw_wield_tool; context.show_hud = _show_hud; + context.show_chat = _show_chat; pipeline->reset(context); pipeline->run(context); diff --git a/src/client/render/core.h b/src/client/render/core.h index f9d92bee5..515c0864e 100644 --- a/src/client/render/core.h +++ b/src/client/render/core.h @@ -45,7 +45,7 @@ public: RenderingCore &operator=(const RenderingCore &) = delete; RenderingCore &operator=(RenderingCore &&) = delete; - void draw(video::SColor _skycolor, bool _show_hud, + void draw(video::SColor _skycolor, bool _show_hud, bool _show_chat, bool _draw_wield_tool, bool _draw_crosshair); v2u32 getVirtualSize() const; diff --git a/src/client/render/pipeline.h b/src/client/render/pipeline.h index d91d523b9..b09ffdb16 100644 --- a/src/client/render/pipeline.h +++ b/src/client/render/pipeline.h @@ -38,6 +38,7 @@ struct PipelineContext v2u32 target_size; bool show_hud {true}; + bool show_chat {true}; bool draw_wield_tool {true}; bool draw_crosshair {true}; }; diff --git a/src/client/render/plain.cpp b/src/client/render/plain.cpp index 0f94e3ef0..a26fe218d 100644 --- a/src/client/render/plain.cpp +++ b/src/client/render/plain.cpp @@ -13,6 +13,10 @@ #include "client/shadows/dynamicshadowsrender.h" #include +#if BUILD_UI +#include "ui/manager.h" +#endif + /// Draw3D pipeline step void Draw3D::run(PipelineContext &context) { @@ -29,6 +33,11 @@ void Draw3D::run(PipelineContext &context) void DrawWield::run(PipelineContext &context) { +#if BUILD_UI + ui::g_manager.preDraw(); + ui::g_manager.drawType(ui::WindowType::FILTER); +#endif + if (m_target) m_target->activate(context); @@ -46,10 +55,26 @@ void DrawHUD::run(PipelineContext &context) if (context.draw_crosshair) context.hud->drawCrosshair(); + } +#if BUILD_UI + ui::g_manager.drawType(ui::WindowType::MASK); +#endif + + if (context.show_hud) { context.hud->drawLuaElements(context.client->getCamera()->getOffset()); +#if BUILD_UI + ui::g_manager.drawType(ui::WindowType::HUD); +#endif + context.client->getCamera()->drawNametags(); } + +#if BUILD_UI + if (context.show_chat) + ui::g_manager.drawType(ui::WindowType::CHAT); +#endif + context.device->getGUIEnvironment()->drawAll(); } diff --git a/src/client/renderingengine.cpp b/src/client/renderingengine.cpp index 3b3d114ea..1358b9fa2 100644 --- a/src/client/renderingengine.cpp +++ b/src/client/renderingengine.cpp @@ -401,10 +401,10 @@ void RenderingEngine::finalize() core.reset(); } -void RenderingEngine::draw_scene(video::SColor skycolor, bool show_hud, +void RenderingEngine::draw_scene(video::SColor skycolor, bool show_hud, bool show_chat, bool draw_wield_tool, bool draw_crosshair) { - core->draw(skycolor, show_hud, draw_wield_tool, draw_crosshair); + core->draw(skycolor, show_hud, show_chat, draw_wield_tool, draw_crosshair); } const VideoDriverInfo &RenderingEngine::getVideoDriverInfo(irr::video::E_DRIVER_TYPE type) diff --git a/src/client/renderingengine.h b/src/client/renderingengine.h index 34918ec7a..91165fde8 100644 --- a/src/client/renderingengine.h +++ b/src/client/renderingengine.h @@ -129,7 +129,7 @@ public: gui::IGUIEnvironment *guienv, ITextureSource *tsrc, float dtime = 0, int percent = 0, float *indef_pos = nullptr); - void draw_scene(video::SColor skycolor, bool show_hud, + void draw_scene(video::SColor skycolor, bool show_hud, bool show_chat, bool draw_wield_tool, bool draw_crosshair); void initialize(Client *client, Hud *hud); diff --git a/src/cmake_config.h.in b/src/cmake_config.h.in index 2ec91dfd1..492166ae2 100644 --- a/src/cmake_config.h.in +++ b/src/cmake_config.h.in @@ -41,5 +41,6 @@ #cmakedefine01 CURSES_HAVE_NCURSESW_CURSES_H #cmakedefine01 BUILD_UNITTESTS #cmakedefine01 BUILD_BENCHMARKS +#cmakedefine01 BUILD_UI #cmakedefine01 USE_SDL2 #cmakedefine01 BUILD_WITH_TRACY diff --git a/src/network/clientopcodes.cpp b/src/network/clientopcodes.cpp index 9a9cb5968..a02dc5634 100644 --- a/src/network/clientopcodes.cpp +++ b/src/network/clientopcodes.cpp @@ -105,7 +105,7 @@ const ToClientCommandHandler toClientCommandTable[TOCLIENT_NUM_MSG_TYPES] = { "TOCLIENT_SET_MOON", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_HudSetMoon }, // 0x5b { "TOCLIENT_SET_STARS", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_HudSetStars }, // 0x5c { "TOCLIENT_MOVE_PLAYER_REL", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_MovePlayerRel }, // 0x5d, - null_command_handler, + { "TOCLIENT_UI_MESSAGE", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_UiMessage }, // 0x5e, null_command_handler, { "TOCLIENT_SRP_BYTES_S_B", TOCLIENT_STATE_NOT_CONNECTED, &Client::handleCommand_SrpBytesSandB }, // 0x60 { "TOCLIENT_FORMSPEC_PREPEND", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_FormspecPrepend }, // 0x61, diff --git a/src/network/clientpackethandler.cpp b/src/network/clientpackethandler.cpp index bb1930d96..1b7db4989 100644 --- a/src/network/clientpackethandler.cpp +++ b/src/network/clientpackethandler.cpp @@ -953,6 +953,17 @@ void Client::handleCommand_ShowFormSpec(NetworkPacket* pkt) m_client_event_queue.push(event); } +void Client::handleCommand_UiMessage(NetworkPacket* pkt) +{ + std::string *data = new std::string(pkt->getString(0), pkt->getSize()); + + ClientEvent *event = new ClientEvent(); + event->type = CE_UI_MESSAGE; + event->ui_message.data = data; + + m_client_event_queue.push(event); +} + void Client::handleCommand_SpawnParticle(NetworkPacket* pkt) { std::string datastring(pkt->getString(0), pkt->getSize()); diff --git a/src/network/networkprotocol.cpp b/src/network/networkprotocol.cpp index a88b5b091..610fe5c4f 100644 --- a/src/network/networkprotocol.cpp +++ b/src/network/networkprotocol.cpp @@ -59,8 +59,9 @@ Rename TOSERVER_RESPAWN to TOSERVER_RESPAWN_LEGACY Support float animation frame numbers in TOCLIENT_LOCAL_PLAYER_ANIMATIONS [scheduled bump for 5.10.0] - PROTOCOL VERSION 47 + PROTOCOL VERSION 47: Add particle blend mode "clip" + Add TOCLIENT_UI_MESSAGE [scheduled bump for 5.11.0] PROTOCOL VERSION 48 Add compression to some existing packets diff --git a/src/network/networkprotocol.h b/src/network/networkprotocol.h index 5ce3f4221..4ae563e7f 100644 --- a/src/network/networkprotocol.h +++ b/src/network/networkprotocol.h @@ -648,6 +648,12 @@ enum ToClientCommand : u16 v3f added_pos */ + TOCLIENT_UI_MESSAGE = 0x5e, + /* + Complicated variable-length structure with many optional fields and + length-prefixed data for future compatibility. + */ + TOCLIENT_SRP_BYTES_S_B = 0x60, /* Belonging to AUTH_MECHANISM_SRP. diff --git a/src/network/serveropcodes.cpp b/src/network/serveropcodes.cpp index b50e13082..96ed4d60b 100644 --- a/src/network/serveropcodes.cpp +++ b/src/network/serveropcodes.cpp @@ -205,7 +205,7 @@ const ClientCommandFactory clientCommandFactoryTable[TOCLIENT_NUM_MSG_TYPES] = { "TOCLIENT_SET_MOON", 0, true }, // 0x5b { "TOCLIENT_SET_STARS", 0, true }, // 0x5c { "TOCLIENT_MOVE_PLAYER_REL", 0, true }, // 0x5d - null_command_factory, // 0x5e + { "TOCLIENT_UI_MESSAGE", 0, true }, // 0x5e null_command_factory, // 0x5f { "TOCLIENT_SRP_BYTES_S_B", 0, true }, // 0x60 { "TOCLIENT_FORMSPEC_PREPEND", 0, true }, // 0x61 diff --git a/src/script/lua_api/l_server.cpp b/src/script/lua_api/l_server.cpp index 698b2dba6..779ae1d26 100644 --- a/src/script/lua_api/l_server.cpp +++ b/src/script/lua_api/l_server.cpp @@ -418,6 +418,19 @@ int ModApiServer::l_show_formspec(lua_State *L) return 1; } +// send_ui_message(player, data) +int ModApiServer::l_send_ui_message(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + + size_t len; + const char *player = luaL_checkstring(L, 1); + const char *data = luaL_checklstring(L, 2, &len); + + getServer(L)->sendUiMessage(player, data, len); + return 0; +} + // get_current_modname() int ModApiServer::l_get_current_modname(lua_State *L) { @@ -730,6 +743,7 @@ void ModApiServer::Initialize(lua_State *L, int top) API_FCT(chat_send_all); API_FCT(chat_send_player); API_FCT(show_formspec); + API_FCT(send_ui_message); API_FCT(sound_play); API_FCT(sound_stop); API_FCT(sound_fade); diff --git a/src/script/lua_api/l_server.h b/src/script/lua_api/l_server.h index 6de7de363..6c3267045 100644 --- a/src/script/lua_api/l_server.h +++ b/src/script/lua_api/l_server.h @@ -55,6 +55,9 @@ private: // show_formspec(playername,formname,formspec) static int l_show_formspec(lua_State *L); + // send_ui_message(player, data) + static int l_send_ui_message(lua_State *L); + // sound_play(spec, parameters) static int l_sound_play(lua_State *L); diff --git a/src/server.cpp b/src/server.cpp index 89bba75fb..4b72c7d36 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -3401,6 +3401,19 @@ bool Server::showFormspec(const char *playername, const std::string &formspec, return true; } +void Server::sendUiMessage(const char *name, const char *data, size_t len) +{ + RemotePlayer *player = m_env->getPlayer(name); + if (!player) { + return; + } + + NetworkPacket pkt(TOCLIENT_UI_MESSAGE, 0, player->getPeerId()); + pkt.putRawString(data, len); + + Send(&pkt); +} + u32 Server::hudAdd(RemotePlayer *player, HudElement *form) { if (!player) diff --git a/src/server.h b/src/server.h index 177af002f..5582eb9b6 100644 --- a/src/server.h +++ b/src/server.h @@ -357,6 +357,8 @@ public: void addShutdownError(const ModError &e); bool showFormspec(const char *name, const std::string &formspec, const std::string &formname); + void sendUiMessage(const char *name, const char *data, size_t len); + Map & getMap() { return m_env->getMap(); } ServerEnvironment & getEnv() { return *m_env; } v3f findSpawnPos(); diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt new file mode 100644 index 000000000..e18b52ae7 --- /dev/null +++ b/src/ui/CMakeLists.txt @@ -0,0 +1,9 @@ +set(ui_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/box.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/manager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/static_elems.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/style.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/window.cpp + PARENT_SCOPE +) diff --git a/src/ui/box.cpp b/src/ui/box.cpp new file mode 100644 index 000000000..345517be9 --- /dev/null +++ b/src/ui/box.cpp @@ -0,0 +1,512 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2023 v-rob, Vincent Robinson + +#include "ui/box.h" + +#include "debug.h" +#include "log.h" +#include "porting.h" +#include "ui/elem.h" +#include "ui/manager.h" +#include "ui/window.h" +#include "util/serialize.h" + +namespace ui +{ + Window &Box::getWindow() + { + return m_elem.getWindow(); + } + + const Window &Box::getWindow() const + { + return m_elem.getWindow(); + } + + void Box::reset() + { + m_content.clear(); + m_style.reset(); + + for (State i = 0; i < m_style_refs.size(); i++) { + m_style_refs[i] = NO_STYLE; + } + } + + void Box::read(std::istream &full_is) + { + auto is = newIs(readStr16(full_is)); + u32 style_mask = readU32(is); + + for (State i = 0; i < m_style_refs.size(); i++) { + // If we have a style for this state in the mask, add it to the + // list of styles. + if (!testShift(style_mask)) { + continue; + } + + u32 index = readU32(is); + if (getWindow().getStyleStr(index) != nullptr) { + m_style_refs[i] = index; + } else { + errorstream << "Style " << index << " does not exist" << std::endl; + } + } + } + + void Box::restyle() + { + // First, clear our current style and compute what state we're in. + m_style.reset(); + State state = STATE_NONE; + + // Loop over each style state from lowest precedence to highest since + // they should be applied in that order. + for (State i = 0; i < m_style_refs.size(); i++) { + // If this state we're looking at is a subset of the current state, + // then it's a match for styling. + if ((state & i) != i) { + continue; + } + + u32 index = m_style_refs[i]; + + // If the index for this state has an associated style string, + // apply it to our current style. + if (index != NO_STYLE) { + auto is = newIs(*getWindow().getStyleStr(index)); + m_style.read(is); + } + } + + // Since our box has been restyled, the previously computed layout + // information is no longer valid. + m_min_layout = SizeF(); + m_min_content = SizeF(); + + m_display_rect = RectF(); + m_icon_rect = RectF(); + m_content_rect = RectF(); + + m_clip_rect = RectF(); + + // Finally, make sure to restyle our content. + for (Box *box : m_content) { + box->restyle(); + } + } + + void Box::resize() + { + for (Box *box : m_content) { + box->resize(); + } + + switch (m_style.layout.type) { + case LayoutType::PLACE: + resizePlace(); + break; + } + + resizeBox(); + } + + void Box::relayout(RectF layout_rect, RectF layout_clip) + { + relayoutBox(layout_rect, layout_clip); + + switch (m_style.layout.type) { + case LayoutType::PLACE: + relayoutPlace(); + break; + } + } + + void Box::draw() + { + if (m_style.display != DisplayMode::HIDDEN) { + drawBox(); + drawIcon(); + } + + for (Box *box : m_content) { + box->draw(); + } + } + + RectF Box::getLayerSource(const Layer &layer) + { + RectF src = layer.source; + + // If we have animations, we need to adjust the source rect by the + // frame offset in accordance with the current frame. + if (layer.num_frames > 1) { + float frame_height = src.H() / layer.num_frames; + src.B = src.T + frame_height; + + float frame_offset = frame_height * + ((porting::getTimeMs() / layer.frame_time) % layer.num_frames); + src.T += frame_offset; + src.B += frame_offset; + } + + return src; + } + + SizeF Box::getLayerSize(const Layer &layer) + { + return getLayerSource(layer).size() * getTextureSize(layer.image); + } + + DispF Box::getMiddleEdges() + { + // Scale the middle rect by the scaling factor and de-normalize it into + // actual pixels based on the image source rect. + return m_style.box_middle * DispF(getLayerSize(m_style.box)) * m_style.box.scale; + } + + void Box::resizeBox() + { + // If the box is set to clip its contents in either dimension, we can + // set the minimum content size to zero for that coordinate. + if (m_style.layout.clip == DirFlags::X || m_style.layout.clip == DirFlags::BOTH) { + m_min_content.W = 0.0f; + } + if (m_style.layout.clip == DirFlags::Y || m_style.layout.clip == DirFlags::BOTH) { + m_min_content.H = 0.0f; + } + + // We need to factor the icon into the minimum size of the box. The + // minimum size of the padding rect is either the size of the contents + // or the scaled icon, depending on which is larger. If the scale is + // zero, then the icon doesn't add anything to the minimum size. + SizeF icon_size = getLayerSize(m_style.icon) * m_style.icon.scale; + SizeF padding_size = m_min_content.max(icon_size); + + // If the icon should not overlap the content, then we must take into + // account the extra space required for this, including the gutter. + if (!m_style.icon_overlap && m_style.icon.image != nullptr) { + switch (m_style.icon_place) { + case IconPlace::CENTER: + break; + case IconPlace::LEFT: + case IconPlace::RIGHT: + padding_size.W = m_min_content.W + icon_size.W + m_style.icon_gutter; + break; + case IconPlace::TOP: + case IconPlace::BOTTOM: + padding_size.H = m_min_content.H + icon_size.H + m_style.icon_gutter; + break; + } + + padding_size = padding_size.clip(); + } + + // Now that we have a minimum size for the padding rect, we can + // calculate the display rect size by adjusting for the padding and + // middle rect edges. We also clamp the size of the display rect to be + // at least as large as the user-specified minimum size. + SizeF display_size = (padding_size + getMiddleEdges().extents() + + m_style.sizing.padding.extents()).max(m_style.sizing.size); + + // The final minimum size is the display size adjusted for the margin. + m_min_layout = (display_size + m_style.sizing.margin.extents()).clip(); + } + + void Box::relayoutBox(RectF layout_rect, RectF layout_clip) + { + // The display rect is created by insetting the layout rect by the + // margin. The padding rect is inset from that by the middle rect edges + // and the padding. We must make sure these do not have negative sizes. + m_display_rect = layout_rect.insetBy(m_style.sizing.margin).clip(); + RectF padding_rect = m_display_rect.insetBy( + getMiddleEdges() + m_style.sizing.padding).clip(); + + // The icon is aligned and scaled in a particular area of the box. + // First, get the basic size of the icon rect. + SizeF icon_size = getLayerSize(m_style.icon); + + // Then, modify it based on the scale that we should use. A scale of + // zero means the image should take up as much room as possible while + // still preserving the aspect ratio of the image. + if (m_style.icon.scale == 0.0f) { + SizeF max_icon = padding_rect.size(); + + // If the icon should not overlap the content, then we need to + // adjust the area in which we compute the maximum scale by + // subtracting the content and gutter from the padding rect size. + if (!m_style.icon_overlap && m_style.icon.image != nullptr) { + switch (m_style.icon_place) { + case IconPlace::CENTER: + break; + case IconPlace::LEFT: + case IconPlace::RIGHT: + max_icon.W -= m_min_content.W + m_style.icon_gutter; + break; + case IconPlace::TOP: + case IconPlace::BOTTOM: + max_icon.H -= m_min_content.H + m_style.icon_gutter; + break; + } + + max_icon = max_icon.clip(); + } + + // Choose the scale factor based on the space we have for the icon, + // choosing the smaller of the two possible image size ratios. + icon_size *= std::min(max_icon.W / icon_size.W, max_icon.H / icon_size.H); + } else { + icon_size *= m_style.icon.scale; + } + + // Now that we have the size of the icon, we can compute the icon rect + // based on the desired placement of the icon. + PosF icon_start = padding_rect.TopLeft; + PosF icon_center = icon_start + (padding_rect.size() - icon_size) / 2.0f; + PosF icon_end = icon_start + (padding_rect.size() - icon_size); + + switch (m_style.icon_place) { + case IconPlace::CENTER: + m_icon_rect = RectF(icon_center, icon_size); + break; + case IconPlace::LEFT: + m_icon_rect = RectF(PosF(icon_start.X, icon_center.Y), icon_size); + break; + case IconPlace::TOP: + m_icon_rect = RectF(PosF(icon_center.X, icon_start.Y), icon_size); + break; + case IconPlace::RIGHT: + m_icon_rect = RectF(PosF(icon_end.X, icon_center.Y), icon_size); + break; + case IconPlace::BOTTOM: + m_icon_rect = RectF(PosF(icon_center.X, icon_end.Y), icon_size); + break; + } + + // If the overlap property is set or the icon is centered, the content + // rect is identical to the padding rect. Otherwise, the content rect + // needs to be adjusted to account for the icon and gutter. + m_content_rect = padding_rect; + + if (!m_style.icon_overlap && m_style.icon.image != nullptr) { + switch (m_style.icon_place) { + case IconPlace::CENTER: + break; + case IconPlace::LEFT: + m_content_rect.L += icon_size.W + m_style.icon_gutter; + break; + case IconPlace::TOP: + m_content_rect.T += icon_size.H + m_style.icon_gutter; + break; + case IconPlace::RIGHT: + m_content_rect.R -= icon_size.W + m_style.icon_gutter; + break; + case IconPlace::BOTTOM: + m_content_rect.B -= icon_size.H + m_style.icon_gutter; + break; + } + + m_content_rect = m_content_rect.clip(); + } + + // We set our clipping rect based on the display mode. + switch (m_style.display) { + case DisplayMode::VISIBLE: + case DisplayMode::HIDDEN: + // If the box is visible or hidden, then we clip the box and its + // content as normal against the drawing and layout clip rects. + m_clip_rect = m_display_rect.intersectWith(layout_clip); + break; + case DisplayMode::OVERFLOW: + // If the box allows overflow, then clip to the drawing rect, since + // we never want to expand outside our own visible boundaries, but + // we don't clip to the layout clip rect. + m_clip_rect = m_display_rect; + break; + case DisplayMode::CLIPPED: + // If the box and its content should be entirely removed, then we + // clip everything entirely. + m_clip_rect = RectF(); + break; + } + } + + void Box::resizePlace() + { + for (Box *box : m_content) { + // Calculate the size of the box according to the span and scale + // factor. If the scale is zero, we don't know how big the span + // will end up being, so the span size goes to zero. + SizeF span_size = box->m_style.sizing.span * m_style.layout.scale; + + // Ensure that the computed minimum size for our content is at + // least as large as the minimum size of the box and its span size. + m_min_content = m_min_content.max(box->m_min_layout).max(span_size); + } + } + + void Box::relayoutPlace() + { + for (Box *box : m_content) { + const Sizing &sizing = box->m_style.sizing; + + // Compute the scale factor. If the scale is zero, then we use the + // size of the parent box to achieve normalized coordinates. + SizeF scale = m_style.layout.scale == 0.0f ? + m_content_rect.size() : SizeF(m_style.layout.scale); + + // Calculate the position and size of the box relative to the + // origin, taking into account the scale factor and anchor. Also + // make sure the size doesn't go below the minimum size. + SizeF size = (sizing.span * scale).max(box->m_min_layout); + SizeF pos = (sizing.pos * scale) - (sizing.anchor * size); + + // The layout rect of the box is made by shifting the above rect by + // the top left of the content rect. + RectF layout_rect = RectF(m_content_rect.TopLeft + pos, size); + box->relayout(layout_rect, m_clip_rect); + } + } + + void Box::drawBox() + { + // First, fill the display rectangle with the fill color. + getWindow().drawRect(m_display_rect, m_clip_rect, m_style.box.fill); + + // If there's no image, then we don't need to do a bunch of + // calculations in order to draw nothing. + if (m_style.box.image == nullptr) { + return; + } + + // For the image, first get the source rect adjusted for animations. + RectF src = getLayerSource(m_style.box); + + // We need to make sure the the middle rect is relative to the source + // rect rather than the entire image, so scale the edges appropriately. + DispF middle_src = m_style.box_middle * DispF(src.size()); + DispF middle_dst = getMiddleEdges(); + + // If the source rect for this image is flipped, we need to flip the + // sign of our middle rect as well to get the right adjustments. + if (src.W() < 0.0f) { + middle_src.L = -middle_src.L; + middle_src.R = -middle_src.R; + } + if (src.H() < 0.0f) { + middle_src.T = -middle_src.T; + middle_src.B = -middle_src.B; + } + + for (int slice_y = 0; slice_y < 3; slice_y++) { + for (int slice_x = 0; slice_x < 3; slice_x++) { + // Compute each slice of the nine-slice image. If the middle + // rect equals the whole source rect, the middle slice will + // occupy the entire display rectangle. + RectF slice_src = src; + RectF slice_dst = m_display_rect; + + switch (slice_x) { + case 0: + slice_dst.R = slice_dst.L + middle_dst.L; + slice_src.R = slice_src.L + middle_src.L; + break; + + case 1: + slice_dst.L += middle_dst.L; + slice_dst.R -= middle_dst.R; + slice_src.L += middle_src.L; + slice_src.R -= middle_src.R; + break; + + case 2: + slice_dst.L = slice_dst.R - middle_dst.R; + slice_src.L = slice_src.R - middle_src.R; + break; + } + + switch (slice_y) { + case 0: + slice_dst.B = slice_dst.T + middle_dst.T; + slice_src.B = slice_src.T + middle_src.T; + break; + + case 1: + slice_dst.T += middle_dst.T; + slice_dst.B -= middle_dst.B; + slice_src.T += middle_src.T; + slice_src.B -= middle_src.B; + break; + + case 2: + slice_dst.T = slice_dst.B - middle_dst.B; + slice_src.T = slice_src.B - middle_src.B; + break; + } + + // If we have a tiled image, then some of the tiles may bleed + // out of the slice rect, so we need to clip to both the + // clipping rect and the destination rect. + RectF slice_clip = m_clip_rect.intersectWith(slice_dst); + + // If this slice is empty or has been entirely clipped, then + // don't bother drawing anything. + if (slice_clip.empty()) { + continue; + } + + // This may be a tiled image, so we need to calculate the size + // of each tile. If the image is not tiled, this should equal + // the size of the destination rect. + SizeF tile_size = slice_dst.size(); + + if (m_style.box_tile != DirFlags::NONE) { + // We need to calculate the tile size based on the texture + // size and the scale of each tile. If the scale is too + // small, then the number of tiles will explode, so we + // clamp it to a reasonable minimum of 1/8 of a pixel. + SizeF tex_size = getTextureSize(m_style.box.image); + float tile_scale = std::max(m_style.box.scale, 0.125f); + + if (m_style.box_tile != DirFlags::Y) { + tile_size.W = slice_src.W() * tex_size.W * tile_scale; + } + if (m_style.box_tile != DirFlags::X) { + tile_size.H = slice_src.H() * tex_size.H * tile_scale; + } + } + + // Now we can draw each tile for this slice. If the image is + // not tiled, then each of these loops will run only once. + float tile_y = slice_dst.T; + + while (tile_y < slice_dst.B) { + float tile_x = slice_dst.L; + + while (tile_x < slice_dst.R) { + // Draw the texture in the appropriate destination rect + // for this tile, and clip it to the clipping rect for + // this slice. + RectF tile_dst = RectF(PosF(tile_x, tile_y), tile_size); + + getWindow().drawTexture(tile_dst, slice_clip, + m_style.box.image, slice_src, m_style.box.tint); + + tile_x += tile_size.W; + } + tile_y += tile_size.H; + } + } + } + } + + void Box::drawIcon() + { + // The icon rect is computed while the box is being laid out, so we + // just need to draw it with the fill color behind it. + getWindow().drawRect(m_icon_rect, m_clip_rect, m_style.icon.fill); + getWindow().drawTexture(m_icon_rect, m_clip_rect, m_style.icon.image, + getLayerSource(m_style.icon), m_style.icon.tint); + } +} diff --git a/src/ui/box.h b/src/ui/box.h new file mode 100644 index 000000000..2a874e89b --- /dev/null +++ b/src/ui/box.h @@ -0,0 +1,102 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2023 v-rob, Vincent Robinson + +#pragma once + +#include "ui/helpers.h" +#include "ui/style.h" +#include "util/basic_macros.h" + +#include +#include +#include +#include + +namespace ui +{ + class Elem; + class Window; + + class Box + { + public: + using State = u32; + + // These states are organized in order of precedence. States with a + // larger value will override the styles of states with a lower value. + static constexpr State STATE_NONE = 0; + + static constexpr State STATE_FOCUSED = 1 << 0; + static constexpr State STATE_SELECTED = 1 << 1; + static constexpr State STATE_HOVERED = 1 << 2; + static constexpr State STATE_PRESSED = 1 << 3; + static constexpr State STATE_DISABLED = 1 << 4; + + static constexpr State NUM_STATES = 1 << 5; + + private: + // Indicates that there is no style string for this state combination. + static constexpr u32 NO_STYLE = -1; + + Elem &m_elem; + + std::vector m_content; + + Style m_style; + std::array m_style_refs; + + // Cached information about the layout of the box, which is cleared in + // restyle() and recomputed in resize() and relayout(). + SizeF m_min_layout; + SizeF m_min_content; + + RectF m_display_rect; + RectF m_icon_rect; + RectF m_content_rect; + + RectF m_clip_rect; + + public: + Box(Elem &elem) : + m_elem(elem) + { + reset(); + } + + DISABLE_CLASS_COPY(Box) + + Elem &getElem() { return m_elem; } + const Elem &getElem() const { return m_elem; } + + Window &getWindow(); + const Window &getWindow() const; + + const std::vector &getContent() const { return m_content; } + void setContent(std::vector content) { m_content = std::move(content); } + + void reset(); + void read(std::istream &is); + + void restyle(); + void resize(); + void relayout(RectF layout_rect, RectF layout_clip); + + void draw(); + + private: + static RectF getLayerSource(const Layer &layer); + static SizeF getLayerSize(const Layer &layer); + + DispF getMiddleEdges(); + + void resizeBox(); + void relayoutBox(RectF layout_rect, RectF layout_clip); + + void resizePlace(); + void relayoutPlace(); + + void drawBox(); + void drawIcon(); + }; +} diff --git a/src/ui/elem.cpp b/src/ui/elem.cpp new file mode 100644 index 000000000..bcc87691f --- /dev/null +++ b/src/ui/elem.cpp @@ -0,0 +1,105 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2023 v-rob, Vincent Robinson + +#include "ui/elem.h" + +#include "debug.h" +#include "log.h" +#include "ui/manager.h" +#include "ui/window.h" +#include "util/serialize.h" + +// Include every element header for Elem::create() +#include "ui/static_elems.h" + +namespace ui +{ + std::unique_ptr Elem::create(Type type, Window &window, std::string id) + { + std::unique_ptr elem = nullptr; + +#define CREATE(name, type) \ + case name: \ + elem = std::make_unique(window, std::move(id)); \ + break + + switch (type) { + CREATE(ELEM, Elem); + CREATE(ROOT, Root); + default: + return nullptr; + } + +#undef CREATE + + // It's a pain to call reset() in the constructor of every single + // element due to how virtual functions work in C++, so we reset + // elements after creating them here. + elem->reset(); + return elem; + } + + Elem::Elem(Window &window, std::string id) : + m_window(window), + m_id(std::move(id)), + m_main_box(*this) + {} + + void Elem::reset() + { + m_order = (size_t)-1; + + m_parent = nullptr; + m_children.clear(); + + m_main_box.reset(); + } + + void Elem::read(std::istream &is) + { + u32 set_mask = readU32(is); + + if (testShift(set_mask)) + readChildren(is); + if (testShift(set_mask)) + m_main_box.read(is); + + std::vector content; + for (Elem *elem : m_children) { + content.push_back(&elem->getMain()); + } + m_main_box.setContent(std::move(content)); + } + + void Elem::readChildren(std::istream &is) + { + u32 num_children = readU32(is); + + for (size_t i = 0; i < num_children; i++) { + std::string id = readNullStr(is); + + Elem *child = m_window.getElem(id, true); + if (child == nullptr) { + continue; + } + + /* Check if this child already has a parent before adding it as a + * child. Elements are deserialized in unspecified order rather + * than a prefix order of parents before their children, so + * isolated circular element refrences are still possible at this + * point. However, cycles including the root are impossible. + */ + if (child->m_parent != nullptr) { + errorstream << "Element \"" << id << "\" already has parent \"" << + child->m_parent->m_id << "\"" << std::endl; + } else if (child == m_window.getRoot()) { + errorstream << "Element \"" << id << + "\" is the root element and cannot have a parent" << std::endl; + } else { + m_children.push_back(child); + child->m_parent = this; + } + } + } +} diff --git a/src/ui/elem.h b/src/ui/elem.h new file mode 100644 index 000000000..0ee388902 --- /dev/null +++ b/src/ui/elem.h @@ -0,0 +1,79 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2023 v-rob, Vincent Robinson + +#pragma once + +#include "ui/box.h" +#include "ui/helpers.h" +#include "util/basic_macros.h" + +#include +#include +#include +#include + +namespace ui +{ + class Window; + + class Elem + { + public: + // Serialized enum; do not change values of entries. + enum Type : u8 + { + ELEM = 0x00, + ROOT = 0x01, + }; + + private: + // The window and ID are intrinsic to the element's identity, so they + // are set by the constructor and aren't cleared in reset() or changed + // in read(). + Window &m_window; + std::string m_id; + + size_t m_order; + + Elem *m_parent; + std::vector m_children; + + Box m_main_box; + + public: + static std::unique_ptr create(Type type, Window &window, std::string id); + + Elem(Window &window, std::string id); + + DISABLE_CLASS_COPY(Elem) + + virtual ~Elem() = default; + + Window &getWindow() { return m_window; } + const Window &getWindow() const { return m_window; } + + const std::string &getId() const { return m_id; } + virtual Type getType() const { return ELEM; } + + size_t getOrder() const { return m_order; } + void setOrder(size_t order) { m_order = order; } + + Elem *getParent() { return m_parent; } + const std::vector &getChildren() { return m_children; } + + Box &getMain() { return m_main_box; } + + virtual void reset(); + virtual void read(std::istream &is); + + protected: + void enableEvent(u32 event); + bool testEvent(u32 event) const; + + std::ostringstream createEvent(u32 event) const; + + private: + void readChildren(std::istream &is); + }; +} diff --git a/src/ui/helpers.h b/src/ui/helpers.h new file mode 100644 index 000000000..6bf90f4a8 --- /dev/null +++ b/src/ui/helpers.h @@ -0,0 +1,515 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 v-rob, Vincent Robinson + +#pragma once + +#include "irrlichttypes.h" +#include "util/serialize.h" + +#include +#include +#include +#include +#include + +#include +#include + +namespace ui +{ + // Define some useful named colors. + const video::SColor BLANK = 0x00000000; + const video::SColor BLACK = 0xFF000000; + const video::SColor WHITE = 0xFFFFFFFF; + + /* UIs deal with tons of 2D positions, sizes, rectangles, and the like, but + * Irrlicht's core::vector2d, core::dimension2d, and core::rect classes are + * inadequate for the job. For instance, vectors use the mathematical + * definition and hence have multiple special forms of multiplication. + * Notably, they don't have component-wise multiplication, which is the + * only one useful for UIs aside from scalar multiplication. Additionally, + * the distinction between a position and a dimension are blurred since + * vectors can perform operations that make no sense for absolute + * positions, e.g. position + position. Dimensions are underpowered, and + * rectangles are clunky to work with for multiple reasons. + * + * So, we create our own classes for use with 2D drawing and UIs. The Pos + * class represents an absolute position, whereas the Size class represents + * a relative displacement, size, or scaling factor. Similarly, the Rect + * class represents an absolute rectangle defined by two Pos fields, + * whereas the Disp class represents a double relative displacement or + * scaling factor defined by two Size fields. + * + * All operations are component-wise, e.g. the multiplication operation for + * two sizes is defined as `a * b == {a.W * b.W, a.H * b.H}`. Other useful + * operations exist, like component-wise minimums and maximums, unions and + * intersections of rectangles, offseting rectangles with displacements in + * multiple ways, and so on. Lastly, functions never mutate the class in + * place, so you can do + * + * doThing(a.intersectWith(b)); + * + * rather than being forced to use the much more clunky + * + * core::recti c = a; + * c.clipAgainst(b); + * doThing(c); + * + * Implicit conversions between these classes and the corresponding + * Irrlicht classes are defined for seamless interoperability with code + * that still uses Irrlicht's types. + */ + template struct Pos; + template struct Size; + template struct Rect; + template struct Disp; + + template + struct Size + { + E W; + E H; + + Size() : W(), H() {} + explicit Size(E n) : W(n), H(n) {} + Size(E w, E h) : W(w), H(h) {} + + template + explicit Size(Pos pos) : W(pos.X), H(pos.Y) {} + template + Size(Size other) : W(other.W), H(other.H) {} + + template + explicit Size(core::vector2d vec) : W(vec.X), H(vec.Y) {} + template + Size(core::dimension2d dim) : W(dim.Width), H(dim.Height) {} + + template + explicit operator core::vector2d() const { return core::vector2d(W, H); } + template + operator core::dimension2d() const { return core::dimension2d(W, H); } + + E area() const { return W * H; } + bool empty() const { return area() == 0; } + + bool operator==(Size other) const { return W == other.W && H == other.H; } + bool operator!=(Size other) const { return !(*this == other); } + + E &operator[](int index) { return index ? H : W; } + const E &operator[](int index) const { return index ? H : W; } + + Size operator+() const { return Size(+W, +H); } + Size operator-() const { return Size(-W, -H); } + + Size operator+(Size other) const { return Size(W + other.W, H + other.H); } + Size operator-(Size other) const { return Size(W - other.W, H - other.H); } + + Size &operator+=(Size other) { *this = *this + other; return *this; } + Size &operator-=(Size other) { *this = *this - other; return *this; } + + Size operator*(Size other) const { return Size(W * other.W, H * other.H); } + Size operator/(Size other) const { return Size(W / other.W, H / other.H); } + + Size &operator*=(Size other) { *this = *this * other; return *this; } + Size &operator/=(Size other) { *this = *this / other; return *this; } + + Size operator*(E scalar) const { return Size(W * scalar, H * scalar); } + Size operator/(E scalar) const { return Size(W / scalar, H / scalar); } + + Size &operator*=(E scalar) { *this = *this * scalar; return *this; } + Size &operator/=(E scalar) { *this = *this / scalar; return *this; } + + Size min(Size other) const + { return Size(std::min(W, other.W), std::min(H, other.H)); } + Size max(Size other) const + { return Size(std::max(W, other.W), std::max(H, other.H)); } + + Size clamp(Size lo, Size hi) const + { return Size(std::clamp(W, lo.W, hi.W), std::clamp(H, lo.H, hi.H)); } + Size clamp(Disp disp) const + { return clamp(disp.TopLeft, disp.BottomRight); } + + Size clip() const { return max(Size()); } + + friend std::ostream &operator<<(std::ostream &os, Size size) + { + os << "(" << size.W << ", " << size.H << ")"; + return os; + } + }; + + using SizeI = Size; + using SizeU = Size; + using SizeF = Size; + + template + struct Pos + { + E X; + E Y; + + Pos() : X(), Y() {} + explicit Pos(E n) : X(n), Y(n) {} + Pos(E x, E y) : X(x), Y(y) {} + + template + Pos(Pos other) : X(other.X), Y(other.Y) {} + template + explicit Pos(Size size) : X(size.W), Y(size.H) {} + + template + Pos(core::vector2d vec) : X(vec.X), Y(vec.Y) {} + template + explicit Pos(core::dimension2d dim) : X(dim.Width), Y(dim.Height) {} + + template + operator core::vector2d() const { return core::vector2d(X, Y); } + template + explicit operator core::dimension2d() const { return core::dimension2d(X, Y); } + + bool operator==(Pos other) const { return X == other.X && Y == other.Y; } + bool operator!=(Pos other) const { return !(*this == other); } + + E &operator[](int index) { return index ? Y : X; } + const E &operator[](int index) const { return index ? Y : X; } + + Pos operator+(Size size) const { return Pos(X + size.W, Y + size.H); } + Pos operator-(Size size) const { return Pos(X - size.W, Y - size.H); } + + Pos &operator+=(Size size) { *this = *this + size; return *this; } + Pos &operator-=(Size size) { *this = *this - size; return *this; } + + Pos operator*(Size size) const { return Pos(X * size.W, Y * size.H); } + Pos operator/(Size size) const { return Pos(X / size.W, Y / size.H); } + + Pos &operator*=(Size size) { *this = *this * size; return *this; } + Pos &operator/=(Size size) { *this = *this / size; return *this; } + + Pos operator*(E scalar) const { return Pos(X * scalar, Y * scalar); } + Pos operator/(E scalar) const { return Pos(X / scalar, Y / scalar); } + + Pos &operator*=(E scalar) { *this = *this * scalar; return *this; } + Pos &operator/=(E scalar) { *this = *this / scalar; return *this; } + + Size operator-(Pos other) const { return Size(X - other.X, Y - other.Y); } + Size operator/(Pos other) const { return Size(X / other.X, Y / other.Y); } + + Pos min(Pos other) const + { return Pos(std::min(X, other.X), std::min(Y, other.Y)); } + Pos max(Pos other) const + { return Pos(std::max(X, other.X), std::max(Y, other.Y)); } + + Pos clamp(Pos lo, Pos hi) const + { return Pos(std::clamp(X, lo.X, hi.X), std::clamp(Y, lo.Y, hi.Y)); } + Pos clamp(Rect rect) const + { return clamp(rect.TopLeft, rect.BottomRight); } + + friend std::ostream &operator<<(std::ostream &os, Pos pos) + { + os << "(" << pos.X << ", " << pos.Y << ")"; + return os; + } + }; + + using PosI = Pos; + using PosU = Pos; + using PosF = Pos; + + template + struct Disp + { + union { + struct { + E L; + E T; + }; + Size TopLeft; + }; + union { + struct { + E R; + E B; + }; + Size BottomRight; + }; + + Disp() : L(), T(), R(), B() {} + explicit Disp(E n) : L(n), T(n), R(n), B(n) {} + Disp(E x, E y) : L(x), T(y), R(x), B(y) {} + Disp(E l, E t, E r, E b) : L(l), T(t), R(r), B(b) {} + + explicit Disp(Size size) : TopLeft(size), BottomRight(size) {} + Disp(Size tl, Size br) : TopLeft(tl), BottomRight(br) {} + + template + explicit Disp(Rect rect) : TopLeft(rect.TopLeft), BottomRight(rect.BottomRight) {} + template + Disp(Disp other) : TopLeft(other.TopLeft), BottomRight(other.BottomRight) {} + + template + explicit Disp(core::rect rect) : + TopLeft(rect.UpperLeftCorner), BottomRight(rect.LowerRightCorner) {} + + template + explicit operator core::rect() const { return core::rect(Rect(*this)); } + + E X() const { return L + R; } + E Y() const { return T + B; } + Size extents() const { return TopLeft + BottomRight; } + + bool operator==(Disp other) const + { return TopLeft == other.TopLeft && BottomRight == other.BottomRight; } + bool operator!=(Disp other) const { return !(*this == other); } + + Disp operator+() const { return Disp(+TopLeft, +BottomRight); } + Disp operator-() const { return Disp(-TopLeft, -BottomRight); } + + Disp operator+(Disp other) const + { return Disp(TopLeft + other.TopLeft, BottomRight + other.BottomRight); } + Disp operator-(Disp other) const + { return Disp(TopLeft - other.TopLeft, BottomRight - other.BottomRight); } + + Disp &operator+=(Disp other) { *this = *this + other; return *this; } + Disp &operator-=(Disp other) { *this = *this - other; return *this; } + + Disp operator*(Disp other) const + { return Disp(TopLeft * other.TopLeft, BottomRight * other.BottomRight); } + Disp operator/(Disp other) const + { return Disp(TopLeft / other.TopLeft, BottomRight / other.BottomRight); } + + Disp &operator*=(Disp other) { *this = *this * other; return *this; } + Disp &operator/=(Disp other) { *this = *this / other; return *this; } + + Disp operator*(E scalar) const + { return Disp(TopLeft * scalar, BottomRight * scalar); } + Disp operator/(E scalar) const + { return Disp(TopLeft / scalar, BottomRight / scalar); } + + Disp &operator*=(E scalar) { *this = *this * scalar; return *this; } + Disp &operator/=(E scalar) { *this = *this / scalar; return *this; } + + Disp clip() const { return Disp(TopLeft.clip(), BottomRight.clip()); } + + friend std::ostream &operator<<(std::ostream &os, Disp disp) + { + os << "(" << disp.L << ", " << disp.T << ", " << disp.R << ", " << disp.B << ")"; + return os; + } + }; + + using DispI = Disp; + using DispU = Disp; + using DispF = Disp; + + template + struct Rect + { + union { + struct { + E L; + E T; + }; + Pos TopLeft; + }; + union { + struct { + E R; + E B; + }; + Pos BottomRight; + }; + + Rect() : L(), T(), R(), B() {} + Rect(E l, E t, E r, E b) : L(l), T(t), R(r), B(b) {} + + explicit Rect(Pos pos) : TopLeft(pos), BottomRight(pos) {} + Rect(Pos tl, Pos br) : TopLeft(tl), BottomRight(br) {} + + explicit Rect(Size size) : TopLeft(), BottomRight(size) {} + Rect(Pos pos, Size size) : TopLeft(pos), BottomRight(pos + size) {} + + template + Rect(Rect other) : TopLeft(other.TopLeft), BottomRight(other.BottomRight) {} + template + explicit Rect(Disp disp) : TopLeft(disp.TopLeft), BottomRight(disp.BottomRight) {} + + template + Rect(core::rect rect) : + TopLeft(rect.UpperLeftCorner), BottomRight(rect.LowerRightCorner) {} + + template + operator core::rect() const { return core::rect(TopLeft, BottomRight); } + + E W() const { return R - L; } + E H() const { return B - T; } + Size size() const { return BottomRight - TopLeft; } + + E area() const { return size().area(); } + bool empty() const { return size().empty(); } + + bool operator==(Rect other) const + { return TopLeft == other.TopLeft && BottomRight == other.BottomRight; } + bool operator!=(Rect other) const { return !(*this == other); } + + Rect operator+(Disp disp) const + { return Rect(TopLeft + disp.TopLeft, BottomRight + disp.BottomRight); } + Rect operator-(Disp disp) const + { return Rect(TopLeft - disp.TopLeft, BottomRight - disp.BottomRight); } + + Rect &operator+=(Disp disp) { *this = *this + disp; return *this; } + Rect &operator-=(Disp disp) { *this = *this - disp; return *this; } + + Rect operator*(Disp disp) const + { return Rect(TopLeft * disp.TopLeft, BottomRight * disp.BottomRight); } + Rect operator/(Disp disp) const + { return Rect(TopLeft / disp.TopLeft, BottomRight / disp.BottomRight); } + + Rect &operator*=(Disp disp) { *this = *this * disp; return *this; } + Rect &operator/=(Disp disp) { *this = *this / disp; return *this; } + + Rect operator*(E scalar) const + { return Rect(TopLeft * scalar, BottomRight * scalar); } + Rect operator/(E scalar) const + { return Rect(TopLeft / scalar, BottomRight / scalar); } + + Rect &operator*=(E scalar) { *this = *this * scalar; return *this; } + Rect &operator/=(E scalar) { *this = *this / scalar; return *this; } + + Disp operator-(Rect other) const + { return Disp(TopLeft - other.TopLeft, BottomRight - other.BottomRight); } + Disp operator/(Rect other) const + { return Disp(TopLeft / other.TopLeft, BottomRight / other.BottomRight); } + + Rect insetBy(Disp disp) const + { return Rect(TopLeft + disp.TopLeft, BottomRight - disp.BottomRight); } + Rect outsetBy(Disp disp) const + { return Rect(TopLeft - disp.TopLeft, BottomRight + disp.BottomRight); } + + Rect unionWith(Rect other) const + { return Rect(TopLeft.min(other.TopLeft), BottomRight.max(other.BottomRight)); } + Rect intersectWith(Rect other) const + { return Rect(TopLeft.max(other.TopLeft), BottomRight.min(other.BottomRight)); } + + Rect clip() const { return Rect(TopLeft, size().clip()); } + + bool contains(Pos pos) const + { return pos.X >= L && pos.Y >= T && pos.X < R && pos.Y < B; } + + friend std::ostream &operator<<(std::ostream &os, Rect rect) + { + os << "(" << rect.L << ", " << rect.T << ", " << rect.R << ", " << rect.B << ")"; + return os; + } + }; + + using RectI = Rect; + using RectU = Rect; + using RectF = Rect; + + // Define a few functions that are particularly useful for UI serialization + // and deserialization. The testShift() function shifts out and returns the + // lower bit of an integer containing flags, which is particularly useful + // for testing whether an optional serialized field is present or not. + inline bool testShift(u32 &mask) + { + bool test = mask & 1; + mask >>= 1; + return test; + } + + // Booleans are often stored directly in the flags value. However, we want + // the bit position of each field to stay constant, so the mask needs to be + // shifted regardless of whether the boolean is set. + inline void testShiftBool(u32 &mask, bool &flag) + { + if (testShift(mask)) { + flag = testShift(mask); + } else { + testShift(mask); + } + } + + // Convenience functions for creating new binary streams. + inline std::istringstream newIs(const std::string &str) + { + return std::istringstream(str, std::ios_base::binary); + } + + inline std::ostringstream newOs() + { + return std::ostringstream(std::ios_base::binary); + } + + // The UI purposefully avoids dealing with SerializationError, so it always + // uses string functions that truncate gracefully. Hence, we make + // convenience wrappers around the string functions in "serialize.h". + inline std::string readStr16(std::istream &is) { return deSerializeString16(is, true); } + inline std::string readStr32(std::istream &is) { return deSerializeString32(is, true); } + + inline void writeStr16(std::ostream &os, std::string_view str) + { os << serializeString16(str, true); } + inline void writeStr32(std::ostream &os, std::string_view str) + { os << serializeString32(str, true); } + + // The UI also uses null-terminated strings for certain fields as well, so + // define functions to work with them. + inline std::string readNullStr(std::istream &is) + { + std::string str; + std::getline(is, str, '\0'); + return str; + } + + inline void writeNullStr(std::ostream &os, std::string_view str) + { + os << std::string_view(str.data(), std::min(str.find('\0'), str.size())) << '\0'; + } + + // Define serialization and deserialization functions that work with the + // positioning types above. Brace initializer lists are used for the + // constructors because they guarantee left-to-right argument evaluation. + inline PosI readPosI(std::istream &is) { return PosI{readS32(is), readS32(is)}; } + inline PosU readPosU(std::istream &is) { return PosU{readU32(is), readU32(is)}; } + inline PosF readPosF(std::istream &is) { return PosF{readF32(is), readF32(is)}; } + + inline void writePosI(std::ostream &os, PosI pos) + { writeS32(os, pos.X); writeS32(os, pos.Y); } + inline void writePosU(std::ostream &os, PosU pos) + { writeU32(os, pos.X); writeU32(os, pos.Y); } + inline void writePosF(std::ostream &os, PosF pos) + { writeF32(os, pos.X); writeF32(os, pos.Y); } + + inline SizeI readSizeI(std::istream &is) { return SizeI{readS32(is), readS32(is)}; } + inline SizeU readSizeU(std::istream &is) { return SizeU{readU32(is), readU32(is)}; } + inline SizeF readSizeF(std::istream &is) { return SizeF{readF32(is), readF32(is)}; } + + inline void writeSizeI(std::ostream &os, SizeI size) + { writeS32(os, size.W); writeS32(os, size.H); } + inline void writeSizeU(std::ostream &os, SizeU size) + { writeU32(os, size.W); writeU32(os, size.H); } + inline void writeSizeF(std::ostream &os, SizeF size) + { writeF32(os, size.W); writeF32(os, size.H); } + + inline RectI readRectI(std::istream &is) { return RectI{readPosI(is), readPosI(is)}; } + inline RectU readRectU(std::istream &is) { return RectU{readPosU(is), readPosU(is)}; } + inline RectF readRectF(std::istream &is) { return RectF{readPosF(is), readPosF(is)}; } + + inline void writeRectI(std::ostream &os, RectI rect) + { writePosI(os, rect.TopLeft); writePosI(os, rect.BottomRight); } + inline void writeRectU(std::ostream &os, RectU rect) + { writePosU(os, rect.TopLeft); writePosU(os, rect.BottomRight); } + inline void writeRectF(std::ostream &os, RectF rect) + { writePosF(os, rect.TopLeft); writePosF(os, rect.BottomRight); } + + inline DispI readDispI(std::istream &is) { return DispI{readSizeI(is), readSizeI(is)}; } + inline DispU readDispU(std::istream &is) { return DispU{readSizeU(is), readSizeU(is)}; } + inline DispF readDispF(std::istream &is) { return DispF{readSizeF(is), readSizeF(is)}; } + + inline void writeDispI(std::ostream &os, DispI disp) + { writeSizeI(os, disp.TopLeft); writeSizeI(os, disp.BottomRight); } + inline void writeDispU(std::ostream &os, DispU disp) + { writeSizeU(os, disp.TopLeft); writeSizeU(os, disp.BottomRight); } + inline void writeDispF(std::ostream &os, DispF disp) + { writeSizeF(os, disp.TopLeft); writeSizeF(os, disp.BottomRight); } +} diff --git a/src/ui/manager.cpp b/src/ui/manager.cpp new file mode 100644 index 000000000..5a8764578 --- /dev/null +++ b/src/ui/manager.cpp @@ -0,0 +1,123 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2023 v-rob, Vincent Robinson + +#include "ui/manager.h" + +#include "debug.h" +#include "log.h" +#include "settings.h" +#include "client/client.h" +#include "client/renderingengine.h" +#include "client/texturesource.h" +#include "client/tile.h" +#include "util/serialize.h" + +namespace ui +{ + video::ITexture *Manager::getTexture(const std::string &name) const + { + return m_client->tsrc()->getTexture(name); + } + + float Manager::getScale(WindowType type) const + { + if (type == WindowType::GUI || type == WindowType::CHAT) { + return m_gui_scale; + } + return m_hud_scale; + } + + void Manager::reset() + { + m_client = nullptr; + + m_windows.clear(); + } + + void Manager::removeWindow(u64 id) + { + auto it = m_windows.find(id); + if (it == m_windows.end()) { + errorstream << "Window " << id << " is already closed" << std::endl; + return; + } + + m_windows.erase(it); + } + + void Manager::receiveMessage(const std::string &data) + { + auto is = newIs(data); + + u32 action = readU8(is); + u64 id = readU64(is); + + switch (action) { + case REOPEN_WINDOW: { + u64 close_id = readU64(is); + removeWindow(close_id); + + [[fallthrough]]; + } + + case OPEN_WINDOW: { + auto it = m_windows.find(id); + if (it != m_windows.end()) { + errorstream << "Window " << id << " is already open" << std::endl; + break; + } + + it = m_windows.emplace(id, id).first; + if (!it->second.read(is, true)) { + errorstream << "Fatal error when opening window " << id << + "; closing window" << std::endl; + removeWindow(id); + break; + } + break; + } + + case UPDATE_WINDOW: { + auto it = m_windows.find(id); + if (it == m_windows.end()) { + errorstream << "Window " << id << " does not exist" << std::endl; + } + + if (!it->second.read(is, false)) { + errorstream << "Fatal error when updating window " << id << + "; closing window" << std::endl; + removeWindow(id); + break; + } + break; + } + + case CLOSE_WINDOW: + removeWindow(id); + break; + + default: + errorstream << "Invalid manager action: " << action << std::endl; + break; + } + } + + void Manager::preDraw() + { + float base_scale = RenderingEngine::getDisplayDensity(); + m_gui_scale = base_scale * g_settings->getFloat("gui_scaling"); + m_hud_scale = base_scale * g_settings->getFloat("hud_scaling"); + } + + void Manager::drawType(WindowType type) + { + for (auto &it : m_windows) { + if (it.second.getType() == type) { + it.second.drawAll(); + } + } + } + + Manager g_manager; +} diff --git a/src/ui/manager.h b/src/ui/manager.h new file mode 100644 index 000000000..f9d0f8b7f --- /dev/null +++ b/src/ui/manager.h @@ -0,0 +1,85 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2023 v-rob, Vincent Robinson + +#pragma once + +#include "ui/helpers.h" +#include "ui/window.h" +#include "util/basic_macros.h" + +#include + +#include +#include +#include + +class Client; + +namespace ui +{ + class Manager + { + public: + // Serialized enum; do not change values of entries. + enum ReceiveAction : u8 + { + OPEN_WINDOW = 0x00, + REOPEN_WINDOW = 0x01, + UPDATE_WINDOW = 0x02, + CLOSE_WINDOW = 0x03, + }; + + private: + Client *m_client; + + float m_gui_scale = 0.0f; + float m_hud_scale = 0.0f; + + // Use map rather than unordered_map so that windows are always sorted + // by window ID to make sure that they are drawn in order of creation. + std::map m_windows; + + public: + Manager() + { + reset(); + } + + DISABLE_CLASS_COPY(Manager) + + Client *getClient() const { return m_client; } + void setClient(Client *client) { m_client = client; } + + video::ITexture *getTexture(const std::string &name) const; + + float getScale(WindowType type) const; + + void reset(); + void removeWindow(u64 id); + + void receiveMessage(const std::string &data); + + void preDraw(); + void drawType(WindowType type); + }; + + extern Manager g_manager; + + // Inconveniently, we need a way to draw the "gui" window types after the + // chat console but before other GUIs like the key change menu, formspecs, + // etc. So, we inject our own mini Irrlicht element in between. + class GUIManagerElem : public gui::IGUIElement + { + public: + GUIManagerElem(gui::IGUIEnvironment* env, gui::IGUIElement* parent, s32 id) : + gui::IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, core::recti()) + {} + + virtual void draw() override + { + g_manager.drawType(ui::WindowType::GUI); + gui::IGUIElement::draw(); + } + }; +} diff --git a/src/ui/static_elems.cpp b/src/ui/static_elems.cpp new file mode 100644 index 000000000..0ff5d70d3 --- /dev/null +++ b/src/ui/static_elems.cpp @@ -0,0 +1,33 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 v-rob, Vincent Robinson + +#include "ui/static_elems.h" + +#include "debug.h" +#include "log.h" +#include "ui/manager.h" +#include "util/serialize.h" + +namespace ui +{ + void Root::reset() + { + Elem::reset(); + + m_backdrop_box.reset(); + } + + void Root::read(std::istream &is) + { + auto super = newIs(readStr32(is)); + Elem::read(super); + + u32 set_mask = readU32(is); + + if (testShift(set_mask)) + m_backdrop_box.read(is); + + m_backdrop_box.setContent({&getMain()}); + } +} diff --git a/src/ui/static_elems.h b/src/ui/static_elems.h new file mode 100644 index 000000000..146f4c570 --- /dev/null +++ b/src/ui/static_elems.h @@ -0,0 +1,34 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 v-rob, Vincent Robinson + +#pragma once + +#include "ui/box.h" +#include "ui/elem.h" +#include "ui/helpers.h" + +#include +#include + +namespace ui +{ + class Root : public Elem + { + private: + Box m_backdrop_box; + + public: + Root(Window &window, std::string id) : + Elem(window, std::move(id)), + m_backdrop_box(*this) + {} + + virtual Type getType() const override { return ROOT; } + + Box &getBackdrop() { return m_backdrop_box; } + + virtual void reset() override; + virtual void read(std::istream &is) override; + }; +} diff --git a/src/ui/style.cpp b/src/ui/style.cpp new file mode 100644 index 000000000..b0e92482d --- /dev/null +++ b/src/ui/style.cpp @@ -0,0 +1,185 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 v-rob, Vincent Robinson + +#include "ui/style.h" + +#include "debug.h" +#include "log.h" +#include "ui/manager.h" +#include "util/serialize.h" + +namespace ui +{ + static LayoutType toLayoutType(u8 type) + { + if (type > (u8)LayoutType::MAX) { + return LayoutType::PLACE; + } + return (LayoutType)type; + } + + static DirFlags toDirFlags(u8 dir) + { + if (dir > (u8)DirFlags::MAX) { + return DirFlags::NONE; + } + return (DirFlags)dir; + } + + static DisplayMode toDisplayMode(u8 mode) + { + if (mode > (u8)DisplayMode::MAX) { + return DisplayMode::VISIBLE; + } + return (DisplayMode)mode; + } + + static IconPlace toIconPlace(u8 place) + { + if (place > (u8)IconPlace::MAX) { + return IconPlace::CENTER; + } + return (IconPlace)place; + } + + void Layout::reset() + { + type = LayoutType::PLACE; + clip = DirFlags::NONE; + + scale = 0.0f; + } + + void Layout::read(std::istream &full_is) + { + auto is = newIs(readStr16(full_is)); + u32 set_mask = readU32(is); + + if (testShift(set_mask)) + type = toLayoutType(readU8(is)); + if (testShift(set_mask)) + clip = toDirFlags(readU8(is)); + + if (testShift(set_mask)) + scale = std::max(readF32(is), 0.0f); + } + + void Sizing::reset() + { + size = SizeF(0.0f, 0.0f); + span = SizeF(1.0f, 1.0f); + + pos = PosF(0.0f, 0.0f); + anchor = PosF(0.0f, 0.0f); + + margin = DispF(0.0f, 0.0f, 0.0f, 0.0f); + padding = DispF(0.0f, 0.0f, 0.0f, 0.0f); + } + + void Sizing::read(std::istream &full_is) + { + auto is = newIs(readStr16(full_is)); + u32 set_mask = readU32(is); + + if (testShift(set_mask)) + size = readSizeF(is).clip(); + if (testShift(set_mask)) + span = readSizeF(is).clip(); + + if (testShift(set_mask)) + pos = readPosF(is); + if (testShift(set_mask)) + anchor = readPosF(is); + + if (testShift(set_mask)) + margin = readDispF(is); + if (testShift(set_mask)) + padding = readDispF(is); + } + + void Layer::reset() + { + image = nullptr; + fill = BLANK; + tint = WHITE; + + scale = 1.0f; + source = RectF(0.0f, 0.0f, 1.0f, 1.0f); + + num_frames = 1; + frame_time = 1000; + } + + void Layer::read(std::istream &full_is) + { + auto is = newIs(readStr16(full_is)); + u32 set_mask = readU32(is); + + if (testShift(set_mask)) + image = g_manager.getTexture(readNullStr(is)); + if (testShift(set_mask)) + fill = readARGB8(is); + if (testShift(set_mask)) + tint = readARGB8(is); + + if (testShift(set_mask)) + scale = std::max(readF32(is), 0.0f); + if (testShift(set_mask)) + source = readRectF(is); + + if (testShift(set_mask)) + num_frames = std::max(readU32(is), 1U); + if (testShift(set_mask)) + frame_time = std::max(readU32(is), 1U); + } + + void Style::reset() + { + layout.reset(); + sizing.reset(); + + display = DisplayMode::VISIBLE; + + box.reset(); + icon.reset(); + + box_middle = DispF(0.0f, 0.0f, 0.0f, 0.0f); + box_tile = DirFlags::NONE; + + icon_place = IconPlace::CENTER; + icon_gutter = 0.0f; + icon_overlap = false; + } + + void Style::read(std::istream &is) + { + // No need to read a size prefix; styles are already read in as size- + // prefixed strings in Window. + u32 set_mask = readU32(is); + + if (testShift(set_mask)) + layout.read(is); + if (testShift(set_mask)) + sizing.read(is); + + if (testShift(set_mask)) + display = toDisplayMode(readU8(is)); + + if (testShift(set_mask)) + box.read(is); + if (testShift(set_mask)) + icon.read(is); + + if (testShift(set_mask)) + box_middle = readDispF(is).clip(); + if (testShift(set_mask)) + box_tile = toDirFlags(readU8(is)); + + if (testShift(set_mask)) + icon_place = toIconPlace(readU8(is)); + if (testShift(set_mask)) + icon_gutter = readF32(is); + testShiftBool(set_mask, icon_overlap); + } +} diff --git a/src/ui/style.h b/src/ui/style.h new file mode 100644 index 000000000..bde469365 --- /dev/null +++ b/src/ui/style.h @@ -0,0 +1,126 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 v-rob, Vincent Robinson + +#pragma once + +#include "ui/helpers.h" + +#include +#include + +namespace ui +{ + // Serialized enum; do not change order of entries. + enum class LayoutType : u8 + { + PLACE, + + MAX = PLACE, + }; + + // Serialized enum; do not change order of entries. + enum class DirFlags : u8 + { + NONE, + X, + Y, + BOTH, + + MAX = BOTH, + }; + + // Serialized enum; do not change order of entries. + enum class DisplayMode : u8 + { + VISIBLE, + OVERFLOW, + HIDDEN, + CLIPPED, + + MAX = CLIPPED, + }; + + // Serialized enum; do not change order of entries. + enum class IconPlace : u8 + { + CENTER, + LEFT, + TOP, + RIGHT, + BOTTOM, + + MAX = BOTTOM, + }; + + struct Layout + { + LayoutType type; + DirFlags clip; + + float scale; + + Layout() { reset(); } + + void reset(); + void read(std::istream &is); + }; + + struct Sizing + { + SizeF size; + SizeF span; + + PosF pos; + PosF anchor; + + DispF margin; + DispF padding; + + Sizing() { reset(); } + + void reset(); + void read(std::istream &is); + }; + + struct Layer + { + video::ITexture *image; + video::SColor fill; + video::SColor tint; + + float scale; + RectF source; + + u32 num_frames; + u32 frame_time; + + Layer() { reset(); } + + void reset(); + void read(std::istream &is); + }; + + struct Style + { + Layout layout; + Sizing sizing; + + DisplayMode display; + + Layer box; + Layer icon; + + DispF box_middle; + DirFlags box_tile; + + IconPlace icon_place; + float icon_gutter; + bool icon_overlap; + + Style() { reset(); } + + void reset(); + void read(std::istream &is); + }; +} diff --git a/src/ui/window.cpp b/src/ui/window.cpp new file mode 100644 index 000000000..1f38715c7 --- /dev/null +++ b/src/ui/window.cpp @@ -0,0 +1,310 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2023 v-rob, Vincent Robinson + +#include "ui/window.h" + +#include "debug.h" +#include "log.h" +#include "settings.h" +#include "client/client.h" +#include "client/renderingengine.h" +#include "client/tile.h" +#include "ui/box.h" +#include "ui/manager.h" +#include "ui/static_elems.h" +#include "util/serialize.h" +#include "util/string.h" + +namespace ui +{ + SizeI getTextureSize(video::ITexture *texture) + { + if (texture != nullptr) { + return SizeI(texture->getOriginalSize()); + } + return SizeI(0, 0); + } + + WindowType toWindowType(u8 type) + { + if (type > (u8)WindowType::MAX) { + return WindowType::HUD; + } + return (WindowType)type; + } + + Elem *Window::getElem(const std::string &id, bool required) + { + // Empty IDs may be valid values if the element is optional. + if (id.empty() && !required) { + return nullptr; + } + + // If the ID is not empty, then we need to search for an actual + // element. Not finding one means that an error occurred. + auto it = m_elems.find(id); + if (it != m_elems.end()) { + return it->second.get(); + } + + errorstream << "Element \"" << id << "\" does not exist" << std::endl; + return nullptr; + } + + const std::string *Window::getStyleStr(u32 index) const + { + if (index < m_style_strs.size()) { + return &m_style_strs[index]; + } + return nullptr; + } + + void Window::reset() + { + m_elems.clear(); + m_ordered_elems.clear(); + + m_root_elem = nullptr; + + m_style_strs.clear(); + } + + bool Window::read(std::istream &is, bool opening) + { + std::unordered_map elem_contents; + readElems(is, elem_contents); + + if (!readRootElem(is)) { + return false; + } + + readStyles(is); + + if (opening) { + m_type = toWindowType(readU8(is)); + } + + // Finally, we can proceed to read in all the element properties. + return updateElems(elem_contents); + } + + float Window::getScale() const + { + return g_manager.getScale(m_type); + } + + SizeF Window::getScreenSize() const + { + SizeF size = RenderingEngine::get_video_driver()->getCurrentRenderTargetSize(); + return size / getScale(); + } + + void Window::drawRect(RectF dst, RectF clip, video::SColor color) + { + if (dst.intersectWith(clip).empty() || color.getAlpha() == 0) { + return; + } + + core::recti scaled_clip = clip * getScale(); + + RenderingEngine::get_video_driver()->draw2DRectangle( + color, dst * getScale(), &scaled_clip); + } + + void Window::drawTexture(RectF dst, RectF clip, video::ITexture *texture, + RectF src, video::SColor tint) + { + if (dst.intersectWith(clip).empty() || + texture == nullptr || tint.getAlpha() == 0) { + return; + } + + core::recti scaled_clip = clip * getScale(); + video::SColor colors[] = {tint, tint, tint, tint}; + + RenderingEngine::get_video_driver()->draw2DImage(texture, dst * getScale(), + src * DispF(getTextureSize(texture)), &scaled_clip, colors, true); + } + + void Window::drawAll() + { + Box &backdrop = m_root_elem->getBackdrop(); + + // Since the elements, screen size, pixel size, or style properties + // might have changed since the last frame, we need to recompute stuff + // before drawing: restyle all the boxes, recompute the base sizes from + // the leaves to the root, and then layout each element in the element + // tree from the root to the leaves. + backdrop.restyle(); + backdrop.resize(); + + RectF layout_rect(getScreenSize()); + backdrop.relayout(layout_rect, layout_rect); + + // Draw all of the newly layouted and updated elements. + backdrop.draw(); + } + + void Window::readElems(std::istream &is, + std::unordered_map &elem_contents) + { + // Read in all the new elements and updates to existing elements. + u32 num_elems = readU32(is); + + std::unordered_map> new_elems; + + for (size_t i = 0; i < num_elems; i++) { + u32 type = readU8(is); + std::string id = readNullStr(is); + + // Make sure that elements have non-empty IDs since that indicates + // a nonexistent element in getElem(). If the string has non-ID + // characters in it, though, we don't particularly care. + if (id.empty()) { + errorstream << "Element has empty ID" << std::endl; + continue; + } + + // Each element has a size prefix stating how big the element is. + // This allows new fields to be added to elements without breaking + // compatibility. So, read it in as a string and save it for later. + std::string contents = readStr32(is); + + // If this is a duplicate element, skip it right away. + if (new_elems.find(id) != new_elems.end()) { + errorstream << "Duplicate element \"" << id << "\"" << std::endl; + continue; + } + + /* Now we need to decide whether to create a new element or to + * modify the state of an already existing one. This allows + * changing attributes of an element (like the style or the + * element's children) while leaving leaving persistent state + * intact (such as the position of a scrollbar or the contents of a + * text field). + */ + std::unique_ptr elem = nullptr; + + // Search for a pre-existing element. + auto it = m_elems.find(id); + + if (it == m_elems.end() || it->second->getType() != type) { + // If the element was not found or the existing element has the + // wrong type, create a new element. + elem = Elem::create((Elem::Type)type, *this, id); + + // If we couldn't create the element, the type was invalid. + // Skip this element entirely. + if (elem == nullptr) { + errorstream << "Element \"" << id << "\" has an invalid type: " << + type << std::endl; + continue; + } + } else { + // Otherwise, use the existing element. + elem = std::move(it->second); + } + + // Now that we've gotten our element, reset its contents. + elem->reset(); + + // We need to read in all elements before updating each element, so + // save the element's contents for later. + elem_contents[elem.get()] = contents; + new_elems.emplace(id, std::move(elem)); + } + + // Set these elements as our list of new elements. + m_elems = std::move(new_elems); + + // Clear the ordered elements for now. They will be regenerated later. + m_ordered_elems.clear(); + } + + bool Window::readRootElem(std::istream &is) + { + // Get the root element of the window and make sure it's valid. + Elem *root = getElem(readNullStr(is), true); + + if (root == nullptr) { + errorstream << "Window " << m_id << " has no root element" << std::endl; + return false; + } else if (root->getType() != Elem::ROOT) { + errorstream << "Window " << m_id << + " has wrong type for root element" << std::endl; + return false; + } + + m_root_elem = static_cast(root); + return true; + } + + void Window::readStyles(std::istream &is) + { + // Styles are stored in their raw binary form; every time a style needs + // to be recalculated, these binary strings can be applied one over the + // other, resulting in automatic cascading styles. + u32 num_styles = readU32(is); + m_style_strs.clear(); + + for (size_t i = 0; i < num_styles; i++) { + m_style_strs.push_back(readStr16(is)); + } + } + + bool Window::updateElems(std::unordered_map &elem_contents) + { + // Now that we have a fully updated window, we can update each element + // with its contents and set up the parent-child relations. We couldn't + // do this before because elements need to be able to call getElem() + // and getStyleStr(). + for (auto &contents : elem_contents) { + auto is = newIs(contents.second); + contents.first->read(is); + } + + // Check the depth of the element tree; if it's too deep, there's + // potential for stack overflow. We also create the list of ordered + // elements since we're already doing a preorder traversal. + if (!updateTree(m_root_elem, 1)) { + return false; + } + + // If the number of elements discovered by the tree traversal is less + // than the total number of elements, orphaned elements must exist. + if (m_elems.size() != m_ordered_elems.size()) { + errorstream << "Window " << m_id << " has orphaned elements" << std::endl; + return false; + } + return true; + } + + bool Window::updateTree(Elem *elem, size_t depth) + { + // The parent gets ordered before its children since the ordering of + // elements follows draw order. + elem->setOrder(m_ordered_elems.size()); + m_ordered_elems.push_back(elem); + + if (depth > MAX_TREE_DEPTH) { + errorstream << "Window " << m_id << + " exceeds the max tree depth of " << MAX_TREE_DEPTH << std::endl; + return false; + } + + for (Elem *child : elem->getChildren()) { + if (child->getType() == Elem::ROOT) { + errorstream << "Element of root type \"" << child->getId() << + "\" is not root of window" << std::endl; + return false; + } + + if (!updateTree(child, depth + 1)) { + return false; + } + } + + return true; + } +} diff --git a/src/ui/window.h b/src/ui/window.h new file mode 100644 index 000000000..234418329 --- /dev/null +++ b/src/ui/window.h @@ -0,0 +1,96 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2023 v-rob, Vincent Robinson + +#pragma once + +#include "ui/elem.h" +#include "ui/helpers.h" +#include "util/basic_macros.h" + +#include +#include +#include +#include +#include + +namespace ui +{ + class Root; + + SizeI getTextureSize(video::ITexture *texture); + + // Serialized enum; do not change order of entries. + enum class WindowType : u8 + { + FILTER, + MASK, + HUD, + CHAT, + GUI, + + MAX = GUI, + }; + + WindowType toWindowType(u8 type); + + class Window + { + private: + static constexpr size_t MAX_TREE_DEPTH = 64; + + // The ID and type are intrinsic to the box's identity, so they aren't + // cleared in reset(). The ID is set by the constructor, whereas the + // type is deserialized when the window is first opened. + u64 m_id; + WindowType m_type = WindowType::GUI; + + std::unordered_map> m_elems; + std::vector m_ordered_elems; + + Root *m_root_elem; + + std::vector m_style_strs; + + public: + Window(u64 id) : + m_id(id) + { + reset(); + } + + DISABLE_CLASS_COPY(Window) + + u64 getId() const { return m_id; } + WindowType getType() const { return m_type; } + + const std::vector &getElems() { return m_ordered_elems; } + + Elem *getElem(const std::string &id, bool required); + Root *getRoot() { return m_root_elem; } + + const std::string *getStyleStr(u32 index) const; + + void reset(); + bool read(std::istream &is, bool opening); + + float getScale() const; + + SizeF getScreenSize() const; + + void drawRect(RectF dst, RectF clip, video::SColor color); + void drawTexture(RectF dst, RectF clip, video::ITexture *texture, + RectF src = RectF(0.0f, 0.0f, 1.0f, 1.0f), video::SColor tint = WHITE); + + void drawAll(); + + private: + void readElems(std::istream &is, + std::unordered_map &elem_contents); + bool readRootElem(std::istream &is); + void readStyles(std::istream &is); + + bool updateElems(std::unordered_map &elem_contents); + bool updateTree(Elem *elem, size_t depth); + }; +}