mirror of
https://github.com/luanti-org/luanti.git
synced 2025-08-01 17:38:41 +00:00
Create UI event handling infrastructure
This commit is contained in:
parent
9cc73f16f0
commit
bb2f857b04
27 changed files with 1060 additions and 49 deletions
|
@ -141,3 +141,47 @@ core.register_on_leaveplayer(function(player)
|
|||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local WINDOW_EVENT = 0x00
|
||||
local ELEM_EVENT = 0x01
|
||||
|
||||
function core.receive_ui_message(player, data)
|
||||
local action, id, code, rest = ui._decode("BLB Z", data, -1)
|
||||
|
||||
-- Discard events for any window that isn't currently open, since it's
|
||||
-- probably due to network latency and events coming late.
|
||||
local context = open_contexts[id]
|
||||
if not context then
|
||||
core.log("info", "Window " .. id .. " is not open")
|
||||
return
|
||||
end
|
||||
|
||||
-- If the player doesn't match up with what we expected, ignore the
|
||||
-- (probably malicious) event.
|
||||
if context:get_player() ~= player then
|
||||
core.log("action", "Window " .. id .. " has player '" .. context:get_player() ..
|
||||
"', but received event from player '" .. player .. "'")
|
||||
return
|
||||
end
|
||||
|
||||
-- No events should ever fire for non-GUI windows.
|
||||
if context._window._type ~= "gui" then
|
||||
core.log("info", "Non-GUI window received event: " .. code)
|
||||
return
|
||||
end
|
||||
|
||||
-- Prepare the basic event table shared by all events.
|
||||
local ev = {
|
||||
context = context,
|
||||
player = context:get_player(),
|
||||
state = context:get_state(),
|
||||
}
|
||||
|
||||
if action == WINDOW_EVENT then
|
||||
context._window:_on_window_event(code, ev, rest)
|
||||
elseif action == ELEM_EVENT then
|
||||
context._window:_on_elem_event(code, ev, rest)
|
||||
else
|
||||
core.log("info", "Invalid window action: " .. action)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ function ui._new_type(base, type, type_id, id_required)
|
|||
class._type = type
|
||||
class._type_id = type_id
|
||||
class._id_required = id_required
|
||||
class._handlers = setmetatable({}, {__index = base and base._handlers})
|
||||
|
||||
ui._elem_types[type] = class
|
||||
|
||||
|
@ -138,3 +139,19 @@ function ui.Elem:_encode_box(fl, box)
|
|||
|
||||
ui._encode_flag(fl, "s", ui._encode_flags(box_fl))
|
||||
end
|
||||
|
||||
function ui.Elem:_on_event(code, ev, data)
|
||||
-- Get the handler function for this event if we recognize it.
|
||||
local handler = self._handlers[code]
|
||||
if not handler then
|
||||
core.log("info", "Invalid event for " .. self._type_id .. ": " .. code)
|
||||
return
|
||||
end
|
||||
|
||||
-- If the event handler returned a callback function for the user, call it
|
||||
-- with the event table.
|
||||
local callback = handler(self, ev, data)
|
||||
if callback then
|
||||
callback(ev)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,6 +28,13 @@ function ui.Window:_init(props)
|
|||
|
||||
self._root = ui._req(props.root, ui.Root)
|
||||
|
||||
self._focused = ui._opt(props.focused, "string")
|
||||
self._allow_close = ui._opt(props.allow_close, "boolean", true)
|
||||
|
||||
self._on_close = ui._opt(props.on_close, "function")
|
||||
self._on_submit = ui._opt(props.on_submit, "function")
|
||||
self._on_focus_change = ui._opt(props.on_focus_change, "function")
|
||||
|
||||
self._context = nil -- Set by ui.Context
|
||||
|
||||
self._elems = self._root:_get_flat()
|
||||
|
@ -43,6 +50,11 @@ function ui.Window:_init(props)
|
|||
elem._window = self
|
||||
end
|
||||
|
||||
if self._focused and self._focused ~= "" then
|
||||
assert(self._elems_by_id[self._focused],
|
||||
"Invalid focused element: '" .. self._focused .. "'")
|
||||
end
|
||||
|
||||
for _, item in ipairs(props) do
|
||||
ui._req(item, ui.Style)
|
||||
end
|
||||
|
@ -52,12 +64,22 @@ function ui.Window:_encode(player, opening)
|
|||
local enc_styles = self:_encode_styles()
|
||||
local enc_elems = self:_encode_elems()
|
||||
|
||||
local fl = ui._make_flags()
|
||||
|
||||
if ui._shift_flag(fl, self._focused) then
|
||||
ui._encode_flag(fl, "z", self._focused)
|
||||
end
|
||||
ui._shift_flag(fl, opening and self._allow_close)
|
||||
|
||||
ui._shift_flag(fl, self._on_submit)
|
||||
ui._shift_flag(fl, self._on_focus_change)
|
||||
|
||||
local data = ui._encode("ZzZ", enc_elems, self._root._id, enc_styles)
|
||||
if opening then
|
||||
data = ui._encode("ZB", data, ui._window_types[self._type])
|
||||
end
|
||||
|
||||
return data
|
||||
return ui._encode("ZZ", data, ui._encode_flags(fl))
|
||||
end
|
||||
|
||||
function ui.Window:_encode_styles()
|
||||
|
@ -188,3 +210,74 @@ function ui.Window:_encode_elems()
|
|||
|
||||
return ui._encode_array("Z", enc_elems)
|
||||
end
|
||||
|
||||
function ui.Window:_on_window_event(code, ev, data)
|
||||
-- Get the handler function for this event if we recognize it.
|
||||
local handler = self._handlers[code]
|
||||
if not handler then
|
||||
core.log("info", "Invalid window event: " .. code)
|
||||
return
|
||||
end
|
||||
|
||||
-- If the event handler returned a callback function for the user, call it
|
||||
-- with the event table.
|
||||
local callback = handler(self, ev, data)
|
||||
if callback then
|
||||
callback(ev)
|
||||
end
|
||||
end
|
||||
|
||||
function ui.Window:_on_elem_event(code, ev, data)
|
||||
local type_id, target, rest = ui._decode("BzZ", data, -1)
|
||||
ev.target = target
|
||||
|
||||
-- Get the element for this ID. If it doesn't exist or has a different
|
||||
-- type, the window probably updated before receiving this event.
|
||||
local elem = self._elems_by_id[target]
|
||||
if not elem then
|
||||
core.log("info", "Dropped event for non-existent element '" .. target .. "'")
|
||||
return
|
||||
elseif elem._type_id ~= type_id then
|
||||
core.log("info", "Dropped event with type " .. type_id ..
|
||||
" sent to element with type " .. elem._type_id)
|
||||
return
|
||||
end
|
||||
|
||||
-- Pass the event and data to the element for further processing.
|
||||
elem:_on_event(code, ev, rest)
|
||||
end
|
||||
|
||||
ui.Window._handlers = {}
|
||||
|
||||
ui.Window._handlers[0x00] = function(self, ev, data)
|
||||
-- We should never receive an event for an uncloseable window. If we
|
||||
-- did, this player might be trying to cheat.
|
||||
if not self._allow_close then
|
||||
core.log("action", "Player '" .. self._context:get_player() ..
|
||||
"' closed uncloseable window")
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Since the window is now closed, remove the open window data.
|
||||
self._context:_close_window()
|
||||
return self._on_close
|
||||
end
|
||||
|
||||
ui.Window._handlers[0x01] = function(self, ev, data)
|
||||
return self._on_submit
|
||||
end
|
||||
|
||||
ui.Window._handlers[0x02] = function(self, ev, data)
|
||||
ev.unfocused, ev.focused = ui._decode("zz", data)
|
||||
|
||||
-- If the ID for either element doesn't exist, we probably updated the
|
||||
-- window to remove the element. Assume nothing is focused then.
|
||||
if not self._elems_by_id[ev.unfocused] then
|
||||
ev.unfocused = ""
|
||||
end
|
||||
if not self._elems_by_id[ev.focused] then
|
||||
ev.focused = ""
|
||||
end
|
||||
|
||||
return self._on_focus_change
|
||||
end
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
#include "Keycodes.h"
|
||||
#include "irrString.h"
|
||||
|
||||
union SDL_Event;
|
||||
|
||||
namespace irr
|
||||
{
|
||||
//! Enumeration for all event types there are.
|
||||
|
@ -82,6 +84,11 @@ enum EEVENT_TYPE
|
|||
//! Application state events like a resume, pause etc.
|
||||
EET_APPLICATION_EVENT,
|
||||
|
||||
//! Any other raw SDL event that doesn't fall into the above categories. In
|
||||
// this case, only SEvent::SdlEvent contains valid information. Note that
|
||||
// SEvent::SdlEvent is non-nullptr for most other event types as well.
|
||||
EET_OTHER_SDL_EVENT,
|
||||
|
||||
//! This enum is never used, it only forces the compiler to
|
||||
//! compile these enumeration values to 32 bit.
|
||||
EGUIET_FORCE_32_BIT = 0x7fffffff
|
||||
|
@ -526,6 +533,7 @@ struct SEvent
|
|||
};
|
||||
|
||||
EEVENT_TYPE EventType;
|
||||
union SDL_Event *SdlEvent = nullptr;
|
||||
union
|
||||
{
|
||||
struct SGUIEvent GUIEvent;
|
||||
|
|
|
@ -714,6 +714,10 @@ bool CIrrDeviceSDL::run()
|
|||
// os::Printer::log("event: ", core::stringc((int)SDL_event.type).c_str(), ELL_INFORMATION); // just for debugging
|
||||
irrevent = {};
|
||||
|
||||
// Initially, we assume that there is no other applicable event type.
|
||||
irrevent.EventType = irr::EET_OTHER_SDL_EVENT;
|
||||
irrevent.SdlEvent = &SDL_event;
|
||||
|
||||
switch (SDL_event.type) {
|
||||
case SDL_MOUSEMOTION: {
|
||||
SDL_Keymod keymod = SDL_GetModState();
|
||||
|
@ -736,31 +740,28 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.MouseInput.ButtonStates = MouseButtonStates;
|
||||
irrevent.MouseInput.Shift = (keymod & KMOD_SHIFT) != 0;
|
||||
irrevent.MouseInput.Control = (keymod & KMOD_CTRL) != 0;
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
break;
|
||||
}
|
||||
case SDL_MOUSEWHEEL: {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
double wheel = SDL_event.wheel.preciseY;
|
||||
#else
|
||||
double wheel = SDL_event.wheel.y;
|
||||
#endif
|
||||
// wheel y can be 0 if scrolling sideways
|
||||
if (wheel == 0.0f)
|
||||
break;
|
||||
|
||||
SDL_Keymod keymod = SDL_GetModState();
|
||||
|
||||
irrevent.EventType = irr::EET_MOUSE_INPUT_EVENT;
|
||||
irrevent.MouseInput.Event = irr::EMIE_MOUSE_WHEEL;
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
irrevent.MouseInput.Wheel = SDL_event.wheel.preciseY;
|
||||
#else
|
||||
irrevent.MouseInput.Wheel = SDL_event.wheel.y;
|
||||
#endif
|
||||
irrevent.MouseInput.Wheel = wheel;
|
||||
irrevent.MouseInput.ButtonStates = MouseButtonStates;
|
||||
irrevent.MouseInput.Shift = (keymod & KMOD_SHIFT) != 0;
|
||||
irrevent.MouseInput.Control = (keymod & KMOD_CTRL) != 0;
|
||||
irrevent.MouseInput.X = MouseX;
|
||||
irrevent.MouseInput.Y = MouseY;
|
||||
|
||||
// wheel y can be 0 if scrolling sideways
|
||||
if (irrevent.MouseInput.Wheel == 0.0f)
|
||||
break;
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
break;
|
||||
}
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
|
@ -854,16 +855,22 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.MouseInput.Y = static_cast<s32>(SDL_event.button.y * ScaleY);
|
||||
irrevent.MouseInput.Shift = shift;
|
||||
irrevent.MouseInput.Control = control;
|
||||
postEventFromUser(irrevent);
|
||||
|
||||
if (irrevent.MouseInput.Event >= EMIE_LMOUSE_PRESSED_DOWN && irrevent.MouseInput.Event <= EMIE_MMOUSE_PRESSED_DOWN) {
|
||||
u32 clicks = checkSuccessiveClicks(irrevent.MouseInput.X, irrevent.MouseInput.Y, irrevent.MouseInput.Event);
|
||||
if (clicks == 2) {
|
||||
// Since we need to send two events, explicitly send the first
|
||||
// event and clear out the SdlEvent field for the second event so
|
||||
// we don't get duplicate SDL events.
|
||||
postEventFromUser(irrevent);
|
||||
irrevent.SdlEvent = nullptr;
|
||||
|
||||
irrevent.MouseInput.Event = (EMOUSE_INPUT_EVENT)(EMIE_LMOUSE_DOUBLE_CLICK + irrevent.MouseInput.Event - EMIE_LMOUSE_PRESSED_DOWN);
|
||||
postEventFromUser(irrevent);
|
||||
} else if (clicks == 3) {
|
||||
irrevent.MouseInput.Event = (EMOUSE_INPUT_EVENT)(EMIE_LMOUSE_TRIPLE_CLICK + irrevent.MouseInput.Event - EMIE_LMOUSE_PRESSED_DOWN);
|
||||
postEventFromUser(irrevent);
|
||||
irrevent.SdlEvent = nullptr;
|
||||
|
||||
irrevent.MouseInput.Event = (EMOUSE_INPUT_EVENT)(EMIE_LMOUSE_TRIPLE_CLICK + irrevent.MouseInput.Event - EMIE_LMOUSE_PRESSED_DOWN);
|
||||
}
|
||||
}
|
||||
} else if (irrevent.EventType == irr::EET_KEY_INPUT_EVENT) {
|
||||
|
@ -871,7 +878,6 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.KeyInput.PressedDown = SDL_event.type == SDL_MOUSEBUTTONDOWN;
|
||||
irrevent.KeyInput.Shift = shift;
|
||||
irrevent.KeyInput.Control = control;
|
||||
postEventFromUser(irrevent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -880,9 +886,6 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.EventType = irr::EET_STRING_INPUT_EVENT;
|
||||
irrevent.StringInput.Str = new core::stringw();
|
||||
irr::core::utf8ToWString(*irrevent.StringInput.Str, SDL_event.text.text);
|
||||
postEventFromUser(irrevent);
|
||||
delete irrevent.StringInput.Str;
|
||||
irrevent.StringInput.Str = NULL;
|
||||
} break;
|
||||
|
||||
case SDL_KEYDOWN:
|
||||
|
@ -914,8 +917,6 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.KeyInput.Char = findCharToPassToIrrlicht(keysym, key,
|
||||
(SDL_event.key.keysym.mod & KMOD_NUM) != 0);
|
||||
irrevent.KeyInput.SystemKeyCode = scancode;
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
} break;
|
||||
|
||||
case SDL_QUIT:
|
||||
|
@ -939,7 +940,6 @@ bool CIrrDeviceSDL::run()
|
|||
if (old_scale_x != ScaleX || old_scale_y != ScaleY) {
|
||||
irrevent.EventType = EET_APPLICATION_EVENT;
|
||||
irrevent.ApplicationEvent.EventType = EAET_DPI_CHANGED;
|
||||
postEventFromUser(irrevent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -949,8 +949,6 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.EventType = irr::EET_USER_EVENT;
|
||||
irrevent.UserEvent.UserData1 = reinterpret_cast<uintptr_t>(SDL_event.user.data1);
|
||||
irrevent.UserEvent.UserData2 = reinterpret_cast<uintptr_t>(SDL_event.user.data2);
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
break;
|
||||
|
||||
case SDL_FINGERDOWN:
|
||||
|
@ -961,8 +959,6 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.TouchInput.Y = static_cast<s32>(SDL_event.tfinger.y * Height);
|
||||
CurrentTouchCount++;
|
||||
irrevent.TouchInput.touchedCount = CurrentTouchCount;
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
break;
|
||||
|
||||
case SDL_FINGERMOTION:
|
||||
|
@ -972,8 +968,6 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.TouchInput.X = static_cast<s32>(SDL_event.tfinger.x * Width);
|
||||
irrevent.TouchInput.Y = static_cast<s32>(SDL_event.tfinger.y * Height);
|
||||
irrevent.TouchInput.touchedCount = CurrentTouchCount;
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
break;
|
||||
|
||||
case SDL_FINGERUP:
|
||||
|
@ -988,8 +982,6 @@ bool CIrrDeviceSDL::run()
|
|||
if (CurrentTouchCount > 0) {
|
||||
CurrentTouchCount--;
|
||||
}
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
break;
|
||||
|
||||
// Contrary to what the SDL documentation says, SDL_APP_WILLENTERBACKGROUND
|
||||
|
@ -1017,6 +1009,14 @@ bool CIrrDeviceSDL::run()
|
|||
default:
|
||||
break;
|
||||
} // end switch
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
|
||||
if (SDL_event.type == SDL_TEXTINPUT) {
|
||||
delete irrevent.StringInput.Str;
|
||||
irrevent.StringInput.Str = nullptr;
|
||||
}
|
||||
|
||||
resetReceiveTextInputEvents();
|
||||
} // end while
|
||||
|
||||
|
|
|
@ -1288,6 +1288,14 @@ void Client::sendInventoryFields(const std::string &formname,
|
|||
Send(&pkt);
|
||||
}
|
||||
|
||||
void Client::sendUiMessage(const char *data, size_t len)
|
||||
{
|
||||
NetworkPacket pkt(TOSERVER_UI_MESSAGE, 0);
|
||||
pkt.putRawString(data, len);
|
||||
|
||||
Send(&pkt);
|
||||
}
|
||||
|
||||
void Client::sendInventoryAction(InventoryAction *a)
|
||||
{
|
||||
std::ostringstream os(std::ios_base::binary);
|
||||
|
|
|
@ -231,6 +231,7 @@ public:
|
|||
const StringMap &fields);
|
||||
void sendInventoryFields(const std::string &formname,
|
||||
const StringMap &fields);
|
||||
void sendUiMessage(const char *data, size_t len);
|
||||
void sendInventoryAction(InventoryAction *a);
|
||||
void sendChatMessage(const std::wstring &message);
|
||||
void clearOutChatQueue();
|
||||
|
|
|
@ -163,7 +163,7 @@ void GameUI::update(const RunStats &stats, Client *client, MapDrawControl *draw_
|
|||
m_guitext2->setVisible(m_flags.show_basic_debug);
|
||||
|
||||
setStaticText(m_guitext_info, m_infotext.c_str());
|
||||
m_guitext_info->setVisible(m_flags.show_hud && g_menumgr.menuCount() == 0);
|
||||
m_guitext_info->setVisible(m_flags.show_hud && !isMenuActive());
|
||||
|
||||
static const float statustext_time_max = 1.5f;
|
||||
|
||||
|
|
|
@ -7,12 +7,17 @@
|
|||
/*
|
||||
All kinds of stuff that needs to be exposed from main.cpp
|
||||
*/
|
||||
#include "config.h"
|
||||
#include "modalMenu.h"
|
||||
#include <cassert>
|
||||
#include <list>
|
||||
|
||||
#include "IGUIEnvironment.h"
|
||||
|
||||
#if BUILD_UI
|
||||
#include "ui/manager.h"
|
||||
#endif
|
||||
|
||||
namespace irr::gui {
|
||||
class IGUIStaticText;
|
||||
}
|
||||
|
@ -67,10 +72,16 @@ public:
|
|||
// Returns true to prevent further processing
|
||||
virtual bool preprocessEvent(const SEvent& event)
|
||||
{
|
||||
if (m_stack.empty())
|
||||
return false;
|
||||
GUIModalMenu *mm = dynamic_cast<GUIModalMenu*>(m_stack.back());
|
||||
return mm && mm->preprocessEvent(event);
|
||||
if (!m_stack.empty()) {
|
||||
GUIModalMenu *mm = dynamic_cast<GUIModalMenu*>(m_stack.back());
|
||||
return mm && mm->preprocessEvent(event);
|
||||
#if BUILD_UI
|
||||
} else if (ui::g_manager.isFocused() && event.SdlEvent != nullptr) {
|
||||
return ui::g_manager.processInput(*event.SdlEvent);
|
||||
#endif
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t menuCount() const
|
||||
|
@ -109,7 +120,11 @@ extern MainMenuManager g_menumgr;
|
|||
|
||||
static inline bool isMenuActive()
|
||||
{
|
||||
#if BUILD_UI
|
||||
return g_menumgr.menuCount() != 0 || ui::g_manager.isFocused();
|
||||
#else
|
||||
return g_menumgr.menuCount() != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
class MainGameCallback : public IGameCallback
|
||||
|
|
|
@ -188,7 +188,7 @@ const ServerCommandFactory serverCommandFactoryTable[TOSERVER_NUM_MSG_TYPES] =
|
|||
{ "TOSERVER_REMOVED_SOUNDS", 2, true }, // 0x3a
|
||||
{ "TOSERVER_NODEMETA_FIELDS", 0, true }, // 0x3b
|
||||
{ "TOSERVER_INVENTORY_FIELDS", 0, true }, // 0x3c
|
||||
null_command_factory, // 0x3d
|
||||
{ "TOSERVER_UI_MESSAGE", 0, true }, // 0x3d
|
||||
null_command_factory, // 0x3e
|
||||
null_command_factory, // 0x3f
|
||||
{ "TOSERVER_REQUEST_MEDIA", 1, true }, // 0x40
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
[scheduled bump for 5.10.0]
|
||||
PROTOCOL VERSION 47:
|
||||
Add particle blend mode "clip"
|
||||
Add TOCLIENT_UI_MESSAGE
|
||||
Add TOCLIENT_UI_MESSAGE and TOSERVER_UI_MESSAGE
|
||||
[scheduled bump for 5.11.0]
|
||||
PROTOCOL VERSION 48
|
||||
Add compression to some existing packets
|
||||
|
|
|
@ -838,6 +838,11 @@ enum ToServerCommand : u16
|
|||
u8[len] field value
|
||||
*/
|
||||
|
||||
TOSERVER_UI_MESSAGE = 0x3d,
|
||||
/*
|
||||
Variable-length structure that changes depending on the message type.
|
||||
*/
|
||||
|
||||
TOSERVER_REQUEST_MEDIA = 0x40,
|
||||
/*
|
||||
u16 number of files requested
|
||||
|
|
|
@ -72,7 +72,7 @@ const ToServerCommandHandler toServerCommandTable[TOSERVER_NUM_MSG_TYPES] =
|
|||
{ "TOSERVER_REMOVED_SOUNDS", TOSERVER_STATE_INGAME, &Server::handleCommand_RemovedSounds }, // 0x3a
|
||||
{ "TOSERVER_NODEMETA_FIELDS", TOSERVER_STATE_INGAME, &Server::handleCommand_NodeMetaFields }, // 0x3b
|
||||
{ "TOSERVER_INVENTORY_FIELDS", TOSERVER_STATE_INGAME, &Server::handleCommand_InventoryFields }, // 0x3c
|
||||
null_command_handler, // 0x3d
|
||||
{ "TOSERVER_UI_MESSAGE", TOSERVER_STATE_INGAME, &Server::handleCommand_UiMessage }, // 0x3d
|
||||
null_command_handler, // 0x3e
|
||||
null_command_handler, // 0x3f
|
||||
{ "TOSERVER_REQUEST_MEDIA", TOSERVER_STATE_STARTUP, &Server::handleCommand_RequestMedia }, // 0x40
|
||||
|
|
|
@ -1422,6 +1422,15 @@ void Server::handleCommand_InventoryFields(NetworkPacket* pkt)
|
|||
actionstream << ", possible exploitation attempt" << std::endl;
|
||||
}
|
||||
|
||||
void Server::handleCommand_UiMessage(NetworkPacket* pkt)
|
||||
{
|
||||
session_t peer_id = pkt->getPeerId();
|
||||
RemoteClient *client = getClient(peer_id, CS_Invalid);
|
||||
|
||||
std::string data(pkt->getString(0), pkt->getSize());
|
||||
m_script->receive_ui_message(client->getName().c_str(), data);
|
||||
}
|
||||
|
||||
void Server::handleCommand_FirstSrp(NetworkPacket* pkt)
|
||||
{
|
||||
session_t peer_id = pkt->getPeerId();
|
||||
|
|
|
@ -245,3 +245,18 @@ void ScriptApiServer::on_dynamic_media_added(u32 token, const std::string &playe
|
|||
lua_pushstring(L, playername.c_str());
|
||||
PCALL_RES(lua_pcall(L, 1, 0, error_handler));
|
||||
}
|
||||
|
||||
void ScriptApiServer::receive_ui_message(const char *name, const std::string &data)
|
||||
{
|
||||
SCRIPTAPI_PRECHECKHEADER
|
||||
|
||||
int error_handler = PUSH_ERROR_HANDLER(L);
|
||||
|
||||
lua_getglobal(L, "core");
|
||||
lua_getfield(L, -1, "receive_ui_message");
|
||||
|
||||
lua_pushstring(L, name);
|
||||
lua_pushlstring(L, data.c_str(), data.size());
|
||||
|
||||
PCALL_RES(lua_pcall(L, 2, 0, error_handler));
|
||||
}
|
||||
|
|
|
@ -40,6 +40,8 @@ public:
|
|||
void freeDynamicMediaCallback(u32 token);
|
||||
void on_dynamic_media_added(u32 token, const std::string &playername);
|
||||
|
||||
void receive_ui_message(const char *name, const std::string &data);
|
||||
|
||||
private:
|
||||
void getAuthHandler();
|
||||
void readPrivileges(int index, std::set<std::string> &result);
|
||||
|
|
|
@ -227,6 +227,7 @@ public:
|
|||
void handleCommand_RemovedSounds(NetworkPacket* pkt);
|
||||
void handleCommand_NodeMetaFields(NetworkPacket* pkt);
|
||||
void handleCommand_InventoryFields(NetworkPacket* pkt);
|
||||
void handleCommand_UiMessage(NetworkPacket* pkt);
|
||||
void handleCommand_FirstSrp(NetworkPacket* pkt);
|
||||
void handleCommand_SrpBytesA(NetworkPacket* pkt);
|
||||
void handleCommand_SrpBytesM(NetworkPacket* pkt);
|
||||
|
|
154
src/ui/box.cpp
154
src/ui/box.cpp
|
@ -12,6 +12,8 @@
|
|||
#include "ui/window.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
Window &Box::getWindow()
|
||||
|
@ -61,6 +63,17 @@ namespace ui
|
|||
m_style.reset();
|
||||
State state = STATE_NONE;
|
||||
|
||||
if (m_elem.isBoxFocused(*this))
|
||||
state |= STATE_FOCUSED;
|
||||
if (m_elem.isBoxSelected(*this))
|
||||
state |= STATE_SELECTED;
|
||||
if (m_elem.isBoxHovered(*this))
|
||||
state |= STATE_HOVERED;
|
||||
if (m_elem.isBoxPressed(*this))
|
||||
state |= STATE_PRESSED;
|
||||
if (m_elem.isBoxDisabled(*this))
|
||||
state |= STATE_DISABLED;
|
||||
|
||||
// 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++) {
|
||||
|
@ -135,6 +148,119 @@ namespace ui
|
|||
}
|
||||
}
|
||||
|
||||
bool Box::isPointed() const
|
||||
{
|
||||
return m_clip_rect.contains(getWindow().getPointerPos());
|
||||
}
|
||||
|
||||
bool Box::isContentPointed() const {
|
||||
// If we're pointed, then we clearly have a pointed box.
|
||||
if (isPointed()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Search through our content. If any of them are contained within the
|
||||
// same element as this box, they are candidates for being pointed.
|
||||
for (Box *box : m_content) {
|
||||
if (&box->getElem() == &m_elem && box->isContentPointed()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Box::processInput(const SDL_Event &event)
|
||||
{
|
||||
switch (event.type) {
|
||||
case UI_USER(FOCUS_REQUEST):
|
||||
// The box is dynamic, so it can be focused.
|
||||
return true;
|
||||
|
||||
case UI_USER(FOCUS_CHANGED):
|
||||
// If the box is no longer focused, it can't be pressed.
|
||||
if (event.user.data1 == &m_elem) {
|
||||
setPressed(false);
|
||||
}
|
||||
return false;
|
||||
|
||||
case UI_USER(FOCUS_SUBVERTED):
|
||||
// If some non-focused element used an event instead of this one,
|
||||
// unpress the box because user interaction has been diverted.
|
||||
setPressed(false);
|
||||
return false;
|
||||
|
||||
case UI_USER(HOVER_REQUEST):
|
||||
// The box can be hovered if the pointer is inside it.
|
||||
return isPointed();
|
||||
|
||||
case UI_USER(HOVER_CHANGED):
|
||||
// Make this box hovered if the element became hovered and the
|
||||
// pointer is inside this box.
|
||||
setHovered(event.user.data2 == &m_elem && isPointed());
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool Box::processFullPress(const SDL_Event &event, void (*on_press)(Elem &))
|
||||
{
|
||||
switch (event.type) {
|
||||
case SDL_KEYDOWN:
|
||||
// If the space key is pressed not due to a key repeat, then the
|
||||
// box becomes pressed. If the escape key is pressed while the box
|
||||
// is pressed, that unpresses the box without triggering it.
|
||||
if (event.key.keysym.sym == SDLK_SPACE && !event.key.repeat) {
|
||||
setPressed(true);
|
||||
return true;
|
||||
} else if (event.key.keysym.sym == SDLK_ESCAPE && isPressed()) {
|
||||
setPressed(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case SDL_KEYUP:
|
||||
// Releasing the space key while the box is pressed causes it to be
|
||||
// unpressed and triggered.
|
||||
if (event.key.keysym.sym == SDLK_SPACE && isPressed()) {
|
||||
setPressed(false);
|
||||
on_press(m_elem);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
// If the box is hovered, then pressing the left mouse button
|
||||
// causes it to be pressed. Otherwise, the mouse is directed at
|
||||
// some other box.
|
||||
if (isHovered() && event.button.button == SDL_BUTTON_LEFT) {
|
||||
setPressed(true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
// If the mouse button was released, the box becomes unpressed. If
|
||||
// it was released while inside the bounds of the box, that counts
|
||||
// as the box being triggered.
|
||||
if (event.button.button == SDL_BUTTON_LEFT) {
|
||||
bool was_pressed = isPressed();
|
||||
setPressed(false);
|
||||
|
||||
if (isHovered() && was_pressed) {
|
||||
on_press(m_elem);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
default:
|
||||
return processInput(event);
|
||||
}
|
||||
}
|
||||
|
||||
RectF Box::getLayerSource(const Layer &layer)
|
||||
{
|
||||
RectF src = layer.source;
|
||||
|
@ -509,4 +635,32 @@ namespace ui
|
|||
getWindow().drawTexture(m_icon_rect, m_clip_rect, m_style.icon.image,
|
||||
getLayerSource(m_style.icon), m_style.icon.tint);
|
||||
}
|
||||
|
||||
bool Box::isHovered() const
|
||||
{
|
||||
return m_elem.getHoveredBox() == getId();
|
||||
}
|
||||
|
||||
bool Box::isPressed() const
|
||||
{
|
||||
return m_elem.getPressedBox() == getId();
|
||||
}
|
||||
|
||||
void Box::setHovered(bool hovered)
|
||||
{
|
||||
if (hovered) {
|
||||
m_elem.setHoveredBox(getId());
|
||||
} else if (isHovered()) {
|
||||
m_elem.setHoveredBox(NO_ID);
|
||||
}
|
||||
}
|
||||
|
||||
void Box::setPressed(bool pressed)
|
||||
{
|
||||
if (pressed) {
|
||||
m_elem.setPressedBox(getId());
|
||||
} else if (isPressed()) {
|
||||
m_elem.setPressedBox(NO_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
38
src/ui/box.h
38
src/ui/box.h
|
@ -13,6 +13,8 @@
|
|||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
union SDL_Event;
|
||||
|
||||
namespace ui
|
||||
{
|
||||
class Elem;
|
||||
|
@ -35,12 +37,26 @@ namespace ui
|
|||
|
||||
static constexpr State NUM_STATES = 1 << 5;
|
||||
|
||||
// For groups that are standalone or not part of any particular group,
|
||||
// this box group can be used.
|
||||
static constexpr u32 NO_GROUP = -1;
|
||||
|
||||
// Represents a nonexistent box, i.e. a box with a group of NO_GROUP
|
||||
// and an item of -1, which no box should use.
|
||||
static constexpr u64 NO_ID = -1;
|
||||
|
||||
private:
|
||||
// Indicates that there is no style string for this state combination.
|
||||
static constexpr u32 NO_STYLE = -1;
|
||||
|
||||
// The element, group, and item are intrinsic to the box's identity, so
|
||||
// they are set by the constructor and aren't cleared in reset() or
|
||||
// changed in read().
|
||||
Elem &m_elem;
|
||||
|
||||
u32 m_group;
|
||||
u32 m_item;
|
||||
|
||||
std::vector<Box *> m_content;
|
||||
|
||||
Style m_style;
|
||||
|
@ -58,8 +74,10 @@ namespace ui
|
|||
RectF m_clip_rect;
|
||||
|
||||
public:
|
||||
Box(Elem &elem) :
|
||||
m_elem(elem)
|
||||
Box(Elem &elem, u32 group, u32 item) :
|
||||
m_elem(elem),
|
||||
m_group(group),
|
||||
m_item(item)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
@ -72,6 +90,10 @@ namespace ui
|
|||
Window &getWindow();
|
||||
const Window &getWindow() const;
|
||||
|
||||
u32 getGroup() const { return m_group; }
|
||||
u32 getItem() const { return m_item; }
|
||||
u64 getId() const { return ((u64)m_group << 32) | (u64)m_item; }
|
||||
|
||||
const std::vector<Box *> &getContent() const { return m_content; }
|
||||
void setContent(std::vector<Box *> content) { m_content = std::move(content); }
|
||||
|
||||
|
@ -84,6 +106,12 @@ namespace ui
|
|||
|
||||
void draw();
|
||||
|
||||
bool isPointed() const;
|
||||
bool isContentPointed() const;
|
||||
|
||||
bool processInput(const SDL_Event &event);
|
||||
bool processFullPress(const SDL_Event &event, void (*on_press)(Elem &));
|
||||
|
||||
private:
|
||||
static RectF getLayerSource(const Layer &layer);
|
||||
static SizeF getLayerSize(const Layer &layer);
|
||||
|
@ -98,5 +126,11 @@ namespace ui
|
|||
|
||||
void drawBox();
|
||||
void drawIcon();
|
||||
|
||||
bool isHovered() const;
|
||||
bool isPressed() const;
|
||||
|
||||
void setPressed(bool pressed);
|
||||
void setHovered(bool hovered);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
// Include every element header for Elem::create()
|
||||
#include "ui/static_elems.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
std::unique_ptr<Elem> Elem::create(Type type, Window &window, std::string id)
|
||||
|
@ -43,9 +45,15 @@ namespace ui
|
|||
Elem::Elem(Window &window, std::string id) :
|
||||
m_window(window),
|
||||
m_id(std::move(id)),
|
||||
m_main_box(*this)
|
||||
m_main_box(*this, Box::NO_GROUP, MAIN_BOX)
|
||||
{}
|
||||
|
||||
Elem::~Elem()
|
||||
{
|
||||
// Make sure we don't leave any dangling pointers in the window.
|
||||
m_window.clearElem(this);
|
||||
}
|
||||
|
||||
void Elem::reset()
|
||||
{
|
||||
m_order = (size_t)-1;
|
||||
|
@ -54,6 +62,8 @@ namespace ui
|
|||
m_children.clear();
|
||||
|
||||
m_main_box.reset();
|
||||
|
||||
m_events = 0;
|
||||
}
|
||||
|
||||
void Elem::read(std::istream &is)
|
||||
|
@ -72,6 +82,34 @@ namespace ui
|
|||
m_main_box.setContent(std::move(content));
|
||||
}
|
||||
|
||||
bool Elem::isFocused() const
|
||||
{
|
||||
return m_window.isFocused() && m_window.getFocused() == this;
|
||||
}
|
||||
|
||||
void Elem::enableEvent(u32 event)
|
||||
{
|
||||
m_events |= (1 << event);
|
||||
}
|
||||
|
||||
bool Elem::testEvent(u32 event) const
|
||||
{
|
||||
return m_events & (1 << event);
|
||||
}
|
||||
|
||||
std::ostringstream Elem::createEvent(u32 event) const
|
||||
{
|
||||
auto os = newOs();
|
||||
|
||||
writeU8(os, Manager::ELEM_EVENT);
|
||||
writeU64(os, m_window.getId());
|
||||
writeU8(os, event);
|
||||
writeU8(os, getType());
|
||||
writeNullStr(os, m_id);
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
void Elem::readChildren(std::istream &is)
|
||||
{
|
||||
u32 num_children = readU32(is);
|
||||
|
|
|
@ -13,10 +13,17 @@
|
|||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
union SDL_Event;
|
||||
|
||||
namespace ui
|
||||
{
|
||||
class Window;
|
||||
|
||||
#define UI_CALLBACK(method) \
|
||||
[](Elem &elem) { \
|
||||
static_cast<decltype(*this)>(elem).method(); \
|
||||
}
|
||||
|
||||
class Elem
|
||||
{
|
||||
public:
|
||||
|
@ -27,6 +34,9 @@ namespace ui
|
|||
ROOT = 0x01,
|
||||
};
|
||||
|
||||
// The main box is always the zeroth item in the Box::NO_GROUP group.
|
||||
static constexpr u32 MAIN_BOX = 0;
|
||||
|
||||
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
|
||||
|
@ -40,6 +50,10 @@ namespace ui
|
|||
std::vector<Elem *> m_children;
|
||||
|
||||
Box m_main_box;
|
||||
u64 m_hovered_box = Box::NO_ID; // Persistent
|
||||
u64 m_pressed_box = Box::NO_ID; // Persistent
|
||||
|
||||
u32 m_events;
|
||||
|
||||
public:
|
||||
static std::unique_ptr<Elem> create(Type type, Window &window, std::string id);
|
||||
|
@ -48,7 +62,7 @@ namespace ui
|
|||
|
||||
DISABLE_CLASS_COPY(Elem)
|
||||
|
||||
virtual ~Elem() = default;
|
||||
virtual ~Elem();
|
||||
|
||||
Window &getWindow() { return m_window; }
|
||||
const Window &getWindow() const { return m_window; }
|
||||
|
@ -64,9 +78,25 @@ namespace ui
|
|||
|
||||
Box &getMain() { return m_main_box; }
|
||||
|
||||
u64 getHoveredBox() const { return m_hovered_box; }
|
||||
u64 getPressedBox() const { return m_pressed_box; }
|
||||
|
||||
void setHoveredBox(u64 id) { m_hovered_box = id; }
|
||||
void setPressedBox(u64 id) { m_pressed_box = id; }
|
||||
|
||||
virtual void reset();
|
||||
virtual void read(std::istream &is);
|
||||
|
||||
bool isFocused() const;
|
||||
|
||||
virtual bool isBoxFocused (const Box &box) const { return isFocused(); }
|
||||
virtual bool isBoxSelected(const Box &box) const { return false; }
|
||||
virtual bool isBoxHovered (const Box &box) const { return box.getId() == m_hovered_box; }
|
||||
virtual bool isBoxPressed (const Box &box) const { return box.getId() == m_pressed_box; }
|
||||
virtual bool isBoxDisabled(const Box &box) const { return false; }
|
||||
|
||||
virtual bool processInput(const SDL_Event &event) { return false; }
|
||||
|
||||
protected:
|
||||
void enableEvent(u32 event);
|
||||
bool testEvent(u32 event) const;
|
||||
|
|
|
@ -11,10 +11,25 @@
|
|||
#include "client/renderingengine.h"
|
||||
#include "client/texturesource.h"
|
||||
#include "client/tile.h"
|
||||
#include "gui/mainmenumanager.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
SDL_Event createUiEvent(UiEvent type, void *data1, void *data2)
|
||||
{
|
||||
SDL_Event event;
|
||||
|
||||
event.user.type = type + SDL_USEREVENT;
|
||||
event.user.code = 0;
|
||||
event.user.data1 = data1;
|
||||
event.user.data2 = data2;
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
video::ITexture *Manager::getTexture(const std::string &name) const
|
||||
{
|
||||
return m_client->tsrc()->getTexture(name);
|
||||
|
@ -33,6 +48,7 @@ namespace ui
|
|||
m_client = nullptr;
|
||||
|
||||
m_windows.clear();
|
||||
m_gui_windows.clear();
|
||||
}
|
||||
|
||||
void Manager::removeWindow(u64 id)
|
||||
|
@ -44,6 +60,7 @@ namespace ui
|
|||
}
|
||||
|
||||
m_windows.erase(it);
|
||||
m_gui_windows.erase(id);
|
||||
}
|
||||
|
||||
void Manager::receiveMessage(const std::string &data)
|
||||
|
@ -75,6 +92,10 @@ namespace ui
|
|||
removeWindow(id);
|
||||
break;
|
||||
}
|
||||
|
||||
if (it->second.getType() == WindowType::GUI) {
|
||||
m_gui_windows.emplace(id, &it->second);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -103,6 +124,11 @@ namespace ui
|
|||
}
|
||||
}
|
||||
|
||||
void Manager::sendMessage(const std::string &data)
|
||||
{
|
||||
m_client->sendUiMessage(data.c_str(), data.size());
|
||||
}
|
||||
|
||||
void Manager::preDraw()
|
||||
{
|
||||
float base_scale = RenderingEngine::getDisplayDensity();
|
||||
|
@ -119,5 +145,28 @@ namespace ui
|
|||
}
|
||||
}
|
||||
|
||||
Window *Manager::getFocused()
|
||||
{
|
||||
if (m_gui_windows.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
return m_gui_windows.rbegin()->second;
|
||||
}
|
||||
|
||||
bool Manager::isFocused() const
|
||||
{
|
||||
return g_menumgr.menuCount() == 0 && !m_gui_windows.empty();
|
||||
}
|
||||
|
||||
bool Manager::processInput(const SDL_Event &event)
|
||||
{
|
||||
Window *focused = getFocused();
|
||||
if (focused != nullptr) {
|
||||
return focused->processInput(event);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Manager g_manager;
|
||||
}
|
||||
|
|
|
@ -16,8 +16,31 @@
|
|||
|
||||
class Client;
|
||||
|
||||
union SDL_Event;
|
||||
|
||||
namespace ui
|
||||
{
|
||||
/* Custom UI-specific event types for events of type SDL_USEREVENT. Create
|
||||
* the event structure with createUiEvent().
|
||||
*
|
||||
* Some events should always return false to give parent elements and other
|
||||
* boxes a chance to see the event. Other events return true to indicate
|
||||
* that the element may become focused or hovered.
|
||||
*/
|
||||
enum UiEvent
|
||||
{
|
||||
UI_FOCUS_REQUEST, // Return true to accept request.
|
||||
UI_FOCUS_CHANGED, // Never return true.
|
||||
UI_FOCUS_SUBVERTED, // Not sent to parent elements. Never return true.
|
||||
|
||||
UI_HOVER_REQUEST, // Return true to accept request.
|
||||
UI_HOVER_CHANGED,
|
||||
};
|
||||
|
||||
#define UI_USER(event) (UI_##event + SDL_USEREVENT)
|
||||
|
||||
SDL_Event createUiEvent(UiEvent type, void *data1 = nullptr, void *data2 = nullptr);
|
||||
|
||||
class Manager
|
||||
{
|
||||
public:
|
||||
|
@ -30,6 +53,13 @@ namespace ui
|
|||
CLOSE_WINDOW = 0x03,
|
||||
};
|
||||
|
||||
// Serialized enum; do not change values of entries.
|
||||
enum SendAction : u8
|
||||
{
|
||||
WINDOW_EVENT = 0x00,
|
||||
ELEM_EVENT = 0x01,
|
||||
};
|
||||
|
||||
private:
|
||||
Client *m_client;
|
||||
|
||||
|
@ -40,6 +70,10 @@ namespace ui
|
|||
// by window ID to make sure that they are drawn in order of creation.
|
||||
std::map<u64, Window> m_windows;
|
||||
|
||||
// Keep track of which GUI windows are currently open. We also use a
|
||||
// map so we can easily find the topmost window.
|
||||
std::map<u64, Window *> m_gui_windows;
|
||||
|
||||
public:
|
||||
Manager()
|
||||
{
|
||||
|
@ -59,9 +93,15 @@ namespace ui
|
|||
void removeWindow(u64 id);
|
||||
|
||||
void receiveMessage(const std::string &data);
|
||||
void sendMessage(const std::string &data);
|
||||
|
||||
void preDraw();
|
||||
void drawType(WindowType type);
|
||||
|
||||
Window *getFocused();
|
||||
|
||||
bool isFocused() const;
|
||||
bool processInput(const SDL_Event &event);
|
||||
};
|
||||
|
||||
extern Manager g_manager;
|
||||
|
|
|
@ -30,4 +30,9 @@ namespace ui
|
|||
|
||||
m_backdrop_box.setContent({&getMain()});
|
||||
}
|
||||
|
||||
bool Root::isBoxFocused(const Box &box) const
|
||||
{
|
||||
return box.getItem() == BACKDROP_BOX ? getWindow().isFocused() : isFocused();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,12 @@ namespace ui
|
|||
private:
|
||||
Box m_backdrop_box;
|
||||
|
||||
static constexpr u32 BACKDROP_BOX = 1;
|
||||
|
||||
public:
|
||||
Root(Window &window, std::string id) :
|
||||
Elem(window, std::move(id)),
|
||||
m_backdrop_box(*this)
|
||||
m_backdrop_box(*this, Box::NO_GROUP, BACKDROP_BOX)
|
||||
{}
|
||||
|
||||
virtual Type getType() const override { return ROOT; }
|
||||
|
@ -30,5 +32,7 @@ namespace ui
|
|||
|
||||
virtual void reset() override;
|
||||
virtual void read(std::istream &is) override;
|
||||
|
||||
virtual bool isBoxFocused(const Box &box) const override;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
#include "util/serialize.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
SizeI getTextureSize(video::ITexture *texture)
|
||||
|
@ -52,6 +54,30 @@ namespace ui
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
Elem *Window::getNextElem(Elem *elem, bool reverse)
|
||||
{
|
||||
size_t next = elem->getOrder();
|
||||
size_t last = m_ordered_elems.size() - 1;
|
||||
|
||||
if (!reverse) {
|
||||
next = (next == last) ? 0 : next + 1;
|
||||
} else {
|
||||
next = (next == 0) ? last : next - 1;
|
||||
}
|
||||
|
||||
return m_ordered_elems[next];
|
||||
}
|
||||
|
||||
void Window::clearElem(Elem *elem)
|
||||
{
|
||||
if (m_focused_elem == elem) {
|
||||
m_focused_elem = nullptr;
|
||||
}
|
||||
if (m_hovered_elem == elem) {
|
||||
m_hovered_elem = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string *Window::getStyleStr(u32 index) const
|
||||
{
|
||||
if (index < m_style_strs.size()) {
|
||||
|
@ -68,10 +94,18 @@ namespace ui
|
|||
m_root_elem = nullptr;
|
||||
|
||||
m_style_strs.clear();
|
||||
|
||||
m_focused_elem = nullptr;
|
||||
m_hovered_elem = nullptr;
|
||||
|
||||
m_allow_close = true;
|
||||
m_events = 0;
|
||||
}
|
||||
|
||||
bool Window::read(std::istream &is, bool opening)
|
||||
{
|
||||
// Read in all the fundamental properties that must be unconditionally
|
||||
// provided for the window.
|
||||
std::unordered_map<Elem *, std::string> elem_contents;
|
||||
readElems(is, elem_contents);
|
||||
|
||||
|
@ -85,8 +119,28 @@ namespace ui
|
|||
m_type = toWindowType(readU8(is));
|
||||
}
|
||||
|
||||
// Finally, we can proceed to read in all the element properties.
|
||||
return updateElems(elem_contents);
|
||||
// After the unconditional properties, read the conditional ones.
|
||||
u32 set_mask = readU32(is);
|
||||
|
||||
bool set_focus = false;
|
||||
Elem *new_focused = nullptr;
|
||||
|
||||
if (testShift(set_mask)) {
|
||||
new_focused = getElem(readNullStr(is), false);
|
||||
set_focus = true;
|
||||
}
|
||||
if (opening) {
|
||||
m_allow_close = testShift(set_mask);
|
||||
} else {
|
||||
testShift(set_mask);
|
||||
}
|
||||
|
||||
if (testShift(set_mask))
|
||||
enableEvent(ON_SUBMIT);
|
||||
if (testShift(set_mask))
|
||||
enableEvent(ON_FOCUS_CHANGE);
|
||||
|
||||
return updateElems(elem_contents, set_focus, new_focused);
|
||||
}
|
||||
|
||||
float Window::getScale() const
|
||||
|
@ -100,6 +154,14 @@ namespace ui
|
|||
return size / getScale();
|
||||
}
|
||||
|
||||
PosF Window::getPointerPos() const
|
||||
{
|
||||
int x, y;
|
||||
SDL_GetMouseState(&x, &y);
|
||||
|
||||
return PosF(x, y) / getScale();
|
||||
}
|
||||
|
||||
void Window::drawRect(RectF dst, RectF clip, video::SColor color)
|
||||
{
|
||||
if (dst.intersectWith(clip).empty() || color.getAlpha() == 0) {
|
||||
|
@ -142,10 +204,326 @@ namespace ui
|
|||
RectF layout_rect(getScreenSize());
|
||||
backdrop.relayout(layout_rect, layout_rect);
|
||||
|
||||
// Find the current hovered element, which will be nothing if we're not
|
||||
// focused. This may have changed between draw calls due to window
|
||||
// resizes or element layouting.
|
||||
hoverPointedElem();
|
||||
|
||||
// If this window isn't focused, tell the currently focused element.
|
||||
if (!isFocused()) {
|
||||
SDL_Event notice = createUiEvent(UI_FOCUS_SUBVERTED);
|
||||
sendTreeInput(m_focused_elem, notice, true);
|
||||
}
|
||||
|
||||
// Draw all of the newly layouted and updated elements.
|
||||
backdrop.draw();
|
||||
}
|
||||
|
||||
bool Window::isFocused() const
|
||||
{
|
||||
return g_manager.isFocused() && g_manager.getFocused() == this;
|
||||
}
|
||||
|
||||
bool Window::processInput(const SDL_Event &event)
|
||||
{
|
||||
switch (event.type) {
|
||||
case SDL_KEYDOWN:
|
||||
case SDL_KEYUP: {
|
||||
// Send the keypresses off to the focused element for processing.
|
||||
// The hovered element never gets keypresses.
|
||||
if (sendFocusedInput(event) != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type == SDL_KEYDOWN) {
|
||||
u16 mod = event.key.keysym.mod;
|
||||
|
||||
switch (event.key.keysym.sym) {
|
||||
case SDLK_ESCAPE:
|
||||
// If we got an Escape keypress, close the window.
|
||||
destroyWindow();
|
||||
return true;
|
||||
|
||||
case SDLK_RETURN:
|
||||
case SDLK_RETURN2:
|
||||
case SDLK_KP_ENTER:
|
||||
// If the Enter key was pressed but not handled by any
|
||||
// elements, send a submit event to the server.
|
||||
if (testEvent(ON_SUBMIT)) {
|
||||
g_manager.sendMessage(createEvent(ON_SUBMIT).str());
|
||||
}
|
||||
return true;
|
||||
|
||||
case SDLK_TAB:
|
||||
case SDLK_KP_TAB:
|
||||
// If we got a Tab key press, but not a Ctrl + Tab (which
|
||||
// is reserved for use by elements), focus the next
|
||||
// element, or the previous element if Shift is pressed.
|
||||
if (!(mod & KMOD_CTRL)) {
|
||||
focusNextElem(mod & KMOD_SHIFT);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case SDL_MOUSEMOTION:
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
case SDL_MOUSEWHEEL: {
|
||||
// Make sure that we have an updated hovered element so that the
|
||||
// hovered element is the one that receives the mouse motion event.
|
||||
if (event.type == SDL_MOUSEMOTION) {
|
||||
hoverPointedElem();
|
||||
}
|
||||
|
||||
// If we just clicked with the left mouse button, see if there's
|
||||
// any element at that position to focus.
|
||||
if (event.type == SDL_MOUSEBUTTONDOWN &&
|
||||
event.button.button == SDL_BUTTON_LEFT) {
|
||||
if (isPointerOutside()) {
|
||||
changeFocusedElem(nullptr, true);
|
||||
} else {
|
||||
focusPointedElem();
|
||||
}
|
||||
}
|
||||
|
||||
// First, give the focused element a chance to see the mouse event,
|
||||
// so it can e.g. unpress a button if a mouse button was released.
|
||||
if (sendFocusedInput(event) != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Then, send the mouse input to the hovered element.
|
||||
if (sendPointedInput(event) != nullptr) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Window::enableEvent(u32 event)
|
||||
{
|
||||
m_events |= (1 << event);
|
||||
}
|
||||
|
||||
bool Window::testEvent(u32 event) const
|
||||
{
|
||||
return m_events & (1 << event);
|
||||
}
|
||||
|
||||
std::ostringstream Window::createEvent(u32 event) const
|
||||
{
|
||||
auto os = newOs();
|
||||
|
||||
writeU8(os, Manager::WINDOW_EVENT);
|
||||
writeU64(os, m_id);
|
||||
writeU8(os, event);
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
void Window::destroyWindow()
|
||||
{
|
||||
if (m_allow_close) {
|
||||
// Always send the close event so the server can update its
|
||||
// internal window tables, even if there's no on_close() handler.
|
||||
g_manager.sendMessage(createEvent(ON_CLOSE).str());
|
||||
|
||||
// This causes the window object to be destroyed. Do not run any
|
||||
// code after this!
|
||||
g_manager.removeWindow(m_id);
|
||||
}
|
||||
}
|
||||
|
||||
Elem *Window::sendTreeInput(Elem *elem, const SDL_Event &event, bool direct)
|
||||
{
|
||||
// Give the event to the element and all its parents for processing.
|
||||
while (elem != nullptr) {
|
||||
bool handled = elem->processInput(event);
|
||||
|
||||
if (handled) {
|
||||
// If we handled the event, return the element that handled it.
|
||||
return elem;
|
||||
} else if (direct) {
|
||||
// If this event is only intended directly for this element and
|
||||
// it didn't handle the event, then we're done.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Otherwise, give the parent a chance to handle it.
|
||||
elem = elem->getParent();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Elem *Window::sendPointedInput(const SDL_Event &event)
|
||||
{
|
||||
// We want to get the topmost hovered element, so we have to iterate in
|
||||
// reverse draw order and check each element the mouse is inside.
|
||||
for (size_t i = m_ordered_elems.size(); i > 0; i--) {
|
||||
Elem *elem = m_ordered_elems[i - 1];
|
||||
if (elem->getMain().isContentPointed() && elem->processInput(event)) {
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Elem *Window::sendFocusedInput(const SDL_Event &event)
|
||||
{
|
||||
if (m_focused_elem == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Send the event to the focused element and its parents.
|
||||
Elem *handled = sendTreeInput(m_focused_elem, event, false);
|
||||
|
||||
// If one of the focused element's parents handled the event, let the
|
||||
// focused element know that focus was subverted.
|
||||
if (handled != nullptr && handled != m_focused_elem) {
|
||||
SDL_Event notice = createUiEvent(UI_FOCUS_SUBVERTED);
|
||||
sendTreeInput(m_focused_elem, notice, true);
|
||||
}
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
||||
void Window::changeFocusedElem(Elem *new_focused, bool send_event)
|
||||
{
|
||||
// If the same element is being focused, do nothing.
|
||||
if (new_focused == m_focused_elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
Elem *old_focused = m_focused_elem;
|
||||
m_focused_elem = new_focused;
|
||||
|
||||
// Let the old and new focused elements know that things have
|
||||
// changed, and their parent elements too.
|
||||
SDL_Event notice = createUiEvent(UI_FOCUS_CHANGED, old_focused, new_focused);
|
||||
|
||||
sendTreeInput(old_focused, notice, false);
|
||||
sendTreeInput(new_focused, notice, false);
|
||||
|
||||
// If the server wants to know when focus changes, send it an event.
|
||||
if (send_event && testEvent(ON_FOCUS_CHANGE)) {
|
||||
auto os = createEvent(ON_FOCUS_CHANGE);
|
||||
|
||||
// If either the old or the new element was unfocused, send an
|
||||
// empty string. Otherwise, send the ID of the element.
|
||||
writeNullStr(os, old_focused == nullptr ? "" : old_focused->getId());
|
||||
writeNullStr(os, new_focused == nullptr ? "" : new_focused->getId());
|
||||
|
||||
g_manager.sendMessage(os.str());
|
||||
}
|
||||
}
|
||||
|
||||
bool Window::requestFocusedElem(Elem *new_focused, bool send_event)
|
||||
{
|
||||
// If this element is already focused, we don't need to do anything.
|
||||
if (new_focused == m_focused_elem) {
|
||||
return m_focused_elem;
|
||||
}
|
||||
|
||||
SDL_Event notice = createUiEvent(UI_FOCUS_REQUEST);
|
||||
|
||||
// Ask the new element if it can take user focus. If it can, make it
|
||||
// the focused element.
|
||||
if (sendTreeInput(new_focused, notice, true) == new_focused) {
|
||||
changeFocusedElem(new_focused, send_event);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Window::focusNextElem(bool reverse)
|
||||
{
|
||||
// Start tabbing from the focused element if there is one, or the root
|
||||
// element otherwise.
|
||||
Elem *start = m_focused_elem != nullptr ? m_focused_elem : m_root_elem;
|
||||
|
||||
// Loop through all the elements in order (not including the starting
|
||||
// element), trying to focus them, until we reach the place we started
|
||||
// again, which means that no element wanted to be focused.
|
||||
Elem *next = getNextElem(start, reverse);
|
||||
|
||||
while (next != start) {
|
||||
if (requestFocusedElem(next, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
next = getNextElem(next, reverse);
|
||||
}
|
||||
}
|
||||
|
||||
void Window::focusPointedElem()
|
||||
{
|
||||
SDL_Event notice = createUiEvent(UI_FOCUS_REQUEST);
|
||||
|
||||
// Ask all elements that the mouse just clicked on if they want to
|
||||
// be the focused element.
|
||||
Elem *new_focused = sendPointedInput(notice);
|
||||
|
||||
// If an element responded to the request that is different from the
|
||||
// currently focused element, then update the focused element.
|
||||
if (new_focused != nullptr && m_focused_elem != new_focused) {
|
||||
changeFocusedElem(new_focused, true);
|
||||
}
|
||||
}
|
||||
|
||||
void Window::hoverPointedElem()
|
||||
{
|
||||
SDL_Event notice = createUiEvent(UI_HOVER_REQUEST);
|
||||
|
||||
// If the window is focused, ask all elements that the mouse is
|
||||
// currently inside if they want to be the hovered element. Otherwise,
|
||||
// make no element hovered.
|
||||
Elem *old_hovered = m_hovered_elem;
|
||||
Elem *new_hovered = nullptr;
|
||||
|
||||
if (isFocused()) {
|
||||
new_hovered = sendPointedInput(notice);
|
||||
}
|
||||
|
||||
// If a different element responded to the hover request (or no element
|
||||
// at all), then update the hovered element.
|
||||
if (old_hovered != new_hovered) {
|
||||
m_hovered_elem = new_hovered;
|
||||
|
||||
// Let the old and new hovered elements know that things have
|
||||
// changed, and their parent elements too.
|
||||
notice = createUiEvent(UI_HOVER_CHANGED, old_hovered, new_hovered);
|
||||
|
||||
sendTreeInput(old_hovered, notice, false);
|
||||
sendTreeInput(new_hovered, notice, false);
|
||||
}
|
||||
}
|
||||
|
||||
bool Window::isPointerOutside() const
|
||||
{
|
||||
// If the mouse is inside any element, it's not outside the window. We
|
||||
// have to check every element, not just the root, because elements may
|
||||
// have the noclip property set. However, the backdrop is not included.
|
||||
for (Elem *elem : m_ordered_elems) {
|
||||
if (elem->getMain().isContentPointed()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Window::readElems(std::istream &is,
|
||||
std::unordered_map<Elem *, std::string> &elem_contents)
|
||||
{
|
||||
|
@ -253,7 +631,8 @@ namespace ui
|
|||
}
|
||||
}
|
||||
|
||||
bool Window::updateElems(std::unordered_map<Elem *, std::string> &elem_contents)
|
||||
bool Window::updateElems(std::unordered_map<Elem *, std::string> &elem_contents,
|
||||
bool set_focus, Elem *new_focused)
|
||||
{
|
||||
// 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
|
||||
|
@ -277,6 +656,19 @@ namespace ui
|
|||
errorstream << "Window " << m_id << " has orphaned elements" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the user wants to focus a new element or unfocus the current
|
||||
// element, remove focus from the current element and request focus on
|
||||
// the new element.
|
||||
if (set_focus && new_focused != m_focused_elem) {
|
||||
if (new_focused != nullptr) {
|
||||
if (!requestFocusedElem(new_focused, false)) {
|
||||
changeFocusedElem(nullptr, false);
|
||||
}
|
||||
} else {
|
||||
changeFocusedElem(nullptr, false);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
union SDL_Event;
|
||||
|
||||
namespace ui
|
||||
{
|
||||
class Root;
|
||||
|
@ -37,6 +39,11 @@ namespace ui
|
|||
class Window
|
||||
{
|
||||
private:
|
||||
// Serialized constants; do not change values of entries.
|
||||
static constexpr u32 ON_CLOSE = 0x00;
|
||||
static constexpr u32 ON_SUBMIT = 0x01;
|
||||
static constexpr u32 ON_FOCUS_CHANGE = 0x02;
|
||||
|
||||
static constexpr size_t MAX_TREE_DEPTH = 64;
|
||||
|
||||
// The ID and type are intrinsic to the box's identity, so they aren't
|
||||
|
@ -52,6 +59,12 @@ namespace ui
|
|||
|
||||
std::vector<std::string> m_style_strs;
|
||||
|
||||
Elem *m_focused_elem;
|
||||
Elem *m_hovered_elem;
|
||||
|
||||
bool m_allow_close;
|
||||
u32 m_events;
|
||||
|
||||
public:
|
||||
Window(u64 id) :
|
||||
m_id(id)
|
||||
|
@ -67,7 +80,13 @@ namespace ui
|
|||
const std::vector<Elem *> &getElems() { return m_ordered_elems; }
|
||||
|
||||
Elem *getElem(const std::string &id, bool required);
|
||||
Elem *getNextElem(Elem *elem, bool reverse);
|
||||
|
||||
Root *getRoot() { return m_root_elem; }
|
||||
Elem *getFocused() { return m_focused_elem; }
|
||||
Elem *getHovered() { return m_hovered_elem; }
|
||||
|
||||
void clearElem(Elem *elem);
|
||||
|
||||
const std::string *getStyleStr(u32 index) const;
|
||||
|
||||
|
@ -77,6 +96,7 @@ namespace ui
|
|||
float getScale() const;
|
||||
|
||||
SizeF getScreenSize() const;
|
||||
PosF getPointerPos() const;
|
||||
|
||||
void drawRect(RectF dst, RectF clip, video::SColor color);
|
||||
void drawTexture(RectF dst, RectF clip, video::ITexture *texture,
|
||||
|
@ -84,13 +104,40 @@ namespace ui
|
|||
|
||||
void drawAll();
|
||||
|
||||
bool isFocused() const;
|
||||
bool processInput(const SDL_Event &event);
|
||||
|
||||
private:
|
||||
void enableEvent(u32 event);
|
||||
bool testEvent(u32 event) const;
|
||||
|
||||
std::ostringstream createEvent(u32 event) const;
|
||||
|
||||
// Warning: This method causes the window object to be destroyed.
|
||||
// Return immediately after use, and don't use the window object again.
|
||||
void destroyWindow();
|
||||
|
||||
Elem *sendTreeInput(Elem *elem, const SDL_Event &event, bool direct);
|
||||
Elem *sendPointedInput(const SDL_Event &event);
|
||||
Elem *sendFocusedInput(const SDL_Event &event);
|
||||
|
||||
void changeFocusedElem(Elem *new_focused, bool send_event);
|
||||
bool requestFocusedElem(Elem *new_focused, bool send_event);
|
||||
|
||||
void focusNextElem(bool reverse);
|
||||
|
||||
void focusPointedElem();
|
||||
void hoverPointedElem();
|
||||
|
||||
bool isPointerOutside() const;
|
||||
|
||||
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 updateElems(std::unordered_map<Elem *, std::string> &elem_contents,
|
||||
bool set_focus, Elem *new_focused);
|
||||
bool updateTree(Elem *elem, size_t depth);
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue