1
0
Fork 0
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:
v-rob 2024-08-31 15:44:01 -07:00
parent 9cc73f16f0
commit bb2f857b04
27 changed files with 1060 additions and 49 deletions

View file

@ -141,3 +141,47 @@ core.register_on_leaveplayer(function(player)
end end
end 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

View file

@ -10,6 +10,7 @@ function ui._new_type(base, type, type_id, id_required)
class._type = type class._type = type
class._type_id = type_id class._type_id = type_id
class._id_required = id_required class._id_required = id_required
class._handlers = setmetatable({}, {__index = base and base._handlers})
ui._elem_types[type] = class 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)) ui._encode_flag(fl, "s", ui._encode_flags(box_fl))
end 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

View file

@ -28,6 +28,13 @@ function ui.Window:_init(props)
self._root = ui._req(props.root, ui.Root) 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._context = nil -- Set by ui.Context
self._elems = self._root:_get_flat() self._elems = self._root:_get_flat()
@ -43,6 +50,11 @@ function ui.Window:_init(props)
elem._window = self elem._window = self
end 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 for _, item in ipairs(props) do
ui._req(item, ui.Style) ui._req(item, ui.Style)
end end
@ -52,12 +64,22 @@ function ui.Window:_encode(player, opening)
local enc_styles = self:_encode_styles() local enc_styles = self:_encode_styles()
local enc_elems = self:_encode_elems() 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) local data = ui._encode("ZzZ", enc_elems, self._root._id, enc_styles)
if opening then if opening then
data = ui._encode("ZB", data, ui._window_types[self._type]) data = ui._encode("ZB", data, ui._window_types[self._type])
end end
return data return ui._encode("ZZ", data, ui._encode_flags(fl))
end end
function ui.Window:_encode_styles() function ui.Window:_encode_styles()
@ -188,3 +210,74 @@ function ui.Window:_encode_elems()
return ui._encode_array("Z", enc_elems) return ui._encode_array("Z", enc_elems)
end 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

View file

