Skip to content

Commit d3ac256

Browse files
committed
Create UI event handling infrastructure
1 parent 0953dd8 commit d3ac256

24 files changed

+1015
-24
lines changed

builtin/ui/window.lua

+119-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ function ui.Window:new(props)
5454
self._style = props.style or ui.Style{}
5555

5656
self._root = props.root
57+
self._focused = props.focused
58+
59+
self._uncloseable = props.uncloseable
60+
61+
self._on_close = props.on_close
62+
self._on_submit = props.on_submit
63+
self._on_focus_change = props.on_focus_change
5764

5865
assert(ui._window_types[self._type], "Invalid window type")
5966
assert(core.is_instance(self._root, ui.Root),
@@ -71,18 +78,35 @@ function ui.Window:new(props)
7178
assert(elem._window == nil, "Element already has window")
7279
elem._window = self
7380
end
81+
82+
if self._focused and self._focused ~= "" then
83+
assert(self._elems_by_id[self._focused],
84+
"Invalid focused element: '" .. self._focused .. "'")
85+
end
7486
end
7587

7688
function ui.Window:_encode(player, opening)
7789
local enc_styles = self:_encode_styles()
7890
local enc_elems = self:_encode_elems()
7991

92+
local fl = ui._make_flags()
93+
94+
if ui._shift_flag(fl, self._focused) then
95+
ui._encode_flag(fl, "z", self._focused)
96+
end
97+
if opening then
98+
ui._shift_flag(fl, self._uncloseable)
99+
end
100+
101+
ui._shift_flag(fl, self._on_submit)
102+
ui._shift_flag(fl, self._on_focus_change)
103+
80104
local data = ui._encode("ZzZ", enc_elems, self._root._id, enc_styles)
81105
if opening then
82106
data = ui._encode("ZB", data, ui._window_types[self._type])
83107
end
84108

85-
return data
109+
return ui._encode("ZZ", data, ui._encode_flags(fl))
86110
end
87111

88112
function ui.Window:_encode_styles()
@@ -215,6 +239,68 @@ function ui.Window:_encode_elems()
215239
return ui._encode_array("Z", enc_elems)
216240
end
217241

242+
local event_handlers = {
243+
[0x00] = function(window, ev, data)
244+
-- We should never receive an event for an uncloseable window. If we
245+
-- did, this player might be trying to cheat.
246+
if window._uncloseable then
247+
core.log("action", "Player '" .. open_windows[window._id].player ..
248+
"' closed uncloseable window")
249+
return
250+
end
251+
252+
-- Since the window is now closed, remove the open window data.
253+
open_windows[window._id] = nil
254+
return window._on_close
255+
end,
256+
257+
[0x01] = function(window, ev, data)
258+
return window._on_submit
259+
end,
260+
261+
[0x02] = function(window, ev, data)
262+
ev.target = ui._decode("z", data)
263+
264+
-- If the ID for this element doesn't exist, we probably updated the
265+
-- window to remove the element. Assume nothing is focused then.
266+
if not window._elems_by_id[ev.target] then
267+
ev.target = ""
268+
end
269+
270+
return window._on_focus_change
271+
end,
272+
}
273+
274+
function ui.Window:_on_window_event(event, data)
275+
-- There are no events that should fire for non-GUI windows.
276+
if self._type ~= "gui" then
277+
core.log("info", "Non-GUI window received event: " .. event)
278+
return
279+
end
280+
281+
-- Prepare the basic window event table.
282+
local info = open_windows[self._id]
283+
local ev = {
284+
window = self._id,
285+
player = info.player,
286+
context = info.context,
287+
}
288+
289+
-- Get the handler function for this event if we recognize it.
290+
local handler = event_handlers[event]
291+
if not handler then
292+
core.log("info", "Invalid window event: " .. event)
293+
return
294+
end
295+
296+
-- If the event handler returned a callback function for the user, call it
297+
-- with the event table.
298+
local callback = handler(self, ev, data)
299+
if callback then
300+
callback(ev)
301+
end
302+
end
303+
218304
local OPEN_WINDOW = 0x00
219305
local REOPEN_WINDOW = 0x01
220306
local UPDATE_WINDOW = 0x02
@@ -305,3 +391,35 @@ function ui.get_open_windows()
305391
end
306392
return ids
307393
end
394+
395+
local WINDOW_EVENT = 0x00
396+
local ELEM_EVENT = 0x01
397+
398+
function core.receive_ui_message(player, data)
399+
local action, id, data = ui._decode("BL Z", data, -1)
400+
401+
local info = open_windows[id]
402+
if not info then
403+
-- Discard events for any window that isn't currently open, since
404+
-- it's probably due to network latency and events coming late.
405+
core.log("info", "Window " .. id .. " is not open")
406+
return
407+
end
408+
409+
if info.player ~= player then
410+
-- The player doesn't match up with what we expected, so ignore the
411+
-- (probably malicious) event.
412+
core.log("action", "Window " .. id .. " has player '" .. info.player ..
413+
"', but received event from player '" .. player .. "'")
414+
return
415+
end
416+
417+
if action == WINDOW_EVENT then
418+
local event, data = ui._decode("BZ", data, -1)
419+
info.window:_on_window_event(event, data)
420+
elseif action == ELEM_EVENT then
421+
-- TODO
422+
else
423+
core.log("info", "Invalid window action: " .. action)
424+
end
425+
end

src/client/client.cpp

+8
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,14 @@ void Client::sendInventoryFields(const std::string &formname,
12901290
Send(&pkt);
12911291
}
12921292

1293+
void Client::sendUiMessage(const char *data, size_t len)
1294+
{
1295+
NetworkPacket pkt(TOSERVER_UI_MESSAGE, 0);
1296+
pkt.putRawString(data, len);
1297+
1298+
Send(&pkt);
1299+
}
1300+
12931301
void Client::sendInventoryAction(InventoryAction *a)
12941302
{
12951303
std::ostringstream os(std::ios_base::binary);

src/client/client.h

+1
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ class Client : public con::PeerHandler, public InventoryManager, public IGameDef
242242
const StringMap &fields);
243243
void sendInventoryFields(const std::string &formname,
244244
const StringMap &fields);
245+
void sendUiMessage(const char *data, size_t len);
245246
void sendInventoryAction(InventoryAction *a);
246247
void sendChatMessage(const std::wstring &message);
247248
void clearOutChatQueue();

src/client/clientlauncher.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
2222
#include "server.h"
2323
#include "filesys.h"
2424
#include "gui/guiMainMenu.h"
25+
#include "gui/manager.h"
2526
#include "game.h"
2627
#include "player.h"
2728
#include "chat.h"
@@ -48,7 +49,7 @@ MainMenuManager g_menumgr;
4849

4950
bool isMenuActive()
5051
{
51-
return g_menumgr.menuCount() != 0;
52+
return g_menumgr.menuCount() != 0 || ui::g_manager.isFocused();
5253
}
5354

5455
// Passed to menus to allow disconnecting and exiting

src/client/gameui.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ void GameUI::update(const RunStats &stats, Client *client, MapDrawControl *draw_
175175
m_guitext2->setVisible(m_flags.show_basic_debug);
176176

177177
setStaticText(m_guitext_info, m_infotext.c_str());
178-
m_guitext_info->setVisible(m_flags.show_hud && g_menumgr.menuCount() == 0);
178+
m_guitext_info->setVisible(m_flags.show_hud && !isMenuActive());
179179

180180
static const float statustext_time_max = 1.5f;
181181

src/gui/box.cpp

+156
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
2727
#include "gui/window.h"
2828
#include "util/serialize.h"
2929

30+
#include <SDL2/SDL.h>
31+
3032
namespace ui
3133
{
3234
Align toAlign(u8 align)
@@ -186,6 +188,11 @@ namespace ui
186188
return m_elem.getWindow();
187189
}
188190

191+
bool Box::isFocused() const
192+
{
193+
return m_elem.isFocused();
194+
}
195+
189196
void Box::reset()
190197
{
191198
m_style.reset();
@@ -302,6 +309,148 @@ namespace ui
302309
drawForeground(canvas);
303310
}
304311

312+
bool Box::isPointerInside() const
313+
{
314+
return rect_contains(m_clip_rect, getWindow().getPointerPos());
315+
}
316+
317+
bool Box::processInput(const SDL_Event &event)
318+
{
319+
// If this is a static box, we ignore events entirely.
320+
if (!(m_flags & DYNAMIC)) {
321+
return false;
322+
}
323+
324+
// If the box was triggered after the last event, it isn't anymore.
325+
m_was_triggered = false;
326+
327+
switch (event.type) {
328+
case SDL_KEYDOWN:
329+
case SDL_KEYUP:
330+
// If the spacebar should be detected, change the pressed state
331+
// based on whether the spacebar was pressed or released. Ignore
332+
// key repeats to avoid immediately pressing a box again when the
333+
// mouse was released to unpress it.
334+
if (m_flags & USE_SPACE &&
335+
event.key.keysym.sym == SDLK_SPACE && !event.key.repeat) {
336+
setPressed(event.type == SDL_KEYDOWN, event.type == SDL_KEYUP);
337+
return true;
338+
}
339+
return false;
340+
341+
case SDL_MOUSEMOTION:
342+
// Peek at the event to see if it affected whether we're pressed.
343+
updatePressed();
344+
return false;
345+
346+
case SDL_MOUSEBUTTONDOWN:
347+
// If the left mouse button was pressed while this box was hovered,
348+
// this box is now a pressed box.
349+
if (isHovered() && event.button.button == SDL_BUTTON_LEFT) {
350+
setPressed(true, false);
351+
return true;
352+
}
353+
return false;
354+
355+
case SDL_MOUSEBUTTONUP:
356+
// If the left mouse button was released anywhere (not just inside
357+
// this box), then we're no longer pressed. The press only counts
358+
// as a trigger if the box is hovered by the mouse.
359+
if (event.button.button == SDL_BUTTON_LEFT) {
360+
setPressed(false, isHovered());
361+
362+
// If the box is hovered when released, handle the event.
363+
// Otherwise, we're just peeking.
364+
return isHovered();
365+
}
366+
return false;
367+
368+
case SDL_USEREVENT:
369+
switch (event.user.code) {
370+
case UI_FOCUS_REQUEST:
371+
// We're a dynamic box, so we can be focused.
372+
return true;
373+
374+
case UI_FOCUS_CHANGED:
375+
// If focus was lost, unpress this box. If focus was gained,
376+
// it's possible for a non-holdable box to become pressed.
377+
if (event.user.data1 == &m_elem) {
378+
setPressed(false, false);
379+
}
380+
updatePressed();
381+
return false;
382+
383+
case UI_FOCUS_SUBVERTED:
384+
// If focus was subverted by another element or window, unpress
385+
// this box. For non-holdable boxes, this will only unpress if
386+
// a window subverted the focus, not an element.
387+
setPressed(false, false);
388+
return false;
389+
390+
case UI_HOVER_REQUEST:
391+
// If the mouse is inside this specific box, accept the hover.
392+
return isPointerInside();
393+
394+
case UI_HOVER_CHANGED:
395+
// If the hovered element changed, make ourself hovered if the
396+
// mouse is inside this box and our parent element is the newly
397+
// hovered element. For non-holdable boxes, this may also cause
398+
// the element to become pressed.
399+
m_is_hovered = isPointerInside() && event.user.data2 == &m_elem;
400+
updatePressed();
401+
return true;
402+
403+
default:
404+
break;
405+
}
406+
return false;
407+
408+
default:
409+
break;
410+
}
411+
412+
return false;
413+
}
414+
415+
void Box::setPressed(bool press, bool trigger)
416+
{
417+
if (m_flags & HOLDABLE) {
418+
// If the box was already pressed and is being unpressed, and if
419+
// the last event was a trigger, then the box has been triggered.
420+
if (m_is_pressed && !press && trigger) {
421+
m_was_triggered = trigger;
422+
}
423+
424+
// Then, update whether the box is actually pressed.
425+
m_is_pressed = press;
426+
}
427+
428+
updatePressed();
429+
}
430+
431+
void Box::updatePressed()
432+
{
433+
if (!(m_flags & HOLDABLE)) {
434+
bool was_pressed = m_is_pressed;
435+
436+
// The box must be focused to be pressed. If the left mouse button
437+
// is pressed while the box is hovered, the box is pressed.
438+
// Alternatively, the box is pressed if the spacebar is down when
439+
// the spacebar is set to be detected.
440+
bool mouse_down = g_manager.isPointerPressed();
441+
bool space_down = SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_SPACE];
442+
443+
m_is_pressed = isFocused() &&
444+
((isHovered() && mouse_down) || (m_flags & USE_SPACE && space_down));
445+
446+
// If the box went from not being pressed to being pressed, then
447+
// the box has been triggered.
448+
if (!was_pressed && m_is_pressed) {
449+
m_was_triggered = true;
450+
}
451+
}
452+
}
453+
305454
void Box::drawForeground(Canvas &canvas)
306455
{
307456
// It makes no sense to draw a foreground when there's no image, since
@@ -469,6 +618,13 @@ namespace ui
469618
m_style.reset();
470619
State state = STATE_NONE;
471620

621+
if (isFocused())
622+
state |= STATE_FOCUSED;
623+
if (isHovered())
624+
state |= STATE_HOVERED;
625+
if (isPressed())
626+
state |= STATE_PRESSED;
627+
472628
// Loop over each style state from lowest precedence to highest since
473629
// they should be applied in that order.
474630
for (State i = 0; i < m_style_refs.size(); i++) {

0 commit comments

Comments
 (0)