Remote menus - client side part

This commit is contained in:
tovjemam 2026-03-28 16:40:15 +01:00
parent d43ca2a45a
commit b69fe6d744
17 changed files with 638 additions and 74 deletions

View File

@ -84,6 +84,8 @@ set(CLIENT_ONLY_SOURCES
"src/gameview/entityview.cpp"
"src/gameview/mapinstanceview.hpp"
"src/gameview/mapinstanceview.cpp"
"src/gameview/remote_menu_view.hpp"
"src/gameview/remote_menu_view.cpp"
"src/gameview/simple_entity_view.hpp"
"src/gameview/simple_entity_view.cpp"
"src/gameview/skinning_ubo.hpp"

View File

@ -6,6 +6,7 @@
#include "net/outmessage.hpp"
#include "assets/cache.hpp"
#include "gameview/worldview.hpp"
#include "gameview/utils.hpp"
App::App() :
gui_(dlist_, assets::CacheManager::GetFont("data/comic32.font"))
@ -64,7 +65,7 @@ void App::Frame()
if (menu_)
{
auto menu_size = menu_->MeasureSize();
menu_->Draw(gui_, glm::vec2(viewport_size_) - menu_size - 10.0f);
menu_->Draw(gui_, (glm::vec2(viewport_size_) - menu_size) * 0.5f);
}
gui_.Render();
@ -114,20 +115,6 @@ void App::Disconnected(const std::string& reason)
session_.reset();
}
static bool InputToMenuInput(game::PlayerInputType in, gui::MenuInput& mi)
{
switch (in)
{
case game::IN_FORWARD: mi = gui::MI_UP; return true;
case game::IN_BACKWARD: mi = gui::MI_DOWN; return true;
case game::IN_LEFT: mi = gui::MI_LEFT; return true;
case game::IN_RIGHT: mi = gui::MI_RIGHT; return true;
case game::IN_JUMP: mi = gui::MI_ENTER; return true;
case game::IN_CROUCH: mi = gui::MI_BACK; return true;
default: return false;
}
};
void App::Input(game::PlayerInputType in, bool pressed, bool repeated)
{
if (in == game::IN_MENU && pressed)
@ -215,7 +202,7 @@ static void AddSlider(gui::Menu& menu, std::string text, int& value, int min, in
else if (value > max)
value = max;
slider.SetSelectionText(std::to_string(value));
slider.SetSelectionText(std::to_string(value) + " %");
changed();
};
@ -226,6 +213,7 @@ static void AddSlider(gui::Menu& menu, std::string text, int& value, int min, in
void App::OpenSettings()
{
menu_ = std::make_unique<gui::Menu>();
menu_->SetTitle("nastavení");
AddSlider(*menu_, "jak moc to řve", volume_, 0, 100, [this]{
ApplyVolume();

View File

@ -86,6 +86,10 @@ void game::Game::PlayerInput(Player& player, PlayerInputType type, bool enabled)
break;
}
case IN_DEBUG3:
DisplayTestMenu(player);
break;
default: {
auto world = FindPlayerWorld(player);
if (world)
@ -201,3 +205,36 @@ game::EnterableWorld* game::Game::FindPlayerWorld(Player& player) const
return it->second.world;
}
void game::Game::DisplayTestMenu(Player& player)
{
if (player.HasOpenMenu())
return;
auto& menu = player.DisplayMenu("test");
auto& btn_echo = menu.AddItem(RM_BUTTON, "echo");
btn_echo.SetOnClick([&player] {
player.SendChat("echo test");
});
auto& btn_bc = menu.AddItem(RM_BUTTON, "broadcast");
btn_bc.SetOnClick([this, &player] {
BroadcastChat(player.GetName() + "^r mele hovna");
});
int test = 0;
auto& sel_test = menu.AddItem(RM_SELECT, "výběr");
sel_test.SetOnSelect([test, &sel_test] (int dir) mutable {
test += dir;
sel_test.SetSelection(std::to_string(test));
});
sel_test.SetSelection(std::to_string(test));
auto& btn_close = menu.AddItem(RM_BUTTON, "zavřít");
btn_close.SetOnClick([&menu, &player] {
player.CloseMenu(menu);
});
}

View File

@ -42,6 +42,8 @@ private:
PlayerGameInfo& GetPlayerInfo(Player& player);
EnterableWorld* FindPlayerWorld(Player& player) const;
void DisplayTestMenu(Player& player);
private:
std::shared_ptr<OpenWorld> openworld_;
std::shared_ptr<EnterableWorld> testworld_;

View File

@ -23,6 +23,9 @@ bool game::Player::ProcessMsg(net::MessageType type, net::InMessage& msg)
case net::MSG_VIEWANGLES:
return ProcessViewAnglesMsg(msg);
case net::MSG_MENUACTION:
return ProcessMenuActionMsg(msg);
default:
return false;
}
@ -30,20 +33,8 @@ bool game::Player::ProcessMsg(net::MessageType type, net::InMessage& msg)
void game::Player::Update()
{
if (world_ != known_world_)
{
SendWorldMsg();
known_world_ = world_;
known_ents_.clear();
return; // send updates next frame
}
if (world_)
{
SendWorldUpdateMsg();
SyncEntities();
}
SyncWorld();
SendMenuMsgs();
}
void game::Player::SetWorld(World* world)
@ -77,11 +68,58 @@ void game::Player::SetUseTarget(const std::string& text, const std::string& erro
msg.Write<net::UseDelayQ>(delay);
}
game::RemoteMenu& game::Player::DisplayMenu(std::string title)
{
if (remote_menu_)
throw std::runtime_error("cannot display multiple menus atm");
net::MenuId id = ++menu_id_;
remote_menu_ = std::make_unique<RemoteMenu>(id, std::move(title));
// send msg
auto msg = BeginMsg(net::MSG_REMOTEMENU);
msg.Write(id);
msg.Write(net::MMSG_CREATE);
return *remote_menu_;
}
void game::Player::CloseMenu(const RemoteMenu& menu)
{
if (&menu != remote_menu_.get())
return;
// send msg
auto msg = BeginMsg(net::MSG_REMOTEMENU);
msg.Write(menu.GetId());
msg.Write(net::MMSG_CLOSE);
remote_menu_.reset();
}
game::Player::~Player()
{
game_.PlayerLeft(*this);
}
void game::Player::SyncWorld()
{
if (world_ != known_world_)
{
SendWorldMsg();
known_world_ = world_;
known_ents_.clear();
return; // send updates next frame
}
if (world_)
{
SendWorldUpdateMsg();
SyncEntities();
}
}
void game::Player::SendWorldMsg()
{
MSGDEBUG(std::cout << "seding CHWORLD" << std::endl;)
@ -255,6 +293,20 @@ bool game::Player::ProcessViewAnglesMsg(net::InMessage& msg)
return true;
}
bool game::Player::ProcessMenuActionMsg(net::InMessage& msg)
{
net::MenuId id;
net::MenuActionType type;
if (!msg.Read(id) || !msg.Read(type))
return false;
if (!remote_menu_ || remote_menu_->GetId() != id)
return true; // not illegal, might be just a late message
return remote_menu_->ProcessActionMsg(msg, type);
}
void game::Player::Input(PlayerInputType type, bool enabled)
{
if (enabled)
@ -265,3 +317,20 @@ void game::Player::Input(PlayerInputType type, bool enabled)
game_.PlayerInput(*this, type, enabled);
}
void game::Player::SendMenuMsgs()
{
if (!remote_menu_)
return;
remote_menu_->Update();
auto menu_msg = remote_menu_->GetMsg();
if (menu_msg.empty())
return;
auto msg = BeginMsg();
msg.Write(menu_msg);
remote_menu_->ResetMsg();
}

View File

@ -12,6 +12,8 @@
#include "player_input.hpp"
#include "remote_menu.hpp"
namespace game
{
@ -34,6 +36,10 @@ public:
void SendChat(const std::string& text);
void SetUseTarget(const std::string& text, const std::string& error_text, float delay);
RemoteMenu& DisplayMenu(std::string title);
void CloseMenu(const RemoteMenu& menu);
bool HasOpenMenu() const { return (bool)remote_menu_; }
const std::string& GetName() const { return name_; }
PlayerInputFlags GetInput() const { return in_; }
@ -44,6 +50,7 @@ public:
private:
// world sync
void SyncWorld();
void SendWorldMsg();
void SendWorldUpdateMsg();
@ -56,10 +63,14 @@ private:
// msg handlers
bool ProcessInputMsg(net::InMessage& msg);
bool ProcessViewAnglesMsg(net::InMessage& msg);
bool ProcessMenuActionMsg(net::InMessage& msg);
// events
void Input(PlayerInputType type, bool enabled);
// menu sync
void SendMenuMsgs();
private:
Game& game_;
std::string name_;
@ -73,6 +84,11 @@ private:
net::EntNum cam_ent_ = 0;
glm::vec3 cull_pos_ = glm::vec3(0.0f);
// menus
// TODO: allow more menus
net::MenuId menu_id_ = 0;
std::unique_ptr<RemoteMenu> remote_menu_;
};
}

View File

@ -1,9 +1,8 @@
#include "remote_menu.hpp"
#include "net/defs.hpp"
game::RemoteMenuItem::RemoteMenuItem(RemoteMenu& menu, RemoteMenuItemType type, std::string text) : menu_(menu), type_(type)
game::RemoteMenuItem::RemoteMenuItem(RemoteMenu& menu, RemoteMenuItemType type, std::string text) : menu_(menu), type_(type), text_(std::move(text))
{
}
void game::RemoteMenuItem::SetText(std::string text)
@ -26,7 +25,7 @@ void game::RemoteMenuItem::SetSelection(std::string selection)
menu_.items_synced_ = false;
}
game::RemoteMenu::RemoteMenu(net::MenuId id) : id_(id)
game::RemoteMenu::RemoteMenu(net::MenuId id, std::string title) : id_(id), title_(std::move(title))
{
}
@ -41,6 +40,20 @@ game::RemoteMenuItem& game::RemoteMenu::AddItem(RemoteMenuItemType type, std::st
return item_ref;
}
bool game::RemoteMenu::ProcessActionMsg(net::InMessage& msg, net::MenuActionType type)
{
switch (type)
{
case net::MA_CLICK:
return ProcessClickMsg(msg);
case net::MA_SELECT:
return ProcessSelectMsg(msg);
case net::MA_HOVER:
return ProcessHoverMsg(msg);
default:
return false;
}
}
void game::RemoteMenu::Update()
{
@ -51,7 +64,7 @@ void game::RemoteMenu::Update()
if (!synced_)
{
auto msg = BeginMenuMsg(net::MMSG_UPDATE);
msg.Write(net::MenuTitle(title_));
msg.Write<net::MenuItemCount>(items_.size());
for (auto& item : items_)
{
@ -103,3 +116,62 @@ net::OutMessage game::RemoteMenu::BeginMenuMsg(net::MenuMessageType type)
msg.Write(type);
return msg;
}
bool game::RemoteMenu::ProcessClickMsg(net::InMessage& msg)
{
net::MenuItemId id;
if (!msg.Read(id))
return false;
if (id > items_.size())
return true; // not illegal
auto& cb = items_[id]->on_click_;
if (cb)
cb();
return true;
}
bool game::RemoteMenu::ProcessSelectMsg(net::InMessage& msg)
{
net::MenuItemId id;
net::MenuSelectDir dir;
if (!msg.Read(id) || !msg.Read(dir))
return false;
if (id > items_.size())
return true; // not illegal
int idir = dir ? 1 : -1;
auto& cb = items_[id]->on_select_;
if (cb)
cb(idir);
return true;
}
bool game::RemoteMenu::ProcessHoverMsg(net::InMessage& msg)
{
net::MenuItemId id;
if (!msg.Read(id))
return false;
if (id > items_.size())
return true; // not illegal
if (id == hovered_)
return true; // already hovered
hovered_ = id;
auto& cb = items_[id]->on_hovered_;
if (cb)
cb();
return true;
}

View File

@ -6,6 +6,7 @@
#include "net/defs.hpp"
#include "net/msg_producer.hpp"
#include "net/outmessage.hpp"
#include "net/inmessage.hpp"
namespace game
{
@ -54,17 +55,27 @@ private:
class RemoteMenu : public net::MsgProducer
{
public:
RemoteMenu(net::MenuId id);
RemoteMenu(net::MenuId id, std::string title);
RemoteMenuItem& AddItem(RemoteMenuItemType type, std::string text);
net::MenuId GetId() const { return id_; }
bool ProcessActionMsg(net::InMessage& msg, net::MenuActionType type);
void Update();
private:
net::OutMessage BeginMenuMsg(net::MenuMessageType type);
// action handlers
bool ProcessClickMsg(net::InMessage& msg);
bool ProcessSelectMsg(net::InMessage& msg);
bool ProcessHoverMsg(net::InMessage& msg);
private:
net::MenuId id_;
std::string title_;
bool synced_ = false;
bool items_synced_ = false;
std::vector<std::unique_ptr<RemoteMenuItem>> items_;

View File

@ -4,15 +4,16 @@
#include <iostream>
// #include <glm/gtx/common.hpp>
#include "vehicleview.hpp"
#include "utils/version.hpp"
#include "utils.hpp"
#include "vehicleview.hpp"
game::view::ClientSession::ClientSession(App& app) : app_(app), use_target_hud_(app.GetTime())
{
// send login
auto msg = BeginMsg(net::MSG_ID);
// send login
auto msg = BeginMsg(net::MSG_ID);
msg.Write<net::Version>(FEKAL_VERSION);
msg.Write(net::PlayerName(app.GetUserName()));
msg.Write(net::PlayerName(app.GetUserName()));
}
bool game::view::ClientSession::ProcessMessage(net::InMessage& msg)
@ -49,6 +50,9 @@ bool game::view::ClientSession::ProcessSingleMessage(net::MessageType type, net:
case net::MSG_USETARGET:
return ProcessUseTargetMsg(msg);
case net::MSG_REMOTEMENU:
return ProcessMenuMsg(msg);
default:
// try pass the msg to world
if (world_ && world_->ProcessMsg(type, msg))
@ -60,13 +64,13 @@ bool game::view::ClientSession::ProcessSingleMessage(net::MessageType type, net:
void game::view::ClientSession::Input(game::PlayerInputType in, bool pressed, bool repeated)
{
if (pressed && ProcessMenuInput(in))
return;
if (repeated)
return;
SendInput(in, pressed);
}
void game::view::ClientSession::ProcessMouseMove(float delta_yaw, float delta_pitch)
@ -102,6 +106,8 @@ void game::view::ClientSession::Draw(gfx::DrawList& dlist, gfx::DrawListParams&
}
use_target_hud_.Draw(gui);
DrawMenus(gui);
}
void game::view::ClientSession::GetViewInfo(glm::vec3& eye, glm::mat4& view) const
@ -115,7 +121,7 @@ void game::view::ClientSession::GetViewInfo(glm::vec3& eye, glm::mat4& view) con
if (ent)
{
start += ent->GetRoot().GetGlobalPosition();
if (dynamic_cast<const VehicleView*>(ent))
distance = 8.0f;
}
@ -129,7 +135,7 @@ void game::view::ClientSession::GetViewInfo(glm::vec3& eye, glm::mat4& view) con
glm::vec3 end = start - dir * distance;
//start.z -= 0.5f; // shift this a bit to make it better when occluded
// start.z -= 0.5f; // shift this a bit to make it better when occluded
eye = world_->CameraSweep(start, end);
view = glm::lookAt(eye, eye + dir, glm::vec3(0, 0, 1));
}
@ -183,6 +189,39 @@ bool game::view::ClientSession::ProcessUseTargetMsg(net::InMessage& msg)
return true;
}
bool game::view::ClientSession::ProcessMenuMsg(net::InMessage& msg)
{
net::MenuId id;
net::MenuMessageType type;
if (!msg.Read(id) || !msg.Read(type))
return false;
switch (type)
{
case net::MMSG_CREATE: {
if (FindMenu(id))
return false;
remote_menus_.push_back(std::make_unique<RemoteMenuView>(*this, id));
return true;
}
case net::MMSG_CLOSE: {
std::erase_if(remote_menus_, [id](auto& menu) { return menu->GetId() == id; });
return true;
}
default: {
auto menu = FindMenu(id);
if (!menu)
return false;
return menu->ProcessMessage(type, msg);
}
}
}
void game::view::ClientSession::DrawWorld(gfx::DrawList& dlist, gfx::DrawListParams& params, gui::Context& gui)
{
// glm::mat4 view = glm::lookAt(glm::vec3(15.0f, 0.0f, 1.0f), glm::vec3(0.0f, 0.0f, -13.0f), glm::vec3(0.0f,
@ -199,7 +238,8 @@ void game::view::ClientSession::DrawWorld(gfx::DrawList& dlist, gfx::DrawListPar
// glm::mat4 fake_view_proj = glm::perspective(glm::radians(30.0f), aspect, 0.1f, 3000.0f) * view;
game::view::DrawArgs draw_args(dlist, params.env, gui, params.view_proj, eye, glm::ivec2(params.screen_width, params.screen_height), 500.0f);
game::view::DrawArgs draw_args(dlist, params.env, gui, params.view_proj, eye,
glm::ivec2(params.screen_width, params.screen_height), 500.0f);
world_->Draw(draw_args);
glm::mat4 camera_world = glm::inverse(view);
@ -209,10 +249,10 @@ void game::view::ClientSession::DrawWorld(gfx::DrawList& dlist, gfx::DrawListPar
void game::view::ClientSession::SendInput(game::PlayerInputType type, bool enable)
{
auto msg = BeginMsg(net::MSG_IN);
uint8_t val = type;
if (enable)
val |= 128;
msg.Write(val);
uint8_t val = type;
if (enable)
val |= 128;
msg.Write(val);
}
void game::view::ClientSession::SendViewAngles(float time)
@ -236,3 +276,37 @@ void game::view::ClientSession::SendViewAngles(float time)
view_pitch_q_.value = pitch_q.value;
last_send_time_ = time;
}
void game::view::ClientSession::DrawMenus(gui::Context& gui) const
{
if (remote_menus_.empty())
return;
auto& top_menu = *remote_menus_.back();
top_menu.Draw(gui, glm::vec2(20.0f, 100.0f));
}
bool game::view::ClientSession::ProcessMenuInput(game::PlayerInputType in)
{
if (remote_menus_.empty())
return false;
gui::MenuInput mi;
if (!InputToMenuInput(in, mi))
return false;
remote_menus_.back()->Input(mi);
return true;
}
game::view::RemoteMenuView* game::view::ClientSession::FindMenu(net::MenuId id) const
{
for (auto& menu : remote_menus_)
{
if (menu->GetId() == id)
return menu.get();
}
return nullptr;
}

View File

@ -11,6 +11,7 @@
#include "net/msg_producer.hpp"
#include "game/player_input.hpp"
#include "gui/use_target_hud.hpp"
#include "remote_menu_view.hpp"
class App;
@ -41,12 +42,17 @@ private:
bool ProcessCameraMsg(net::InMessage& msg);
bool ProcessChatMsg(net::InMessage& msg);
bool ProcessUseTargetMsg(net::InMessage& msg);
bool ProcessMenuMsg(net::InMessage& msg);
void DrawWorld(gfx::DrawList& dlist, gfx::DrawListParams& params, gui::Context& gui);
void SendInput(game::PlayerInputType type, bool enable);
void SendViewAngles(float time);
void DrawMenus(gui::Context& gui) const;
bool ProcessMenuInput(game::PlayerInputType in);
RemoteMenuView* FindMenu(net::MenuId id) const;
private:
App& app_;
@ -60,6 +66,8 @@ private:
float last_send_time_ = 0.0f;
gui::UseTargetHud use_target_hud_;
std::vector<std::unique_ptr<RemoteMenuView>> remote_menus_;
};
} // namespace game::view

View File

@ -0,0 +1,144 @@
#include "remote_menu_view.hpp"
#include "game/remote_menu.hpp"
#include "client_session.hpp"
game::view::RemoteMenuView::RemoteMenuView(ClientSession& session, net::MenuId id) : session_(session), id_(id) {}
bool game::view::RemoteMenuView::ProcessMessage(net::MenuMessageType type, net::InMessage& msg)
{
switch (type)
{
case net::MMSG_UPDATE:
return ProcessUpdateMsg(msg);
case net::MMSG_ITEM_UPDATE_TEXT:
return ProcessItemUpdateTextMsg(msg);
case net::MMSG_ITEM_UPDATE_SELECTION:
return ProcessItemUpdateSelectionMsg(msg);
default:
return false;
}
}
bool game::view::RemoteMenuView::ProcessUpdateMsg(net::InMessage& msg)
{
net::MenuTitle title;
net::MenuItemCount itemcount;
if (!msg.Read(title) || !msg.Read(itemcount))
return false;
SetTitle(title);
Clear();
for (net::MenuItemId i = 0; i < itemcount; ++i)
{
game::RemoteMenuItemType type;
net::MenuItemText text;
net::MenuItemSelection selection;
if (!msg.Read(type) || !msg.Read(text) || !msg.Read(selection))
return false;
switch (type)
{
case RM_BUTTON:
{
auto& btn = Add<gui::ButtonMenuItem>(text);
btn.SetClickCallback([this, i] { OnItemClick(i); });
break;
}
case RM_SELECT:
{
auto& select = Add<gui::SelectMenuItem>(text);
select.SetSelectionText(selection);
select.SetClickCallback([this, i] { OnItemClick(i); });
select.SetSwitchCallback([this, i] (int dir) { OnItemSelectionChange(i, dir); });
break;
}
default:
break;
}
}
return true;
}
bool game::view::RemoteMenuView::ProcessItemUpdateTextMsg(net::InMessage& msg)
{
net::MenuItemId idx;
net::MenuItemText text;
if (!msg.Read(idx) || !msg.Read(text))
return false;
if (idx >= GetNumItems())
return false;
auto& item = GetItem(idx);
if (auto btn = dynamic_cast<gui::ButtonMenuItem*>(&item); btn)
{
btn->SetText(text);
}
else
{
return false;
}
return true;
}
bool game::view::RemoteMenuView::ProcessItemUpdateSelectionMsg(net::InMessage& msg)
{
net::MenuItemId idx;
net::MenuItemSelection selection;
if (!msg.Read(idx) || !msg.Read(selection))
return false;
if (idx >= GetNumItems())
return false;
auto& item = GetItem(idx);
auto select = dynamic_cast<gui::SelectMenuItem*>(&item);
if (!select)
return false;
select->SetSelectionText(selection);
return true;
}
void game::view::RemoteMenuView::OnItemClick(net::MenuItemId idx)
{
auto msg = BeginActionMsg(net::MA_CLICK);
msg.Write(idx);
}
void game::view::RemoteMenuView::OnItemSelectionChange(net::MenuItemId idx, int dir)
{
if (dir == 0)
return;
auto msg = BeginActionMsg(net::MA_SELECT);
msg.Write(idx);
msg.Write<net::MenuSelectDir>(dir < 0 ? 0 : 1);
}
void game::view::RemoteMenuView::OnFocusChanged()
{
auto msg = BeginActionMsg(net::MA_HOVER);
msg.Write<net::MenuItemId>(GetFocusedItemIndex());
}
net::OutMessage game::view::RemoteMenuView::BeginActionMsg(net::MenuActionType type)
{
auto msg = session_.BeginMsg(net::MSG_MENUACTION);
msg.Write(id_);
msg.Write(type);
return msg;
}

View File

@ -0,0 +1,46 @@
#pragma once
#include "gui/menu.hpp"
// #include "net/msg_producer.hpp"
#include "net/inmessage.hpp"
#include "net/outmessage.hpp"
#include "net/defs.hpp"
namespace game::view
{
class ClientSession;
class RemoteMenuView : public gui::Menu
{
public:
using Super = gui::Menu;
RemoteMenuView(ClientSession& session, net::MenuId id);
bool ProcessMessage(net::MenuMessageType type, net::InMessage& msg);
net::MenuId GetId() const { return id_; }
private:
bool ProcessUpdateMsg(net::InMessage& msg);
bool ProcessItemUpdateTextMsg(net::InMessage& msg);
bool ProcessItemUpdateSelectionMsg(net::InMessage& msg);
void OnItemClick(net::MenuItemId idx);
void OnItemSelectionChange(net::MenuItemId idx, int dir);
protected:
void OnFocusChanged() override;
net::OutMessage BeginActionMsg(net::MenuActionType type);
private:
ClientSession& session_;
net::MenuId id_;
};
}

19
src/gameview/utils.hpp Normal file
View File

@ -0,0 +1,19 @@
#pragma once
#include "game/player_input.hpp"
#include "gui/menu.hpp"
inline bool InputToMenuInput(game::PlayerInputType in, gui::MenuInput& mi)
{
switch (in)
{
case game::IN_FORWARD: mi = gui::MI_UP; return true;
case game::IN_BACKWARD: mi = gui::MI_DOWN; return true;
case game::IN_LEFT: mi = gui::MI_LEFT; return true;
case game::IN_RIGHT: mi = gui::MI_RIGHT; return true;
case game::IN_JUMP: mi = gui::MI_ENTER; return true;
case game::IN_USE: mi = gui::MI_ENTER; return true;
case game::IN_CROUCH: mi = gui::MI_BACK; return true;
default: return false;
}
};

View File

@ -96,6 +96,7 @@ glm::vec2 gui::Context::MeasureText(std::string_view text)
}
else if (cp == '\n')
{
size.x = glm::max(size.x, cursor.x);
cursor.x = 0.0f;
cursor.y += line_height;
continue;
@ -122,9 +123,9 @@ glm::vec2 gui::Context::MeasureText(std::string_view text)
continue; // Dont even have "missing" glyph, font is shit
cursor.x += glyph->advance;
size.x = glm::max(size.x, cursor.x);
}
size.x = glm::max(size.x, cursor.x);
size.y = cursor.y + line_height;
return size;

View File

@ -4,17 +4,36 @@
// Menu
static constexpr float menu_title_height = 50.0f;
void gui::Menu::Clear()
{
items_.clear();
focus_ = 0;
}
void gui::Menu::Draw(Context& ctx, const glm::vec2& pos) const
{
// background
auto size = MeasureSize();
ctx.DrawRect(pos, pos + size, 0x55000000);
ctx.DrawRect(pos, pos + size, 0x88000000);
// draw title
glm::vec2 title_size(size.x, menu_title_height);
ctx.DrawRect(pos, pos + title_size, 0x55000000);
ctx.DrawTextAligned(title_, pos + title_size * 0.5f, glm::vec2(-0.5f));
// draw items
DrawMenuItemArgs args(ctx);
args.size = itemsize_;
args.pos.x = pos.x;
glm::vec2 cursor = pos;
for (size_t i = 0; i < items_.size(); ++i)
{
items_[i]->Draw(DrawMenuItemArgs(ctx, cursor, focus_ == i));
cursor.y += items_[i]->GetSize().y;
args.focused = focus_ == i;
args.pos.y = pos.y + menu_title_height + static_cast<float>(i) * itemsize_.y;
items_[i]->Draw(args);
}
}
@ -37,18 +56,14 @@ void gui::Menu::Input(MenuInput in)
}
}
void gui::Menu::SetTitle(std::string title)
{
title_ = std::move(title);
}
glm::vec2 gui::Menu::MeasureSize() const
{
glm::vec2 size(0.0f);
for (const auto& item : items_)
{
const auto& itemsize = item->GetSize();
size.x = glm::max(size.x, itemsize.x);
size.y += itemsize.y;
}
return size;
return glm::vec2(itemsize_.x, menu_title_height + itemsize_.y * static_cast<float>(items_.size()));
}
void gui::Menu::SwitchFocus(int dir)
@ -56,7 +71,17 @@ void gui::Menu::SwitchFocus(int dir)
if (items_.empty())
return;
focus_ = (focus_ + items_.size() + dir) % items_.size();
size_t old_focus = focus_;
if (items_.empty())
focus_ = 0;
else
focus_ = (focus_ + items_.size() + dir) % items_.size();
if (focus_ != old_focus)
{
OnFocusChanged();
}
}
// ButtonMenuItem
@ -64,13 +89,12 @@ void gui::Menu::SwitchFocus(int dir)
gui::ButtonMenuItem::ButtonMenuItem(std::string text)
: text_(std::move(text))
{
size_ = glm::vec2(300.0f, 30.0f);
}
void gui::ButtonMenuItem::Draw(const DrawMenuItemArgs& args) const
{
Super::Draw(args);
glm::vec2 center = args.pos + glm::vec2(0.0f, size_.y * 0.5f);
glm::vec2 center = args.pos + glm::vec2(10.0f, args.size.y * 0.5f);
args.ctx.DrawTextAligned(text_, center, glm::vec2(0.0f, -0.5f), args.focused ? COLOR_FOCUSED : COLOR_INACTIVE);
}
@ -96,12 +120,30 @@ void gui::SelectMenuItem::Draw(const DrawMenuItemArgs& args) const
{
Super::Draw(args);
char buffer[128];
size_t len = snprintf(buffer, sizeof(buffer), "< %s >", select_text_.c_str());
auto text_size = args.ctx.MeasureText(select_text_);
glm::vec2 center_right = args.pos + glm::vec2(size_.x, size_.y * 0.5f);
args.ctx.DrawTextAligned(std::string_view(buffer, len), center_right, glm::vec2(-1.0f, -0.5f),
args.focused ? COLOR_FOCUSED : COLOR_INACTIVE);
glm::vec2 cursor = args.pos + glm::vec2(args.size.x - 10.0f, args.size.y * 0.5f);
cursor.y -= text_size.y * 0.5f; // centered
uint32_t text_color = args.focused ? COLOR_FOCUSED : COLOR_INACTIVE;
uint32_t arrow_color = 0xFFFFFFFF;
float arrow_width = 0.0f;
if (args.focused)
{
arrow_width = args.ctx.MeasureText("<").x;
cursor.x -= arrow_width;
args.ctx.DrawText(">", cursor, arrow_color);
}
cursor.x -= text_size.x;
args.ctx.DrawText(select_text_, cursor, text_color);
if (args.focused)
{
cursor.x -= arrow_width;
args.ctx.DrawText("<", cursor, arrow_color);
}
}
void gui::SelectMenuItem::Input(MenuInput in)