@ -8,6 +8,8 @@
#include "Keycodes.h" #include "Keycodes.h"
#include "irrString.h" #include "irrString.h"
union SDL_Event;
namespace irr namespace irr
{ {
//! Enumeration for all event types there are. //! Enumeration for all event types there are.
@ -82,6 +84,11 @@ enum EEVENT_TYPE
//! Application state events like a resume, pause etc. //! Application state events like a resume, pause etc.
EET_APPLICATION_EVENT, 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 //! This enum is never used, it only forces the compiler to
//! compile these enumeration values to 32 bit. //! compile these enumeration values to 32 bit.
EGUIET_FORCE_32_BIT = 0x7fffffff EGUIET_FORCE_32_BIT = 0x7fffffff
@ -526,6 +533,7 @@ struct SEvent
}; };
EEVENT_TYPE EventType; EEVENT_TYPE EventType;
union SDL_Event *SdlEvent = nullptr;
union union
{ {
struct SGUIEvent GUIEvent; struct SGUIEvent GUIEvent;

View file

@ -714,6 +714,10 @@ bool CIrrDeviceSDL::run()
// os::Printer::log("event: ", core::stringc((int)SDL_event.type).c_str(), ELL_INFORMATION); // just for debugging // os::Printer::log("event: ", core::stringc((int)SDL_event.type).c_str(), ELL_INFORMATION); // just for debugging
irrevent = {}; 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) { switch (SDL_event.type) {
case SDL_MOUSEMOTION: { case SDL_MOUSEMOTION: {
SDL_Keymod keymod = SDL_GetModState(); SDL_Keymod keymod = SDL_GetModState();
@ -736,31 +740,28 @@ bool CIrrDeviceSDL::run()
irrevent.MouseInput.ButtonStates = MouseButtonStates; irrevent.MouseInput.ButtonStates = MouseButtonStates;
irrevent.MouseInput.Shift = (keymod & KMOD_SHIFT) != 0; irrevent.MouseInput.Shift = (keymod & KMOD_SHIFT) != 0;
irrevent.MouseInput.Control = (keymod & KMOD_CTRL) != 0; irrevent.MouseInput.Control = (keymod & KMOD_CTRL) != 0;
postEventFromUser(irrevent);
break; break;
} }
case SDL_MOUSEWHEEL: { 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(); SDL_Keymod keymod = SDL_GetModState();
irrevent.EventType = irr::EET_MOUSE_INPUT_EVENT; irrevent.EventType = irr::EET_MOUSE_INPUT_EVENT;
irrevent.MouseInput.Event = irr::EMIE_MOUSE_WHEEL; irrevent.MouseInput.Event = irr::EMIE_MOUSE_WHEEL;
#if SDL_VERSION_ATLEAST(2, 0, 18) irrevent.MouseInput.Wheel = wheel;
irrevent.MouseInput.Wheel = SDL_event.wheel.preciseY;
#else
irrevent.MouseInput.Wheel = SDL_event.wheel.y;
#endif
irrevent.MouseInput.ButtonStates = MouseButtonStates; irrevent.MouseInput.ButtonStates = MouseButtonStates;
irrevent.MouseInput.Shift = (keymod & KMOD_SHIFT) != 0; irrevent.MouseInput.Shift = (keymod & KMOD_SHIFT) != 0;
irrevent.MouseInput.Control = (keymod & KMOD_CTRL) != 0; irrevent.MouseInput.Control = (keymod & KMOD_CTRL) != 0;
irrevent.MouseInput.X = MouseX; irrevent.MouseInput.X = MouseX;
irrevent.MouseInput.Y = MouseY; irrevent.MouseInput.Y = MouseY;
// wheel y can be 0 if scrolling sideways
if (irrevent.MouseInput.Wheel == 0.0f)
break;
postEventFromUser(irrevent);
break; break;
} }
case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONDOWN:
@ -854,16 +855,22 @@ bool CIrrDeviceSDL::run()
irrevent.MouseInput.Y = static_cast<s32>(SDL_event.button.y * ScaleY); irrevent.MouseInput.Y = static_cast<s32>(SDL_event.button.y * ScaleY);
irrevent.MouseInput.Shift = shift; irrevent.MouseInput.Shift = shift;
irrevent.MouseInput.Control = control; irrevent.MouseInput.Control = control;
postEventFromUser(irrevent);
if (irrevent.MouseInput.Event >= EMIE_LMOUSE_PRESSED_DOWN && irrevent.MouseInput.Event <= EMIE_MMOUSE_PRESSED_DOWN) { 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); u32 clicks = checkSuccessiveClicks(irrevent.MouseInput.X, irrevent.MouseInput.Y, irrevent.MouseInput.Event);
if (clicks == 2) { 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); irrevent.MouseInput.Event = (EMOUSE_INPUT_EVENT)(EMIE_LMOUSE_DOUBLE_CLICK + irrevent.MouseInput.Event - EMIE_LMOUSE_PRESSED_DOWN);
postEventFromUser(irrevent);
} else if (clicks == 3) { } else if (clicks == 3) {
irrevent.MouseInput.Event = (EMOUSE_INPUT_EVENT)(EMIE_LMOUSE_TRIPLE_CLICK + irrevent.MouseInput.Event - EMIE_LMOUSE_PRESSED_DOWN);
postEventFromUser(irrevent); 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) { } 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.PressedDown = SDL_event.type == SDL_MOUSEBUTTONDOWN;
irrevent.KeyInput.Shift = shift; irrevent.KeyInput.Shift = shift;
irrevent.KeyInput.Control = control; irrevent.KeyInput.Control = control;
postEventFromUser(irrevent);
} }
break; break;
} }
@ -880,9 +886,6 @@ bool CIrrDeviceSDL::run()
irrevent.EventType = irr::EET_STRING_INPUT_EVENT; irrevent.EventType = irr::EET_STRING_INPUT_EVENT;
irrevent.StringInput.Str = new core::stringw(); irrevent.StringInput.Str = new core::stringw();
irr::core::utf8ToWString(*irrevent.StringInput.Str, SDL_event.text.text); irr::core::utf8ToWString(*irrevent.StringInput.Str, SDL_event.text.text);
postEventFromUser(irrevent);
delete irrevent.StringInput.Str;
irrevent.StringInput.Str = NULL;
} break; } break;
case SDL_KEYDOWN: case SDL_KEYDOWN:
@ -914,8 +917,6 @@ bool CIrrDeviceSDL::run()
irrevent.KeyInput.Char = findCharToPassToIrrlicht(keysym, key, irrevent.KeyInput.Char = findCharToPassToIrrlicht(keysym, key,
(SDL_event.key.keysym.mod & KMOD_NUM) != 0); (SDL_event.key.keysym.mod & KMOD_NUM) != 0);
irrevent.KeyInput.SystemKeyCode = scancode; irrevent.KeyInput.SystemKeyCode = scancode;
postEventFromUser(irrevent);
} break; } break;
case SDL_QUIT: case SDL_QUIT:
@ -939,7 +940,6 @@ bool CIrrDeviceSDL::run()
if (old_scale_x != ScaleX || old_scale_y != ScaleY) { if (old_scale_x != ScaleX || old_scale_y != ScaleY) {
irrevent.EventType = EET_APPLICATION_EVENT; irrevent.EventType = EET_APPLICATION_EVENT;
irrevent.ApplicationEvent.EventType = EAET_DPI_CHANGED; irrevent.ApplicationEvent.EventType = EAET_DPI_CHANGED;
postEventFromUser(irrevent);
} }
break; break;
} }
@ -949,8 +949,6 @@ bool CIrrDeviceSDL::run()
irrevent.EventType = irr::EET_USER_EVENT; irrevent.EventType = irr::EET_USER_EVENT;
irrevent.UserEvent.UserData1 = reinterpret_cast<uintptr_t>(SDL_event.user.data1); irrevent.UserEvent.UserData1 = reinterpret_cast<uintptr_t>(SDL_event.user.data1);
irrevent.UserEvent.UserData2 = reinterpret_cast<uintptr_t>(SDL_event.user.data2); irrevent.UserEvent.UserData2 = reinterpret_cast<uintptr_t>(SDL_event.user.data2);
postEventFromUser(irrevent);
break; break;
case SDL_FINGERDOWN: case SDL_FINGERDOWN:
@ -961,8 +959,6 @@ bool CIrrDeviceSDL::run()
irrevent.TouchInput.Y = static_cast<s32>(SDL_event.tfinger.y * Height); irrevent.TouchInput.Y = static_cast<s32>(SDL_event.tfinger.y * Height);
CurrentTouchCount++; CurrentTouchCount++;
irrevent.TouchInput.touchedCount = CurrentTouchCount; irrevent.TouchInput.touchedCount = CurrentTouchCount;
postEventFromUser(irrevent);
break; break;
case SDL_FINGERMOTION: case SDL_FINGERMOTION:
@ -972,8 +968,6 @@ bool CIrrDeviceSDL::run()
irrevent.TouchInput.X = static_cast<s32>(SDL_event.tfinger.x * Width); irrevent.TouchInput.X = static_cast<s32>(SDL_event.tfinger.x * Width);
irrevent.TouchInput.Y = static_cast<s32>(SDL_event.tfinger.y * Height); irrevent.TouchInput.Y = static_cast<s32>(SDL_event.tfinger.y * Height);
irrevent.TouchInput.touchedCount = CurrentTouchCount; irrevent.TouchInput.touchedCount = CurrentTouchCount;
postEventFromUser(irrevent);
break; break;
case SDL_FINGERUP: case SDL_FINGERUP:
@ -988,8 +982,6 @@ bool CIrrDeviceSDL::run()
if (CurrentTouchCount > 0) { if (CurrentTouchCount > 0) {
CurrentTouchCount--; CurrentTouchCount--;
} }
postEventFromUser(irrevent);
break; break;
// Contrary to what the SDL documentation says, SDL_APP_WILLENTERBACKGROUND // Contrary to what the SDL documentation says, SDL_APP_WILLENTERBACKGROUND
@ -1017,6 +1009,14 @@ bool CIrrDeviceSDL::run()
default: default:
break; break;
} // end switch } // end switch
postEventFromUser(irrevent);
if (SDL_event.type == SDL_TEXTINPUT) {
delete irrevent.StringInput.Str;
irrevent.StringInput.Str = nullptr;
}
resetReceiveTextInputEvents(); resetReceiveTextInputEvents();
} // end while } // end while

