diff --git a/builtin/ui/context.lua b/builtin/ui/context.lua index 58137e129..efcadd2c6 100644 --- a/builtin/ui/context.lua +++ b/builtin/ui/context.lua @@ -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 diff --git a/builtin/ui/elem.lua b/builtin/ui/elem.lua index 9d0d6274c..dde3ddb80 100644 --- a/builtin/ui/elem.lua +++ b/builtin/ui/elem.lua @@ -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 diff --git a/builtin/ui/window.lua b/builtin/ui/window.lua index 8da479f64..72fc73868 100644 --- a/builtin/ui/window.lua +++ b/builtin/ui/window.lua @@ -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 diff --git a/irr/include/IEventReceiver.h b/irr/include/IEventReceiver.h index 332b23158..c886f72c1 100644 --- a/irr/include/IEventReceiver.h +++ b/irr/include/IEventReceiver.h @@ -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; diff --git a/irr/src/CIrrDeviceSDL.cpp b/irr/src/CIrrDeviceSDL.cpp index bc7627f73..ea35d7b3a 100644 --- a/irr/src/CIrrDeviceSDL.cpp +++ b/irr/src/CIrrDeviceSDL.cpp @@ -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(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(SDL_event.user.data1); irrevent.UserEvent.UserData2 = reinterpret_cast(SDL_event.user.data2); - - postEventFromUser(irrevent); break; case SDL_FINGERDOWN: @@ -961,8 +959,6 @@ bool CIrrDeviceSDL::run() irrevent.TouchInput.Y = static_cast(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(SDL_event.tfinger.x * Width); irrevent.TouchInput.Y = static_cast(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 diff --git a/src/client/client.cpp b/src/client/client.cpp index 5147dd2c9..ee7a14c69 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -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); diff --git a/src/client/client.h b/src/client/client.h index 8ce4149b3..a8e6909a8 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -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(); diff --git a/src/client/gameui.cpp b/src/client/gameui.cpp index 53fae50c0..ff8a64586 100644 --- a/src/client/gameui.cpp +++ b/src/client/gameui.cpp @@ -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; diff --git a/src/gui/mainmenumanager.h b/src/gui/mainmenumanager.h index 553d6ffce..b4d51172e 100644 --- a/src/gui/mainmenumanager.h +++ b/src/gui/mainmenumanager.h @@ -7,12 +7,17 @@ /* All kinds of stuff that needs to be exposed from main.cpp */ +#include "config.h" #include "modalMenu.h" #include #include #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(m_stack.back()); - return mm && mm->preprocessEvent(event); + if (!m_stack.empty()) { + GUIModalMenu *mm = dynamic_cast(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 diff --git a/src/network/clientopcodes.cpp b/src/network/clientopcodes.cpp index a02dc5634..9c4c5c2f9 100644 --- a/src/network/clientopcodes.cpp +++ b/src/network/clientopcodes.cpp @@ -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 diff --git a/src/network/networkprotocol.cpp b/src/network/networkprotocol.cpp index 610fe5c4f..d8388b190 100644 --- a/src/network/networkprotocol.cpp +++ b/src/network/networkprotocol.cpp @@ -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 diff --git a/src/network/networkprotocol.h b/src/network/networkprotocol.h index 4ae563e7f..ca1f54e80 100644 --- a/src/network/networkprotocol.h +++ b/src/network/networkprotocol.h @@ -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 diff --git a/src/network/serveropcodes.cpp b/src/network/serveropcodes.cpp index 96ed4d60b..31149706f 100644 --- a/src/network/serveropcodes.cpp +++ b/src/network/serveropcodes.cpp @@ -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 diff --git a/src/network/serverpackethandler.cpp b/src/network/serverpackethandler.cpp index e9012d351..dfa1cd4af 100644 --- a/src/network/serverpackethandler.cpp +++ b/src/network/serverpackethandler.cpp @@ -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(); diff --git a/src/script/cpp_api/s_server.cpp b/src/script/cpp_api/s_server.cpp index faacf9714..88ba2e5d4 100644 --- a/src/script/cpp_api/s_server.cpp +++ b/src/script/cpp_api/s_server.cpp @@ -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)); +} diff --git a/src/script/cpp_api/s_server.h b/src/script/cpp_api/s_server.h index 148cdfa84..18cacc9e6 100644 --- a/src/script/cpp_api/s_server.h +++ b/src/script/cpp_api/s_server.h @@ -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 &result); diff --git a/src/server.h b/src/server.h index 5582eb9b6..a1fb12e77 100644 --- a/src/server.h +++ b/src/server.h @@ -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); diff --git a/src/ui/box.cpp b/src/ui/box.cpp index 345517be9..86d1f59c4 100644 --- a/src/ui/box.cpp +++ b/src/ui/box.cpp @@ -12,6 +12,8 @@ #include "ui/window.h" #include "util/serialize.h" +#include + 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); + } + } } diff --git a/src/ui/box.h b/src/ui/box.h index 2a874e89b..d2530138c 100644 --- a/src/ui/box.h +++ b/src/ui/box.h @@ -13,6 +13,8 @@ #include #include +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 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 &getContent() const { return m_content; } void setContent(std::vector 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); }; } diff --git a/src/ui/elem.cpp b/src/ui/elem.cpp index bcc87691f..4f2ffc2a1 100644 --- a/src/ui/elem.cpp +++ b/src/ui/elem.cpp @@ -13,6 +13,8 @@ // Include every element header for Elem::create() #include "ui/static_elems.h" +#include + namespace ui { std::unique_ptr 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); diff --git a/src/ui/elem.h b/src/ui/elem.h index 0ee388902..39c0f63c7 100644 --- a/src/ui/elem.h +++ b/src/ui/elem.h @@ -13,10 +13,17 @@ #include #include +union SDL_Event; + namespace ui { class Window; +#define UI_CALLBACK(method) \ + [](Elem &elem) { \ + static_cast(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 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 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; diff --git a/src/ui/manager.cpp b/src/ui/manager.cpp index 5a8764578..e557fae2b 100644 --- a/src/ui/manager.cpp +++ b/src/ui/manager.cpp @@ -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 + 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; } diff --git a/src/ui/manager.h b/src/ui/manager.h index f9d0f8b7f..4c9d1840c 100644 --- a/src/ui/manager.h +++ b/src/ui/manager.h @@ -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 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 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; diff --git a/src/ui/static_elems.cpp b/src/ui/static_elems.cpp index 0ff5d70d3..a3894d0c0 100644 --- a/src/ui/static_elems.cpp +++ b/src/ui/static_elems.cpp @@ -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(); + } } diff --git a/src/ui/static_elems.h b/src/ui/static_elems.h index 146f4c570..ff2bb61b1 100644 --- a/src/ui/static_elems.h +++ b/src/ui/static_elems.h @@ -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; }; } diff --git a/src/ui/window.cpp b/src/ui/window.cpp index 1f38715c7..8b9c65aa4 100644 --- a/src/ui/window.cpp +++ b/src/ui/window.cpp @@ -16,6 +16,8 @@ #include "util/serialize.h" #include "util/string.h" +#include + 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_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_contents) { @@ -253,7 +631,8 @@ namespace ui } } - bool Window::updateElems(std::unordered_map &elem_contents) + bool Window::updateElems(std::unordered_map &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; } diff --git a/src/ui/window.h b/src/ui/window.h index 234418329..7a528d4eb 100644 --- a/src/ui/window.h +++ b/src/ui/window.h @@ -14,6 +14,8 @@ #include #include +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 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 &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_contents); bool readRootElem(std::istream &is); void readStyles(std::istream &is); - bool updateElems(std::unordered_map &elem_contents); + bool updateElems(std::unordered_map &elem_contents, + bool set_focus, Elem *new_focused); bool updateTree(Elem *elem, size_t depth); }; }