View File

@ -24,9 +24,10 @@ struct DrawMenuItemArgs
{
Context& ctx;
glm::vec2 pos;
glm::vec2 size;
bool focused;
DrawMenuItemArgs(Context& ctx, const glm::vec2& pos, bool focused) : ctx(ctx), pos(pos), focused(focused) {}
DrawMenuItemArgs(Context& ctx) : ctx(ctx) {}
};
class Menu
@ -43,16 +44,31 @@ public:
return item_ref;
}
void Clear();
void Draw(Context& ctx, const glm::vec2& pos) const;
void Input(MenuInput in);
void SetTitle(std::string title);
void SetItemSize(const glm::vec2& itemsize) { itemsize_ = itemsize; }
size_t GetFocusedItemIndex() const { return focus_; }
size_t GetNumItems() const { return items_.size(); }
MenuItem& GetItem(size_t idx) const { return *items_[idx]; }
glm::vec2 MeasureSize() const;
protected:
virtual void OnFocusChanged() {}
private:
void SwitchFocus(int dir);
private:
std::string title_;
std::vector<std::unique_ptr<MenuItem>> items_;
glm::vec2 itemsize_ = glm::vec2(300.0f, 40.0f);
size_t focus_ = 0;
};
@ -73,7 +89,7 @@ public:
virtual ~MenuItem() = default;
protected:
glm::vec2 size_;
glm::vec2 size_ = glm::vec2(0.0f);
};
@ -89,6 +105,8 @@ public:
void SetClickCallback(std::function<void()> click_cb);
void SetText(std::string text) { text_ = std::move(text); }
virtual ~ButtonMenuItem() = default;
private:

View File

@ -23,6 +23,9 @@ enum MessageType : uint8_t
// VIEWANGLES <ViewYawQ> <ViewPitchQ>
MSG_VIEWANGLES,
// MENUACTION <MenuId> <MenuActionType> ...
MSG_MENUACTION,
/*~~~~~~~~ Session ~~~~~~~~*/
// CHAT <ChatMessage>
MSG_CHAT,
@ -37,7 +40,7 @@ enum MessageType : uint8_t
// USETARGET ...
MSG_USETARGET,
// REMOTEMENU ...
// REMOTEMENU <MenuId> <MenuMessageType> ...
MSG_REMOTEMENU,
/*~~~~~~~~ Entity ~~~~~~~~*/
@ -159,6 +162,7 @@ enum MenuMessageType
};
using MenuId = uint8_t;
using MenuTitle = FixedStr<64>;
using MenuItemId = uint8_t;
using MenuItemCount = MenuItemId;
@ -166,5 +170,16 @@ using MenuItemCount = MenuItemId;
using MenuItemText = FixedStr<64>;
using MenuItemSelection = FixedStr<64>;
// menu actions
enum MenuActionType
{
MA_CLICK,
MA_SELECT,
MA_HOVER,
};
using MenuSelectDir = uint8_t; // 0=left, 1=right
} // namespace net