View file

@ -1288,6 +1288,14 @@ void Client::sendInventoryFields(const std::string &formname,
Send(&pkt); 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) void Client::sendInventoryAction(InventoryAction *a)
{ {
std::ostringstream os(std::ios_base::binary); std::ostringstream os(std::ios_base::binary);

View file

@ -231,6 +231,7 @@ public:
const StringMap &fields); const StringMap &fields);
void sendInventoryFields(const std::string &formname, void sendInventoryFields(const std::string &formname,
const StringMap &fields); const StringMap &fields);
void sendUiMessage(const char *data, size_t len);
void sendInventoryAction(InventoryAction *a); void sendInventoryAction(InventoryAction *a);
void sendChatMessage(const std::wstring &message); void sendChatMessage(const std::wstring &message);
void clearOutChatQueue(); void clearOutChatQueue();

View file

@ -163,7 +163,7 @@ void GameUI::update(const RunStats &stats, Client *client, MapDrawControl *draw_
m_guitext2->setVisible(m_flags.show_basic_debug); m_guitext2->setVisible(m_flags.show_basic_debug);
setStaticText(m_guitext_info, m_infotext.c_str()); 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; static const float statustext_time_max = 1.5f;

View file

@ -7,12 +7,17 @@
/* /*
All kinds of stuff that needs to be exposed from main.cpp All kinds of stuff that needs to be exposed from main.cpp
*/ */
#include "config.h"
#include "modalMenu.h" #include "modalMenu.h"
#include <cassert> #include <cassert>
#include <list> #include <list>
#include "IGUIEnvironment.h" #include "IGUIEnvironment.h"
#if BUILD_UI
#include "ui/manager.h"
#endif
namespace irr::gui { namespace irr::gui {
class IGUIStaticText; class IGUIStaticText;
} }
@ -67,10 +72,16 @@ public:
// Returns true to prevent further processing // Returns true to prevent further processing
virtual bool preprocessEvent(const SEvent& event) virtual bool preprocessEvent(const SEvent& event)
{ {
if (m_stack.empty()) if (!m_stack.empty()) {
return false; GUIModalMenu *mm = dynamic_cast<GUIModalMenu*>(m_stack.back());
GUIModalMenu *mm = dynamic_cast<GUIModalMenu*>(m_stack.back()); return mm && mm->preprocessEvent(event);
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 size_t menuCount() const
@ -109,7 +120,11 @@ extern MainMenuManager g_menumgr;
static inline bool isMenuActive() static inline bool isMenuActive()
{ {
#if BUILD_UI
return g_menumgr.menuCount() != 0 || ui::g_manager.isFocused();
#else
return g_menumgr.menuCount() != 0; return g_menumgr.menuCount() != 0;
#endif
} }
class MainGameCallback : public IGameCallback class MainGameCallback : public IGameCallback

View file

@ -188,7 +188,7 @@ const ServerCommandFactory serverCommandFactoryTable[TOSERVER_NUM_MSG_TYPES] =
{ "TOSERVER_REMOVED_SOUNDS", 2, true }, // 0x3a { "TOSERVER_REMOVED_SOUNDS", 2, true }, // 0x3a
{ "TOSERVER_NODEMETA_FIELDS", 0, true }, // 0x3b { "TOSERVER_NODEMETA_FIELDS", 0, true }, // 0x3b
{ "TOSERVER_INVENTORY_FIELDS", 0, true }, // 0x3c { "TOSERVER_INVENTORY_FIELDS", 0, true }, // 0x3c
null_command_factory, // 0x3d { "TOSERVER_UI_MESSAGE", 0, true }, // 0x3d
null_command_factory, // 0x3e null_command_factory, // 0x3e
null_command_factory, // 0x3f null_command_factory, // 0x3f
{ "TOSERVER_REQUEST_MEDIA", 1, true }, // 0x40 { "TOSERVER_REQUEST_MEDIA", 1, true }, // 0x40

View file

@ -61,7 +61,7 @@
[scheduled bump for 5.10.0] [scheduled bump for 5.10.0]
PROTOCOL VERSION 47: PROTOCOL VERSION 47:
Add particle blend mode "clip" Add particle blend mode "clip"
Add TOCLIENT_UI_MESSAGE Add TOCLIENT_UI_MESSAGE and TOSERVER_UI_MESSAGE
[scheduled bump for 5.11.0] [scheduled bump for 5.11.0]
PROTOCOL VERSION 48 PROTOCOL VERSION 48
Add compression to some existing packets Add compression to some existing packets

View file

@ -838,6 +838,11 @@ enum ToServerCommand : u16
u8[len] field value u8[len] field value
*/ */
TOSERVER_UI_MESSAGE = 0x3d,
/*
Variable-length structure that changes depending on the message type.
*/
TOSERVER_REQUEST_MEDIA = 0x40, TOSERVER_REQUEST_MEDIA = 0x40,
/* /*
u16 number of files requested u16 number of files requested

View file

@ -72,7 +72,7 @@ const ToServerCommandHandler toServerCommandTable[TOSERVER_NUM_MSG_TYPES] =
{ "TOSERVER_REMOVED_SOUNDS", TOSERVER_STATE_INGAME, &Server::handleCommand_RemovedSounds }, // 0x3a { "TOSERVER_REMOVED_SOUNDS", TOSERVER_STATE_INGAME, &Server::handleCommand_RemovedSounds }, // 0x3a
{ "TOSERVER_NODEMETA_FIELDS", TOSERVER_STATE_INGAME, &Server::handleCommand_NodeMetaFields }, // 0x3b { "TOSERVER_NODEMETA_FIELDS", TOSERVER_STATE_INGAME, &Server::handleCommand_NodeMetaFields }, // 0x3b
{ "TOSERVER_INVENTORY_FIELDS", TOSERVER_STATE_INGAME, &Server::handleCommand_InventoryFields }, // 0x3c { "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, // 0x3e
null_command_handler, // 0x3f null_command_handler, // 0x3f
{ "TOSERVER_REQUEST_MEDIA", TOSERVER_STATE_STARTUP, &Server::handleCommand_RequestMedia }, // 0x40 { "TOSERVER_REQUEST_MEDIA", TOSERVER_STATE_STARTUP, &Server::handleCommand_RequestMedia }, // 0x40

View file

@ -1422,6 +1422,15 @@ void Server::handleCommand_InventoryFields(NetworkPacket* pkt)
actionstream << ", possible exploitation attempt" << std::endl; 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) void Server::handleCommand_FirstSrp(NetworkPacket* pkt)
{ {
session_t peer_id = pkt->getPeerId(); session_t peer_id = pkt->getPeerId();

View file

@ -245,3 +245,18 @@ void ScriptApiServer::on_dynamic_media_added(u32 token, const std::string &playe
lua_pushstring(L, playername.c_str()); lua_pushstring(L, playername.c_str());
PCALL_RES(lua_pcall(L, 1, 0, error_handler)); 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));
}

View file

@ -40,6 +40,8 @@ public:
void freeDynamicMediaCallback(u32 token); void freeDynamicMediaCallback(u32 token);
void on_dynamic_media_added(u32 token, const std::string &playername); void on_dynamic_media_added(u32 token, const std::string &playername);
void receive_ui_message(const char *name, const std::string &data);
private: private:
void getAuthHandler(); void getAuthHandler();
void readPrivileges(int index, std::set<std::string> &result); void readPrivileges(int index, std::set<std::string> &result);

View file

@ -227,6 +227,7 @@ public:
void handleCommand_RemovedSounds(NetworkPacket* pkt); void handleCommand_RemovedSounds(NetworkPacket* pkt);
void handleCommand_NodeMetaFields(NetworkPacket* pkt); void handleCommand_NodeMetaFields(NetworkPacket* pkt);
void handleCommand_InventoryFields(NetworkPacket* pkt); void handleCommand_InventoryFields(NetworkPacket* pkt);
void handleCommand_UiMessage(NetworkPacket* pkt);
void handleCommand_FirstSrp(NetworkPacket* pkt); void handleCommand_FirstSrp(NetworkPacket* pkt);
void handleCommand_SrpBytesA(NetworkPacket* pkt); void handleCommand_SrpBytesA(NetworkPacket* pkt);
void handleCommand_SrpBytesM(NetworkPacket* pkt); void handleCommand_SrpBytesM(NetworkPacket* pkt);

View file

@ -12,6 +12,8 @@
#include "ui/window.h" #include "ui/window.h"
#include "util/serialize.h" #include "util/serialize.h"
#include <SDL2/SDL.h>
namespace ui namespace ui
{ {
Window &Box::getWindow() Window &Box::getWindow()
@ -61,6 +63,17 @@ namespace ui
m_style.reset(); m_style.reset();
State state = STATE_NONE; 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 // Loop over each style state from lowest precedence to highest since
// they should be applied in that order. // they should be applied in that order.
for (State i = 0; i < m_style_refs.size(); i++) { 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 Box::getLayerSource(const Layer &layer)
{ {
RectF src = layer.source; RectF src = layer.source;
@ -509,4 +635,32 @@ namespace ui
getWindow().drawTexture(m_icon_rect, m_clip_rect, m_style.icon.image, getWindow().drawTexture(m_icon_rect, m_clip_rect, m_style.icon.image,
getLayerSource(m_style.icon), m_style.icon.tint); 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);
}
}
} }

View file

@ -13,6 +13,8 @@
#include <string> #include <string>
#include <vector> #include <vector>
union SDL_Event;
namespace ui namespace ui
{ {
class Elem; class Elem;
@ -35,12 +37,26 @@ namespace ui
static constexpr State NUM_STATES = 1 << 5; 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: private:
// Indicates that there is no style string for this state combination. // Indicates that there is no style string for this state combination.
static constexpr u32 NO_STYLE = -1; 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; Elem &m_elem;
u32 m_group;
u32 m_item;
std::vector<Box *> m_content; std::vector<Box *> m_content;
Style m_style; Style m_style;
@ -58,8 +74,10 @@ namespace ui
RectF m_clip_rect; RectF m_clip_rect;
public: public:
Box(Elem &elem) : Box(Elem &elem, u32 group, u32 item) :
m_elem(elem) m_elem(elem),
m_group(group),
m_item(item)
{ {
reset(); reset();
} }
@ -72,6 +90,10 @@ namespace ui
Window &getWindow(); Window &getWindow();
const Window &getWindow() const; 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; } const std::vector<Box *> &getContent() const { return m_content; }
void setContent(std::vector<Box *> content) { m_content = std::move(content); } void setContent(std::vector<Box *> content) { m_content = std::move(content); }
@ -84,6 +106,12 @@ namespace ui
void draw(); 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: private:
static RectF getLayerSource(const Layer &layer); static RectF getLayerSource(const Layer &layer);
static SizeF getLayerSize(const Layer &layer); static SizeF getLayerSize(const Layer &layer);
@ -98,5 +126,11 @@ namespace ui
void drawBox(); void drawBox();
void drawIcon(); void drawIcon();
bool isHovered() const;
bool isPressed() const;
void setPressed(bool pressed);
void setHovered(bool hovered);
}; };
} }

View file

@ -13,6 +13,8 @@
// Include every element header for Elem::create() // Include every element header for Elem::create()
#include "ui/static_elems.h" #include "ui/static_elems.h"
#include <SDL2/SDL.h>
namespace ui namespace ui
{ {
std::unique_ptr<Elem> Elem::create(Type type, Window &window, std::string id) 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) : Elem::Elem(Window &window, std::string id) :
m_window(window), m_window(window),
m_id(std::move(id)), 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() void Elem::reset()
{ {
m_order = (size_t)-1; m_order = (size_t)-1;
@ -54,6 +62,8 @@ namespace ui
m_children.clear(); m_children.clear();
m_main_box.reset(); m_main_box.reset();
m_events = 0;
} }
void Elem::read(std::istream &is) void Elem::read(std::istream &is)
@ -72,6 +82,34 @@ namespace ui
m_main_box.setContent(std::move(content)); 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) void Elem::readChildren(std::istream &is)
{ {
u32 num_children = readU32(is); u32 num_children = readU32(is);

View file

@ -13,10 +13,17 @@
#include <string> #include <string>
#include <vector> #include <vector>
union SDL_Event;
namespace ui namespace ui
{ {
class Window; class Window;
#define UI_CALLBACK(method) \
[](Elem &elem) { \
static_cast<decltype(*this)>(elem).method(); \
}
class Elem class Elem
{ {
public: public:
@ -27,6 +34,9 @@ namespace ui
ROOT = 0x01, ROOT = 0x01,
}; };
// The main box is always the zeroth item in the Box::NO_GROUP group.
static constexpr u32 MAIN_BOX = 0;
private: private:
// The window and ID are intrinsic to the element's identity, so they // 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 // are set by the constructor and aren't cleared in reset() or changed
@ -40,6 +50,10 @@ namespace ui
std::vector<Elem *> m_children; std::vector<Elem *> m_children;
Box m_main_box; Box m_main_box;
u64 m_hovered_box = Box::NO_ID; // Persistent
u64 m_pressed_box = Box::NO_ID; // Persistent
u32 m_events;
public: public:
static std::unique_ptr<Elem> create(Type type, Window &window, std::string id); static std::unique_ptr<Elem> create(Type type, Window &window, std::string id);
@ -48,7 +62,7 @@ namespace ui
DISABLE_CLASS_COPY(Elem) DISABLE_CLASS_COPY(Elem)
virtual ~Elem() = default; virtual ~Elem();
Window &getWindow() { return m_window; } Window &getWindow() { return m_window; }
const Window &getWindow() const { return m_window; } const Window &getWindow() const { return m_window; }
@ -64,9 +78,25 @@ namespace ui
Box &getMain() { return m_main_box; } 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 reset();
virtual void read(std::istream &is); 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: protected:
void enableEvent(u32 event); void enableEvent(u32 event);
bool testEvent(u32 event) const; bool testEvent(u32 event) const;

View file

@ -11,10 +11,25 @@
#include "client/renderingengine.h" #include "client/renderingengine.h"
#include "client/texturesource.h" #include "client/texturesource.h"
#include "client/tile.h" #include "client/tile.h"
#include "gui/mainmenumanager.h"
#include "util/serialize.h" #include "util/serialize.h"
#include <SDL2/SDL.h>
namespace ui 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 video::ITexture *Manager::getTexture(const std::string &name) const
{ {
return m_client->tsrc()->getTexture(name); return m_client->tsrc()->getTexture(name);
@ -33,6 +48,7 @@ namespace ui
m_client = nullptr; m_client = nullptr;
m_windows.clear(); m_windows.clear();
m_gui_windows.clear();
} }
void Manager::removeWindow(u64 id) void Manager::removeWindow(u64 id)
@ -44,6 +60,7 @@ namespace ui
} }
m_windows.erase(it); m_windows.erase(it);
m_gui_windows.erase(id);
} }
void Manager::receiveMessage(const std::string &data) void Manager::receiveMessage(const std::string &data)
@ -75,6 +92,10 @@ namespace ui
removeWindow(id); removeWindow(id);
break; break;
} }
if (it->second.getType() == WindowType::GUI) {
m_gui_windows.emplace(id, &it->second);
}
break; 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() void Manager::preDraw()
{ {
float base_scale = RenderingEngine::getDisplayDensity(); 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; Manager g_manager;
} }

View file

@ -16,8 +16,31 @@
class Client; class Client;
union SDL_Event;
namespace ui 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 class Manager
{ {
public: public:
@ -30,6 +53,13 @@ namespace ui
CLOSE_WINDOW = 0x03, CLOSE_WINDOW = 0x03,
}; };
// Serialized enum; do not change values of entries.
enum SendAction : u8
{
WINDOW_EVENT = 0x00,
ELEM_EVENT = 0x01,
};
private: private:
Client *m_client; Client *m_client;
@ -40,6 +70,10 @@ namespace ui
// by window ID to make sure that they are drawn in order of creation. // by window ID to make sure that they are drawn in order of creation.
std::map<u64, Window> m_windows; 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: public:
Manager() Manager()
{ {
@ -59,9 +93,15 @@ namespace ui
void removeWindow(u64 id); void removeWindow(u64 id);
void receiveMessage(const std::string &data); void receiveMessage(const std::string &data);
void sendMessage(const std::string &data);
void preDraw(); void preDraw();
void drawType(WindowType type); void drawType(WindowType type);
Window *getFocused();
bool isFocused() const;
bool processInput(const SDL_Event &event);
}; };
extern Manager g_manager; extern Manager g_manager;

View file

@ -30,4 +30,9 @@ namespace ui
m_backdrop_box.setContent({&getMain()}); m_backdrop_box.setContent({&getMain()});
} }
bool Root::isBoxFocused(const Box &box) const
{
return box.getItem() == BACKDROP_BOX ? getWindow().isFocused() : isFocused();
}
} }

View file

@ -18,10 +18,12 @@ namespace ui
private: private:
Box m_backdrop_box; Box m_backdrop_box;
static constexpr u32 BACKDROP_BOX = 1;
public: public:
Root(Window &window, std::string id) : Root(Window &window, std::string id) :
Elem(window, std::move(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; } virtual Type getType() const override { return ROOT; }
@ -30,5 +32,7 @@ namespace ui
virtual void reset() override; virtual void reset() override;
virtual void read(std::istream &is) override; virtual void read(std::istream &is) override;
virtual bool isBoxFocused(const Box &box) const override;
}; };
} }

View file

@ -16,6 +16,8 @@
#include "util/serialize.h" #include "util/serialize.h"
#include "util/string.h" #include "util/string.h"
#include <SDL2/SDL.h>
namespace ui namespace ui
{ {
SizeI getTextureSize(video::ITexture *texture) SizeI getTextureSize(video::ITexture *texture)
@ -52,6 +54,30 @@ namespace ui
return nullptr; 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 const std::string *Window::getStyleStr(u32 index) const
{ {
if (index < m_style_strs.size()) { if (index < m_style_strs.size()) {
@ -68,10 +94,18 @@ namespace ui
m_root_elem = nullptr; m_root_elem = nullptr;
m_style_strs.clear(); 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) 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; std::unordered_map<Elem *, std::string> elem_contents;
readElems(is, elem_contents); readElems(is, elem_contents);
@ -85,8 +119,28 @@ namespace ui
m_type = toWindowType(readU8(is)); m_type = toWindowType(readU8(is));
} }
// Finally, we can proceed to read in all the element properties. // After the unconditional properties, read the conditional ones.
return updateElems(elem_contents); 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 float Window::getScale() const
@ -100,6 +154,14 @@ namespace ui
return size / getScale(); 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) void Window::drawRect(RectF dst, RectF clip, video::SColor color)
{ {
if (dst.intersectWith(clip).empty() || color.getAlpha() == 0) { if (dst.intersectWith(clip).empty() || color.getAlpha() == 0) {
@ -142,10 +204,326 @@ namespace ui
RectF layout_rect(getScreenSize()); RectF layout_rect(getScreenSize());
backdrop.relayout(layout_rect, layout_rect); 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. // Draw all of the newly layouted and updated elements.
backdrop.draw(); 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, void Window::readElems(std::istream &is,
std::unordered_map<Elem *, std::string> &elem_contents) 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 // 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 // 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; errorstream << "Window " << m_id << " has orphaned elements" << std::endl;
return false; 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; return true;
} }

View file

@ -14,6 +14,8 @@
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
union SDL_Event;
namespace ui namespace ui
{ {
class Root; class Root;
@ -37,6 +39,11 @@ namespace ui
class Window class Window
{ {
private: 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; static constexpr size_t MAX_TREE_DEPTH = 64;
// The ID and type are intrinsic to the box's identity, so they aren't // 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; std::vector<std::string> m_style_strs;
Elem *m_focused_elem;
Elem *m_hovered_elem;
bool m_allow_close;
u32 m_events;
public: public:
Window(u64 id) : Window(u64 id) :
m_id(id) m_id(id)
@ -67,7 +80,13 @@ namespace ui
const std::vector<Elem *> &getElems() { return m_ordered_elems; } const std::vector<Elem *> &getElems() { return m_ordered_elems; }
Elem *getElem(const std::string &id, bool required); Elem *getElem(const std::string &id, bool required);
Elem *getNextElem(Elem *elem, bool reverse);
Root *getRoot() { return m_root_elem; } 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; const std::string *getStyleStr(u32 index) const;
@ -77,6 +96,7 @@ namespace ui
float getScale() const; float getScale() const;
SizeF getScreenSize() const; SizeF getScreenSize() const;
PosF getPointerPos() const;
void drawRect(RectF dst, RectF clip, video::SColor color); void drawRect(RectF dst, RectF clip, video::SColor color);
void drawTexture(RectF dst, RectF clip, video::ITexture *texture, void drawTexture(RectF dst, RectF clip, video::ITexture *texture,
@ -84,13 +104,40 @@ namespace ui
void drawAll(); void drawAll();
bool isFocused() const;
bool processInput(const SDL_Event &event);
private: 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, void readElems(std::istream &is,
std::unordered_map<Elem *, std::string> &elem_contents); std::unordered_map<Elem *, std::string> &elem_contents);
bool readRootElem(std::istream &is); bool readRootElem(std::istream &is);
void readStyles(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); bool updateTree(Elem *elem, size_t depth);
}; };
} }