mirror of
https://github.com/luanti-org/luanti.git
synced 2025-06-27 16:36:03 +00:00
Create C++ backend UI code
This commit is contained in:
parent
2f33926573
commit
abfb319e9b
36 changed files with 2466 additions and 10 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -64,6 +64,9 @@
|
|||
#if USE_SOUND
|
||||
#include "client/sound/sound_openal.h"
|
||||
#endif
|
||||
#if BUILD_UI
|
||||
#include "ui/manager.h"
|
||||
#endif
|
||||
|
||||
#include <csignal>
|
||||
|
||||
|
@ -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<GameUI> m_game_ui;
|
||||
irr_ptr<GUIChatConsole> gui_chat_console;
|
||||
#if BUILD_UI
|
||||
irr_ptr<ui::GUIManagerElem> gui_manager_elem;
|
||||
#endif
|
||||
MapDrawControl *draw_control = nullptr;
|
||||
Camera *camera = nullptr;
|
||||
irr_ptr<Clouds> 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<GUIChatConsole>(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<ui::GUIManagerElem>(
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
};
|
||||
|
|
|
@ -13,6 +13,10 @@
|
|||
#include "client/shadows/dynamicshadowsrender.h"
|
||||
#include <IGUIEnvironment.h>
|
||||
|
||||
#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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
9
src/ui/CMakeLists.txt
Normal file
9
src/ui/CMakeLists.txt
Normal file
|
@ -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
|
||||
)
|
512
src/ui/box.cpp
Normal file
512
src/ui/box.cpp
Normal file
|
@ -0,0 +1,512 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
102
src/ui/box.h
Normal file
102
src/ui/box.h
Normal file
|
@ -0,0 +1,102 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui/helpers.h"
|
||||
#include "ui/style.h"
|
||||
#include "util/basic_macros.h"
|
||||
|
||||
#include <array>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<Box *> m_content;
|
||||
|
||||
Style m_style;
|
||||
std::array<u32, NUM_STATES> 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<Box *> &getContent() const { return m_content; }
|
||||
void setContent(std::vector<Box *> 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();
|
||||
};
|
||||
}
|
105
src/ui/elem.cpp
Normal file
105
src/ui/elem.cpp
Normal file
|
@ -0,0 +1,105 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#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> Elem::create(Type type, Window &window, std::string id)
|
||||
{
|
||||
std::unique_ptr<Elem> elem = nullptr;
|
||||
|
||||
#define CREATE(name, type) \
|
||||
case name: \
|
||||
elem = std::make_unique<type>(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<Box *> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
79
src/ui/elem.h
Normal file
79
src/ui/elem.h
Normal file
|
@ -0,0 +1,79 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui/box.h"
|
||||
#include "ui/helpers.h"
|
||||
#include "util/basic_macros.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<Elem *> m_children;
|
||||
|
||||
Box m_main_box;
|
||||
|
||||
public:
|
||||
static std::unique_ptr<Elem> 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<Elem *> &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);
|
||||
};
|
||||
}
|
515
src/ui/helpers.h
Normal file
515
src/ui/helpers.h
Normal file
|
@ -0,0 +1,515 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "irrlichttypes.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
#include <dimension2d.h>
|
||||
#include <ITexture.h>
|
||||
#include <rect.h>
|
||||
#include <vector2d.h>
|
||||
#include <vector3d.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
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<typename E> struct Pos;
|
||||
template<typename E> struct Size;
|
||||
template<typename E> struct Rect;
|
||||
template<typename E> struct Disp;
|
||||
|
||||
template<typename E>
|
||||
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<typename K>
|
||||
explicit Size(Pos<K> pos) : W(pos.X), H(pos.Y) {}
|
||||
template<typename K>
|
||||
Size(Size<K> other) : W(other.W), H(other.H) {}
|
||||
|
||||
template<typename K>
|
||||
explicit Size(core::vector2d<K> vec) : W(vec.X), H(vec.Y) {}
|
||||
template<typename K>
|
||||
Size(core::dimension2d<K> dim) : W(dim.Width), H(dim.Height) {}
|
||||
|
||||
template<typename K>
|
||||
explicit operator core::vector2d<K>() const { return core::vector2d<K>(W, H); }
|
||||
template<typename K>
|
||||
operator core::dimension2d<K>() const { return core::dimension2d<K>(W, H); }
|
||||
|
||||
E area() const { return W * H; }
|
||||
bool empty() const { return area() == 0; }
|
||||
|
||||
bool operator==(Size<E> other) const { return W == other.W && H == other.H; }
|
||||
bool operator!=(Size<E> other) const { return !(*this == other); }
|
||||
|
||||
E &operator[](int index) { return index ? H : W; }
|
||||
const E &operator[](int index) const { return index ? H : W; }
|
||||
|
||||
Size<E> operator+() const { return Size<E>(+W, +H); }
|
||||
Size<E> operator-() const { return Size<E>(-W, -H); }
|
||||
|
||||
Size<E> operator+(Size<E> other) const { return Size<E>(W + other.W, H + other.H); }
|
||||
Size<E> operator-(Size<E> other) const { return Size<E>(W - other.W, H - other.H); }
|
||||
|
||||
Size<E> &operator+=(Size<E> other) { *this = *this + other; return *this; }
|
||||
Size<E> &operator-=(Size<E> other) { *this = *this - other; return *this; }
|
||||
|
||||
Size<E> operator*(Size<E> other) const { return Size<E>(W * other.W, H * other.H); }
|
||||
Size<E> operator/(Size<E> other) const { return Size<E>(W / other.W, H / other.H); }
|
||||
|
||||
Size<E> &operator*=(Size<E> other) { *this = *this * other; return *this; }
|
||||
Size<E> &operator/=(Size<E> other) { *this = *this / other; return *this; }
|
||||
|
||||
Size<E> operator*(E scalar) const { return Size<E>(W * scalar, H * scalar); }
|
||||
Size<E> operator/(E scalar) const { return Size<E>(W / scalar, H / scalar); }
|
||||
|
||||
Size<E> &operator*=(E scalar) { *this = *this * scalar; return *this; }
|
||||
Size<E> &operator/=(E scalar) { *this = *this / scalar; return *this; }
|
||||
|
||||
Size<E> min(Size<E> other) const
|
||||
{ return Size<E>(std::min(W, other.W), std::min(H, other.H)); }
|
||||
Size<E> max(Size<E> other) const
|
||||
{ return Size<E>(std::max(W, other.W), std::max(H, other.H)); }
|
||||
|
||||
Size<E> clamp(Size<E> lo, Size<E> hi) const
|
||||
{ return Size<E>(std::clamp(W, lo.W, hi.W), std::clamp(H, lo.H, hi.H)); }
|
||||
Size<E> clamp(Disp<E> disp) const
|
||||
{ return clamp(disp.TopLeft, disp.BottomRight); }
|
||||
|
||||
Size<E> clip() const { return max(Size<E>()); }
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, Size<E> size)
|
||||
{
|
||||
os << "(" << size.W << ", " << size.H << ")";
|
||||
return os;
|
||||
}
|
||||
};
|
||||
|
||||
using SizeI = Size<s32>;
|
||||
using SizeU = Size<u32>;
|
||||
using SizeF = Size<f32>;
|
||||
|
||||
template<typename E>
|
||||
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<typename K>
|
||||
Pos(Pos<K> other) : X(other.X), Y(other.Y) {}
|
||||
template<typename K>
|
||||
explicit Pos(Size<K> size) : X(size.W), Y(size.H) {}
|
||||
|
||||
template<typename K>
|
||||
Pos(core::vector2d<K> vec) : X(vec.X), Y(vec.Y) {}
|
||||
template<typename K>
|
||||
explicit Pos(core::dimension2d<K> dim) : X(dim.Width), Y(dim.Height) {}
|
||||
|
||||
template<typename K>
|
||||
operator core::vector2d<K>() const { return core::vector2d<K>(X, Y); }
|
||||
template<typename K>
|
||||
explicit operator core::dimension2d<K>() const { return core::dimension2d<K>(X, Y); }
|
||||
|
||||
bool operator==(Pos<E> other) const { return X == other.X && Y == other.Y; }
|
||||
bool operator!=(Pos<E> other) const { return !(*this == other); }
|
||||
|
||||
E &operator[](int index) { return index ? Y : X; }
|
||||
const E &operator[](int index) const { return index ? Y : X; }
|
||||
|
||||
Pos<E> operator+(Size<E> size) const { return Pos<E>(X + size.W, Y + size.H); }
|
||||
Pos<E> operator-(Size<E> size) const { return Pos<E>(X - size.W, Y - size.H); }
|
||||
|
||||
Pos<E> &operator+=(Size<E> size) { *this = *this + size; return *this; }
|
||||
Pos<E> &operator-=(Size<E> size) { *this = *this - size; return *this; }
|
||||
|
||||
Pos<E> operator*(Size<E> size) const { return Pos<E>(X * size.W, Y * size.H); }
|
||||
Pos<E> operator/(Size<E> size) const { return Pos<E>(X / size.W, Y / size.H); }
|
||||
|
||||
Pos<E> &operator*=(Size<E> size) { *this = *this * size; return *this; }
|
||||
Pos<E> &operator/=(Size<E> size) { *this = *this / size; return *this; }
|
||||
|
||||
Pos<E> operator*(E scalar) const { return Pos<E>(X * scalar, Y * scalar); }
|
||||
Pos<E> operator/(E scalar) const { return Pos<E>(X / scalar, Y / scalar); }
|
||||
|
||||
Pos<E> &operator*=(E scalar) { *this = *this * scalar; return *this; }
|
||||
Pos<E> &operator/=(E scalar) { *this = *this / scalar; return *this; }
|
||||
|
||||
Size<E> operator-(Pos<E> other) const { return Size<E>(X - other.X, Y - other.Y); }
|
||||
Size<E> operator/(Pos<E> other) const { return Size<E>(X / other.X, Y / other.Y); }
|
||||
|
||||
Pos<E> min(Pos<E> other) const
|
||||
{ return Pos<E>(std::min(X, other.X), std::min(Y, other.Y)); }
|
||||
Pos<E> max(Pos<E> other) const
|
||||
{ return Pos<E>(std::max(X, other.X), std::max(Y, other.Y)); }
|
||||
|
||||
Pos<E> clamp(Pos<E> lo, Pos<E> hi) const
|
||||
{ return Pos<E>(std::clamp(X, lo.X, hi.X), std::clamp(Y, lo.Y, hi.Y)); }
|
||||
Pos<E> clamp(Rect<E> rect) const
|
||||
{ return clamp(rect.TopLeft, rect.BottomRight); }
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, Pos<E> pos)
|
||||
{
|
||||
os << "(" << pos.X << ", " << pos.Y << ")";
|
||||
return os;
|
||||
}
|
||||
};
|
||||
|
||||
using PosI = Pos<s32>;
|
||||
using PosU = Pos<u32>;
|
||||
using PosF = Pos<f32>;
|
||||
|
||||
template<typename E>
|
||||
struct Disp
|
||||
{
|
||||
union {
|
||||
struct {
|
||||
E L;
|
||||
E T;
|
||||
};
|
||||
Size<E> TopLeft;
|
||||
};
|
||||
union {
|
||||
struct {
|
||||
E R;
|
||||
E B;
|
||||
};
|
||||
Size<E> 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<E> size) : TopLeft(size), BottomRight(size) {}
|
||||
Disp(Size<E> tl, Size<E> br) : TopLeft(tl), BottomRight(br) {}
|
||||
|
||||
template<typename K>
|
||||
explicit Disp(Rect<K> rect) : TopLeft(rect.TopLeft), BottomRight(rect.BottomRight) {}
|
||||
template<typename K>
|
||||
Disp(Disp<K> other) : TopLeft(other.TopLeft), BottomRight(other.BottomRight) {}
|
||||
|
||||
template<typename K>
|
||||
explicit Disp(core::rect<K> rect) :
|
||||
TopLeft(rect.UpperLeftCorner), BottomRight(rect.LowerRightCorner) {}
|
||||
|
||||
template<typename K>
|
||||
explicit operator core::rect<K>() const { return core::rect<K>(Rect<K>(*this)); }
|
||||
|
||||
E X() const { return L + R; }
|
||||
E Y() const { return T + B; }
|
||||
Size<E> extents() const { return TopLeft + BottomRight; }
|
||||
|
||||
bool operator==(Disp<E> other) const
|
||||
{ return TopLeft == other.TopLeft && BottomRight == other.BottomRight; }
|
||||
bool operator!=(Disp<E> other) const { return !(*this == other); }
|
||||
|
||||
Disp<E> operator+() const { return Disp<E>(+TopLeft, +BottomRight); }
|
||||
Disp<E> operator-() const { return Disp<E>(-TopLeft, -BottomRight); }
|
||||
|
||||
Disp<E> operator+(Disp<E> other) const
|
||||
{ return Disp<E>(TopLeft + other.TopLeft, BottomRight + other.BottomRight); }
|
||||
Disp<E> operator-(Disp<E> other) const
|
||||
{ return Disp<E>(TopLeft - other.TopLeft, BottomRight - other.BottomRight); }
|
||||
|
||||
Disp<E> &operator+=(Disp<E> other) { *this = *this + other; return *this; }
|
||||
Disp<E> &operator-=(Disp<E> other) { *this = *this - other; return *this; }
|
||||
|
||||
Disp<E> operator*(Disp<E> other) const
|
||||
{ return Disp<E>(TopLeft * other.TopLeft, BottomRight * other.BottomRight); }
|
||||
Disp<E> operator/(Disp<E> other) const
|
||||
{ return Disp<E>(TopLeft / other.TopLeft, BottomRight / other.BottomRight); }
|
||||
|
||||
Disp<E> &operator*=(Disp<E> other) { *this = *this * other; return *this; }
|
||||
Disp<E> &operator/=(Disp<E> other) { *this = *this / other; return *this; }
|
||||
|
||||
Disp<E> operator*(E scalar) const
|
||||
{ return Disp<E>(TopLeft * scalar, BottomRight * scalar); }
|
||||
Disp<E> operator/(E scalar) const
|
||||
{ return Disp<E>(TopLeft / scalar, BottomRight / scalar); }
|
||||
|
||||
Disp<E> &operator*=(E scalar) { *this = *this * scalar; return *this; }
|
||||
Disp<E> &operator/=(E scalar) { *this = *this / scalar; return *this; }
|
||||
|
||||
Disp<E> clip() const { return Disp<E>(TopLeft.clip(), BottomRight.clip()); }
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, Disp<E> disp)
|
||||
{
|
||||
os << "(" << disp.L << ", " << disp.T << ", " << disp.R << ", " << disp.B << ")";
|
||||
return os;
|
||||
}
|
||||
};
|
||||
|
||||
using DispI = Disp<s32>;
|
||||
using DispU = Disp<u32>;
|
||||
using DispF = Disp<f32>;
|
||||
|
||||
template<typename E>
|
||||
struct Rect
|
||||
{
|
||||
union {
|
||||
struct {
|
||||
E L;
|
||||
E T;
|
||||
};
|
||||
Pos<E> TopLeft;
|
||||
};
|
||||
union {
|
||||
struct {
|
||||
E R;
|
||||
E B;
|
||||
};
|
||||
Pos<E> 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<E> pos) : TopLeft(pos), BottomRight(pos) {}
|
||||
Rect(Pos<E> tl, Pos<E> br) : TopLeft(tl), BottomRight(br) {}
|
||||
|
||||
explicit Rect(Size<E> size) : TopLeft(), BottomRight(size) {}
|
||||
Rect(Pos<E> pos, Size<E> size) : TopLeft(pos), BottomRight(pos + size) {}
|
||||
|
||||
template<typename K>
|
||||
Rect(Rect<K> other) : TopLeft(other.TopLeft), BottomRight(other.BottomRight) {}
|
||||
template<typename K>
|
||||
explicit Rect(Disp<K> disp) : TopLeft(disp.TopLeft), BottomRight(disp.BottomRight) {}
|
||||
|
||||
template<typename K>
|
||||
Rect(core::rect<K> rect) :
|
||||
TopLeft(rect.UpperLeftCorner), BottomRight(rect.LowerRightCorner) {}
|
||||
|
||||
template<typename K>
|
||||
operator core::rect<K>() const { return core::rect<K>(TopLeft, BottomRight); }
|
||||
|
||||
E W() const { return R - L; }
|
||||
E H() const { return B - T; }
|
||||
Size<E> size() const { return BottomRight - TopLeft; }
|
||||
|
||||
E area() const { return size().area(); }
|
||||
bool empty() const { return size().empty(); }
|
||||
|
||||
bool operator==(Rect<E> other) const
|
||||
{ return TopLeft == other.TopLeft && BottomRight == other.BottomRight; }
|
||||
bool operator!=(Rect<E> other) const { return !(*this == other); }
|
||||
|
||||
Rect<E> operator+(Disp<E> disp) const
|
||||
{ return Rect<E>(TopLeft + disp.TopLeft, BottomRight + disp.BottomRight); }
|
||||
Rect<E> operator-(Disp<E> disp) const
|
||||
{ return Rect<E>(TopLeft - disp.TopLeft, BottomRight - disp.BottomRight); }
|
||||
|
||||
Rect<E> &operator+=(Disp<E> disp) { *this = *this + disp; return *this; }
|
||||
Rect<E> &operator-=(Disp<E> disp) { *this = *this - disp; return *this; }
|
||||
|
||||
Rect<E> operator*(Disp<E> disp) const
|
||||
{ return Rect<E>(TopLeft * disp.TopLeft, BottomRight * disp.BottomRight); }
|
||||
Rect<E> operator/(Disp<E> disp) const
|
||||
{ return Rect<E>(TopLeft / disp.TopLeft, BottomRight / disp.BottomRight); }
|
||||
|
||||
Rect<E> &operator*=(Disp<E> disp) { *this = *this * disp; return *this; }
|
||||
Rect<E> &operator/=(Disp<E> disp) { *this = *this / disp; return *this; }
|
||||
|
||||
Rect<E> operator*(E scalar) const
|
||||
{ return Rect<E>(TopLeft * scalar, BottomRight * scalar); }
|
||||
Rect<E> operator/(E scalar) const
|
||||
{ return Rect<E>(TopLeft / scalar, BottomRight / scalar); }
|
||||
|
||||
Rect<E> &operator*=(E scalar) { *this = *this * scalar; return *this; }
|
||||
Rect<E> &operator/=(E scalar) { *this = *this / scalar; return *this; }
|
||||
|
||||
Disp<E> operator-(Rect<E> other) const
|
||||
{ return Disp<E>(TopLeft - other.TopLeft, BottomRight - other.BottomRight); }
|
||||
Disp<E> operator/(Rect<E> other) const
|
||||
{ return Disp<E>(TopLeft / other.TopLeft, BottomRight / other.BottomRight); }
|
||||
|
||||
Rect<E> insetBy(Disp<E> disp) const
|
||||
{ return Rect<E>(TopLeft + disp.TopLeft, BottomRight - disp.BottomRight); }
|
||||
Rect<E> outsetBy(Disp<E> disp) const
|
||||
{ return Rect<E>(TopLeft - disp.TopLeft, BottomRight + disp.BottomRight); }
|
||||
|
||||
Rect<E> unionWith(Rect<E> other) const
|
||||
{ return Rect<E>(TopLeft.min(other.TopLeft), BottomRight.max(other.BottomRight)); }
|
||||
Rect<E> intersectWith(Rect<E> other) const
|
||||
{ return Rect<E>(TopLeft.max(other.TopLeft), BottomRight.min(other.BottomRight)); }
|
||||
|
||||
Rect<E> clip() const { return Rect<E>(TopLeft, size().clip()); }
|
||||
|
||||
bool contains(Pos<E> pos) const
|
||||
{ return pos.X >= L && pos.Y >= T && pos.X < R && pos.Y < B; }
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, Rect<E> rect)
|
||||
{
|
||||
os << "(" << rect.L << ", " << rect.T << ", " << rect.R << ", " << rect.B << ")";
|
||||
return os;
|
||||
}
|
||||
};
|
||||
|
||||
using RectI = Rect<s32>;
|
||||
using RectU = Rect<u32>;
|
||||
using RectF = Rect<f32>;
|
||||
|
||||
// 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); }
|
||||
}
|
123
src/ui/manager.cpp
Normal file
123
src/ui/manager.cpp
Normal file
|
@ -0,0 +1,123 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#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;
|
||||
}
|
85
src/ui/manager.h
Normal file
85
src/ui/manager.h
Normal file
|
@ -0,0 +1,85 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui/helpers.h"
|
||||
#include "ui/window.h"
|
||||
#include "util/basic_macros.h"
|
||||
|
||||
#include <IGUIElement.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
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<u64, Window> 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();
|
||||
}
|
||||
};
|
||||
}
|
33
src/ui/static_elems.cpp
Normal file
33
src/ui/static_elems.cpp
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#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()});
|
||||
}
|
||||
}
|
34
src/ui/static_elems.h
Normal file
34
src/ui/static_elems.h
Normal file
|
@ -0,0 +1,34 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui/box.h"
|
||||
#include "ui/elem.h"
|
||||
#include "ui/helpers.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
185
src/ui/style.cpp
Normal file
185
src/ui/style.cpp
Normal file
|
@ -0,0 +1,185 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
126
src/ui/style.h
Normal file
126
src/ui/style.h
Normal file
|
@ -0,0 +1,126 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui/helpers.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
310
src/ui/window.cpp
Normal file
310
src/ui/window.cpp
Normal file
|
@ -0,0 +1,310 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#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 *, std::string> 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 *, std::string> &elem_contents)
|
||||
{
|
||||
// Read in all the new elements and updates to existing elements.
|
||||
u32 num_elems = readU32(is);
|
||||
|
||||
std::unordered_map<std::string, std::unique_ptr<Elem>> 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> 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 *>(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 *, std::string> &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;
|
||||
}
|
||||
}
|
96
src/ui/window.h
Normal file
96
src/ui/window.h
Normal file
|
@ -0,0 +1,96 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui/elem.h"
|
||||
#include "ui/helpers.h"
|
||||
#include "util/basic_macros.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
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<std::string, std::unique_ptr<Elem>> m_elems;
|
||||
std::vector<Elem *> m_ordered_elems;
|
||||
|
||||
Root *m_root_elem;
|
||||
|
||||
std::vector<std::string> 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<Elem *> &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 *, std::string> &elem_contents);
|
||||
bool readRootElem(std::istream &is);
|
||||
void readStyles(std::istream &is);
|
||||
|
||||
bool updateElems(std::unordered_map<Elem *, std::string> &elem_contents);
|
||||
bool updateTree(Elem *elem, size_t depth);
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue