aboutsummaryrefslogtreecommitdiff
path: root/src/client/window
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/window')
-rw-r--r--src/client/window/basic_window.cc33
-rw-r--r--src/client/window/basic_window.hh52
-rw-r--r--src/client/window/button_window.cc71
-rw-r--r--src/client/window/button_window.hh40
-rw-r--r--src/client/window/hud_window.cc88
-rw-r--r--src/client/window/hud_window.hh32
-rw-r--r--src/client/window/inventory_window.cc196
-rw-r--r--src/client/window/inventory_window.hh40
-rw-r--r--src/client/window/text_input_window.cc81
-rw-r--r--src/client/window/text_input_window.hh43
-rw-r--r--src/client/window/window.cc318
-rw-r--r--src/client/window/window.hh38
12 files changed, 1032 insertions, 0 deletions
diff --git a/src/client/window/basic_window.cc b/src/client/window/basic_window.cc
new file mode 100644
index 0000000..c96f94a
--- /dev/null
+++ b/src/client/window/basic_window.cc
@@ -0,0 +1,33 @@
+#include "client/window/basic_window.hh"
+
+namespace client {
+namespace window {
+
+using bw = basic_window;
+
+bool bw::is_inside(const glm::vec2& v, const glm::vec2& p,
+ const glm::vec2& s) noexcept {
+ if (v.x < p.x || v.x > p.x + s.x) {
+ return false;
+ }
+ if (v.y < p.y || v.y > p.y + s.y) {
+ return false;
+ }
+ return true;
+}
+
+bool bw::is_inside(const glm::vec2& v) const noexcept {
+ return is_inside(v, this->pos, this->size);
+}
+
+void bw::draw() noexcept {
+ client::render::draw_rectangle({.pos = {.offset = this->pos + 6.0f},
+ .size = {.offset = this->size},
+ .colour = {this->tertiary_clr, 0.9f}});
+ client::render::draw_rectangle({.pos = {.offset = this->pos},
+ .size = {.offset = this->size},
+ .colour = {this->primary_clr, 1.0f}});
+}
+
+} // namespace window
+} // namespace client
diff --git a/src/client/window/basic_window.hh b/src/client/window/basic_window.hh
new file mode 100644
index 0000000..292cc64
--- /dev/null
+++ b/src/client/window/basic_window.hh
@@ -0,0 +1,52 @@
+#ifndef CLIENT_WINDOW_BASIC_WINDOW_HH_
+#define CLIENT_WINDOW_BASIC_WINDOW_HH_
+
+#include <glm/glm.hpp>
+
+#include "client/entity/player.hh"
+#include "client/render/render.hh"
+#include "client/render/struct.hh"
+
+namespace client {
+namespace window {
+
+// All window objects should derive from basic_window, and make use of multiple
+// inheritance for specific behaviours.
+class basic_window {
+protected:
+ // Colours borrowed from arc-dark(er).
+ static constexpr glm::vec3 primary_clr{0.21f, 0.23f, 0.29f}; // dark
+ static constexpr glm::vec3 secondary_clr{0.29f, 0.32f, 0.38f}; // less dark
+ static constexpr glm::vec3 tertiary_clr{0.48, 0.50, 0.54}; // less dark ^2
+ static constexpr glm::vec3 highlight_clr{0.32, 0.58, 0.88}; // light blue
+ static constexpr glm::vec3 font_colour{0.88, 0.88, 0.88}; // light grey
+
+protected:
+ static constexpr float OUTLINE_WIDTH = 2.0f;
+ float get_item_size() const noexcept { return this->size.x / 10.0f; }
+
+protected:
+ glm::vec2 pos;
+ glm::vec2 size;
+
+public:
+ // Test if a vec2 v is inside a square starting at position p with size s.
+ static bool is_inside(const glm::vec2& v, const glm::vec2& p,
+ const glm::vec2& s) noexcept;
+ // Test if v is inside this->pos, this->size
+ bool is_inside(const glm::vec2& v) const noexcept;
+
+public:
+ basic_window(const client::render::relative_arg& pos,
+ const client::render::relative_arg& size) noexcept
+ : pos(pos.to_vec2()), size(size.to_vec2()) {}
+ virtual ~basic_window() noexcept {}
+
+ virtual bool maybe_handle_event(const SDL_Event&) noexcept { return false; }
+ virtual void draw() noexcept;
+};
+
+} // namespace window
+} // namespace client
+
+#endif
diff --git a/src/client/window/button_window.cc b/src/client/window/button_window.cc
new file mode 100644
index 0000000..4743c0a
--- /dev/null
+++ b/src/client/window/button_window.cc
@@ -0,0 +1,71 @@
+#include "client/window/button_window.hh"
+
+namespace client {
+namespace window {
+
+using bw = class button_window;
+
+void bw::handle_mousebuttondown(const SDL_Event& event) noexcept {
+ if (event.button.button != SDL_BUTTON_LEFT) {
+ return;
+ }
+ this->is_pressed = true;
+}
+
+void bw::handle_mousebuttonup(const SDL_Event& event) noexcept {
+ if (event.button.button != SDL_BUTTON_LEFT) {
+ return;
+ }
+ if (!this->is_pressed) {
+ return;
+ }
+ this->is_pressed = false;
+ std::invoke(this->callback); // edge
+}
+
+const glm::vec3& bw::get_draw_colour() noexcept {
+ if (!this->is_inside(client::input::state.mouse_pos)) {
+ this->is_pressed = false;
+ return this->primary_clr;
+ }
+
+ if (!this->is_pressed) {
+ return this->secondary_clr;
+ }
+
+ return this->highlight_clr;
+}
+
+bool bw::maybe_handle_event(const SDL_Event& event) noexcept {
+ if (this->is_inside(client::input::state.mouse_pos)) {
+ switch (event.type) {
+ case SDL_MOUSEBUTTONDOWN:
+ this->handle_mousebuttondown(event);
+ return true;
+ case SDL_MOUSEBUTTONUP:
+ this->handle_mousebuttonup(event);
+ return true;
+ }
+ }
+
+ return basic_window::maybe_handle_event(event);
+}
+
+void bw::draw() noexcept {
+ basic_window::draw();
+
+ client::render::draw_rectangle({.pos = {.offset = this->pos},
+ .size = {.offset = this->size},
+ .colour = {this->get_draw_colour(), 1.0f}});
+ client::render::draw_text(
+ this->name, {.pos = {.extent = {0.0f, 0.0f},
+ .offset = this->pos + (this->size / 2.0f)},
+ .offset_height = this->size.y / 2.0f,
+ .colour = {this->font_colour, 1.0f},
+ .has_backing = false,
+ .is_centered = true,
+ .is_vcentered = true});
+};
+
+} // namespace window
+} // namespace client
diff --git a/src/client/window/button_window.hh b/src/client/window/button_window.hh
new file mode 100644
index 0000000..6847696
--- /dev/null
+++ b/src/client/window/button_window.hh
@@ -0,0 +1,40 @@
+#ifndef CLIENT_WINDOW_BUTTON_WINDOW_HH_
+#define CLIENT_WINDOW_BUTTON_WINDOW_HH_
+
+#include <functional>
+#include <string>
+
+#include "client/input.hh"
+#include "client/render/render.hh"
+#include "client/window/basic_window.hh"
+
+namespace client {
+namespace window {
+
+class button_window : public basic_window {
+protected:
+ std::string name;
+ std::function<void()> callback;
+ bool is_pressed;
+
+private:
+ void handle_mousebuttondown(const SDL_Event& event) noexcept;
+ void handle_mousebuttonup(const SDL_Event& event) noexcept;
+ const glm::vec3& get_draw_colour() noexcept;
+
+public:
+ template <typename... Args>
+ button_window(const std::string_view n, const decltype(callback)& c,
+ Args&&... args) noexcept
+ : basic_window(std::forward<Args>(args)...), name(n), callback(c),
+ is_pressed(false) {}
+ virtual ~button_window() noexcept {}
+
+ virtual bool maybe_handle_event(const SDL_Event& event) noexcept override;
+ virtual void draw() noexcept override;
+};
+
+} // namespace window
+} // namespace client
+
+#endif
diff --git a/src/client/window/hud_window.cc b/src/client/window/hud_window.cc
new file mode 100644
index 0000000..52995ca
--- /dev/null
+++ b/src/client/window/hud_window.cc
@@ -0,0 +1,88 @@
+#include "client/window/hud_window.hh"
+
+namespace client {
+namespace window {
+
+using hw = class hud_window;
+bool hw::maybe_handle_keydown(const SDL_Event& event) noexcept {
+ const auto& sym = event.key.keysym.sym;
+ const auto index = static_cast<int>(sym);
+
+ const auto min = static_cast<int>(SDLK_0);
+ const auto max = static_cast<int>(SDLK_9);
+ if (index < min || index > max) {
+ return false;
+ }
+
+ const auto active =
+ static_cast<std::uint32_t>((((sym - min) - 1) + 10) % 10);
+ client::get_localplayer().get_mutable_active_item() = active;
+ return true;
+}
+
+bool hw::maybe_handle_event(const SDL_Event& event) noexcept {
+ if (is_open()) {
+ return false;
+ }
+
+ switch (event.type) {
+ case SDL_KEYDOWN:
+ return this->maybe_handle_keydown(event);
+ default:
+ break;
+ }
+ return false;
+}
+
+void hw::draw() noexcept {
+ this->basic_window::draw();
+
+ const float item_size = this->get_item_size();
+
+ const auto& localplayer = client::get_localplayer();
+ client::render::draw_rectangle(
+ {.pos = {.offset = {this->pos.x + static_cast<float>(
+ localplayer.get_active_item()) *
+ item_size,
+ this->pos.y}},
+ .size = {.offset = {item_size, item_size}},
+ .colour = {this->secondary_clr, 1.0f}});
+
+ for (int i = 1; i < shared::player::INVENTORY_COLS; ++i) {
+ const float off = item_size * static_cast<float>(i);
+ client::render::draw_rectangle(
+ {.pos = {.offset = {this->pos.x + off,
+ this->pos.y + OUTLINE_WIDTH}},
+ .size = {.offset = {OUTLINE_WIDTH,
+ item_size - 2.0f * OUTLINE_WIDTH}},
+ .colour = {this->tertiary_clr, 1.0f}});
+ }
+
+ const auto& inventory = localplayer.inventory;
+ for (int x = 0; x < shared::player::INVENTORY_COLS; ++x) {
+ const auto& item = inventory.contents[static_cast<unsigned long>(x)];
+ if (item == nullptr) {
+ continue;
+ }
+
+ const glm::vec2 off{glm::vec2{x, 0} * item_size};
+ const auto item_ptr = dynamic_cast<client::item::item*>(&*item);
+ if (item_ptr == nullptr) {
+ continue;
+ }
+ item_ptr->draw(this->pos + off, glm::vec2{item_size, item_size});
+
+ client::render::draw_text(
+ std::to_string(item->quantity),
+ {.pos = {.offset = this->pos + off +
+ glm::vec2{item_size * 0.75f, item_size * 0.2f}},
+ .offset_height = item_size * 0.40f,
+ .colour = {this->font_colour, 1.0f},
+ .has_backing = false,
+ .is_centered = true,
+ .is_vcentered = true});
+ }
+}
+
+} // namespace window
+} // namespace client
diff --git a/src/client/window/hud_window.hh b/src/client/window/hud_window.hh
new file mode 100644
index 0000000..7c03e0e
--- /dev/null
+++ b/src/client/window/hud_window.hh
@@ -0,0 +1,32 @@
+#ifndef CLIENT_WINDOW_HUD_WINDOW_HH_
+#define CLIENT_WINDOW_HUD_WINDOW_HH_
+
+#include "client/render/struct.hh"
+#include "client/window/basic_window.hh"
+#include "client/window/window.hh"
+#include "shared/entity/player.hh"
+
+namespace client {
+namespace window {
+
+class hud_window : public basic_window {
+
+private:
+ bool maybe_handle_keydown(const SDL_Event& event) noexcept;
+
+public:
+ template <typename... Args>
+ hud_window(const float size) noexcept
+ : basic_window(
+ client::render::relative_arg{.extent = {0.5f, 0.0f},
+ .offset = {-size / 2.0f, 0.0f}},
+ client::render::relative_arg{.offset = {size, size / 10.0f}}) {}
+
+ virtual void draw() noexcept override;
+ virtual bool maybe_handle_event(const SDL_Event& event) noexcept override;
+};
+
+} // namespace window
+} // namespace client
+
+#endif
diff --git a/src/client/window/inventory_window.cc b/src/client/window/inventory_window.cc
new file mode 100644
index 0000000..5023f1d
--- /dev/null
+++ b/src/client/window/inventory_window.cc
@@ -0,0 +1,196 @@
+#include "client/window/inventory_window.hh"
+
+namespace client {
+namespace window {
+
+using iw = class inventory_window;
+
+bool iw::maybe_handle_mousebuttondown(const SDL_Event& event) noexcept {
+
+ if (event.button.button != SDL_BUTTON_LEFT) {
+ return false;
+ }
+ if (!this->is_inside(client::input::state.mouse_pos)) {
+ return false;
+ }
+
+ const auto index =
+ this->maybe_get_inventory_index(client::input::state.mouse_pos);
+ if (!index.has_value()) {
+ return false;
+ }
+
+ const auto& inventory = client::get_localplayer().inventory;
+ if (inventory.contents[*index] != nullptr) {
+ this->grabbed.emplace(*index);
+ }
+ return true;
+}
+
+static proto::packet make_swap_packet(const std::uint32_t& a,
+ const std::uint32_t& b) noexcept {
+ proto::packet ret;
+ const auto packet = ret.mutable_item_swap_packet();
+ packet->set_index_a(a);
+ packet->set_index_b(b);
+ return ret;
+}
+
+bool iw::maybe_handle_mousebuttonup(const SDL_Event& event) noexcept {
+
+ if (event.button.button != SDL_BUTTON_LEFT) {
+ return false;
+ }
+ if (!this->grabbed.has_value()) {
+ return false;
+ }
+
+ if (const auto index =
+ this->maybe_get_inventory_index(input::state.mouse_pos);
+ index.has_value() && index != *grabbed) {
+
+ auto& inventory = client::get_localplayer().inventory;
+ std::swap(inventory.contents[*grabbed], inventory.contents[*index]);
+
+ // replicate on server
+ state::connection->rsend_packet(
+ make_swap_packet(static_cast<std::uint32_t>(*grabbed),
+ static_cast<std::uint32_t>(*index)));
+ }
+
+ this->grabbed.reset();
+ return true;
+}
+
+std::optional<unsigned long>
+iw::maybe_get_inventory_index(const glm::vec2& pos) const noexcept {
+
+ const float item_size = this->get_item_size();
+ for (int x = 0; x < shared::player::INVENTORY_COLS; ++x) {
+ for (int y = 0; y < shared::player::INVENTORY_ROWS; ++y) {
+ const glm::vec2 off{this->pos +
+ glm::vec2{OUTLINE_WIDTH, OUTLINE_WIDTH} +
+ glm::vec2{x, y} * item_size};
+ if (!basic_window::is_inside(pos, off,
+ glm::vec2{item_size, item_size})) {
+ continue;
+ }
+ return x + y * shared::player::INVENTORY_COLS;
+ }
+ }
+ return std::nullopt;
+}
+
+bool iw::maybe_handle_event(const SDL_Event& event) noexcept {
+ switch (event.type) {
+ case SDL_MOUSEBUTTONDOWN:
+ return this->maybe_handle_mousebuttondown(event);
+ case SDL_MOUSEBUTTONUP:
+ return this->maybe_handle_mousebuttonup(event);
+ }
+
+ return basic_window::maybe_handle_event(event);
+}
+
+void iw::draw() noexcept {
+ basic_window::draw();
+
+ const float item_size = this->get_item_size();
+
+ const auto& localplayer = client::get_localplayer();
+ client::render::draw_rectangle(
+ {.pos = {.offset = {this->pos.x + static_cast<float>(
+ localplayer.get_active_item()) *
+ item_size,
+ this->pos.y}},
+ .size = {.offset = {item_size, item_size}},
+ .colour = {this->secondary_clr, 1.0f}});
+ for (int i = 1; i < shared::player::INVENTORY_COLS; ++i) {
+ const float off = item_size * static_cast<float>(i);
+ client::render::draw_rectangle(
+ {.pos = {.offset = {this->pos.x + off,
+ this->pos.y + OUTLINE_WIDTH}},
+ .size = {.offset = {OUTLINE_WIDTH,
+ item_size * shared::player::INVENTORY_ROWS -
+ OUTLINE_WIDTH}},
+ .colour = {this->tertiary_clr, 1.0f}});
+
+ if (i <= shared::player::INVENTORY_ROWS) {
+ client::render::draw_rectangle(
+ {.pos = {.offset = {pos.x + OUTLINE_WIDTH, pos.y + off}},
+ .size = {.offset = {this->size.x - 2.0f * OUTLINE_WIDTH,
+ OUTLINE_WIDTH}},
+ .colour = {this->tertiary_clr, 1.0f}});
+ }
+ }
+
+ const auto& inventory = localplayer.inventory;
+ if (const auto index =
+ this->maybe_get_inventory_index(client::input::state.mouse_pos);
+ index.has_value()) {
+ const glm::vec2 off{glm::vec2{*index % shared::player::INVENTORY_COLS,
+ *index / shared::player::INVENTORY_COLS} *
+ item_size};
+ client::render::draw_rectangle(
+ {.pos = {.offset = this->pos + off +
+ glm::vec2{OUTLINE_WIDTH, OUTLINE_WIDTH}},
+ .size = {.offset = glm::vec2{item_size, item_size} -
+ glm::vec2{OUTLINE_WIDTH, OUTLINE_WIDTH}},
+ .colour = {this->secondary_clr, 1.0f}});
+ }
+
+ for (int x = 0; x < shared::player::INVENTORY_COLS; ++x) {
+ for (int y = 0; y < shared::player::INVENTORY_ROWS; ++y) {
+ const auto index = static_cast<unsigned long>(
+ x + y * shared::player::INVENTORY_COLS);
+ const auto& item = inventory.contents[index];
+ if (item == nullptr) {
+ continue;
+ }
+ if (this->grabbed.has_value() && *this->grabbed == index) {
+ continue;
+ }
+
+ const glm::vec2 off{glm::vec2{x, y} * item_size};
+ const auto item_ptr = dynamic_cast<client::item::item*>(&*item);
+ if (item_ptr == nullptr) {
+ continue;
+ }
+ item_ptr->draw(this->pos + off, glm::vec2{item_size, item_size});
+
+ client::render::draw_text(
+ std::to_string(item->quantity),
+ {.pos = {.offset =
+ this->pos + off +
+ glm::vec2{item_size * 0.75f, item_size * 0.2f}},
+ .offset_height = item_size * 0.40f,
+ .colour = {this->font_colour, 1.0f},
+ .has_backing = false,
+ .is_centered = true,
+ .is_vcentered = true});
+ }
+ }
+
+ if (this->grabbed.has_value()) {
+ const auto item_ptr =
+ dynamic_cast<client::item::item*>(&*inventory.contents[*grabbed]);
+ if (item_ptr != nullptr) {
+ item_ptr->draw(client::input::state.mouse_pos -
+ glm::vec2{item_size, item_size} * 0.5f,
+ glm::vec2{item_size, item_size});
+ client::render::draw_text(
+ std::to_string(item_ptr->quantity),
+ {.pos = {.offset =
+ client::input::state.mouse_pos +
+ glm::vec2{item_size * 0.25f, item_size * -0.3f}},
+ .offset_height = item_size * 0.40f,
+ .colour = {this->font_colour, 1.0f},
+ .has_backing = false,
+ .is_centered = true,
+ .is_vcentered = true});
+ }
+ }
+}
+
+} // namespace window
+} // namespace client
diff --git a/src/client/window/inventory_window.hh b/src/client/window/inventory_window.hh
new file mode 100644
index 0000000..a77ae2f
--- /dev/null
+++ b/src/client/window/inventory_window.hh
@@ -0,0 +1,40 @@
+#ifndef CLIENT_WINDOW_INVENTORY_WINDOW_HH_
+#define CLIENT_WINDOW_INVENTORY_WINDOW_HH_
+
+#include <cstdint>
+#include <optional>
+
+#include "client/item/item.hh"
+#include "client/render/draw.hh"
+#include "client/state/state.hh"
+#include "client/window/basic_window.hh"
+
+namespace client {
+namespace window {
+
+class inventory_window : public basic_window {
+private:
+ std::optional<unsigned long> grabbed;
+
+private:
+ std::optional<unsigned long>
+ maybe_get_inventory_index(const glm::vec2& pos) const noexcept;
+
+private:
+ bool maybe_handle_mousebuttondown(const SDL_Event& event) noexcept;
+ bool maybe_handle_mousebuttonup(const SDL_Event& event) noexcept;
+
+public:
+ template <typename... Args>
+ inventory_window(Args&&... args) noexcept
+ : basic_window(std::forward<Args>(args)...) {}
+ virtual ~inventory_window() noexcept {}
+
+ virtual bool maybe_handle_event(const SDL_Event& event) noexcept override;
+ virtual void draw() noexcept override;
+};
+
+} // namespace window
+} // namespace client
+
+#endif
diff --git a/src/client/window/text_input_window.cc b/src/client/window/text_input_window.cc
new file mode 100644
index 0000000..0d556a4
--- /dev/null
+++ b/src/client/window/text_input_window.cc
@@ -0,0 +1,81 @@
+#include "client/window/text_input_window.hh"
+
+namespace client {
+namespace window {
+
+using tiw = class text_input_window;
+
+const std::string& tiw::get_send_text() noexcept {
+ auto& text = client::input::state.text_input;
+ text = std::string{std::begin(text),
+ std::begin(text) +
+ static_cast<long>(std::min(std::size(text),
+ shared::MAX_SAY_LENGTH))};
+ return text;
+}
+
+proto::packet tiw::make_say_packet() noexcept {
+ proto::packet packet;
+
+ const auto sub_say_packet = packet.mutable_say_packet();
+ sub_say_packet->set_text(get_send_text());
+
+ return packet;
+}
+
+bool tiw::maybe_handle_keydown(const SDL_Event& event) noexcept {
+ if (event.key.keysym.sym == SDLK_BACKSPACE) {
+ if (!client::input::state.text_input.empty()) {
+ client::input::state.text_input.pop_back();
+ }
+ return true;
+ }
+
+ if (event.key.keysym.sym != SDLK_RETURN || event.key.repeat) {
+ return false;
+ }
+
+ if (!client::input::state.text_input.empty()) {
+ client::state::connection->rsend_packet(make_say_packet());
+ }
+
+ pop_window();
+ return true;
+}
+
+const glm::vec3& tiw::get_draw_colour() noexcept {
+ if (client::input::state.text_input.length() >= shared::MAX_SAY_LENGTH) {
+ return basic_window::highlight_clr;
+ }
+ return basic_window::primary_clr;
+}
+
+bool tiw::maybe_handle_event(const SDL_Event& event) noexcept {
+ switch (event.type) {
+ case SDL_KEYDOWN:
+ return this->maybe_handle_keydown(event);
+ }
+
+ return basic_window::maybe_handle_event(event);
+}
+
+void tiw::draw() noexcept {
+ basic_window::draw();
+
+ client::render::draw_rectangle({.pos = {.offset = this->pos},
+ .size = {.offset = this->size},
+ .colour = {this->get_draw_colour(), 1.0f}});
+
+ client::render::draw_text(
+ this->get_send_text(),
+ {.pos = {.extent = {0.0f, 0.0f},
+ .offset = this->pos + (this->size / 2.0f)},
+ .offset_height = this->size.y / 2.0f,
+ .colour = {this->font_colour, 1.0f},
+ .has_backing = false,
+ .is_centered = true,
+ .is_vcentered = true});
+};
+
+} // namespace window
+} // namespace client
diff --git a/src/client/window/text_input_window.hh b/src/client/window/text_input_window.hh
new file mode 100644
index 0000000..edff672
--- /dev/null
+++ b/src/client/window/text_input_window.hh
@@ -0,0 +1,43 @@
+#ifndef CLIENT_WINDOW_TEXT_INPUT_WINDOW_HH_
+#define CLIENT_WINDOW_TEXT_INPUT_WINDOW_HH_
+
+#include <string>
+
+#include "client/input.hh"
+#include "client/shared.hh"
+#include "client/state/state.hh"
+#include "client/window/basic_window.hh"
+#include "shared/net/proto.hh"
+
+namespace client {
+namespace window {
+
+void pop_window() noexcept; // forward declaration
+
+class text_input_window : public basic_window {
+private:
+ // text restricted to a size of 32.
+ static const std::string& get_send_text() noexcept;
+ static proto::packet make_say_packet() noexcept;
+
+ static bool maybe_handle_keydown(const SDL_Event& event) noexcept;
+ static const glm::vec3& get_draw_colour() noexcept;
+
+public:
+ template <typename... Args>
+ text_input_window(Args&&... args) noexcept
+ : basic_window(std::forward<Args>(args)...) {
+ client::input::set_text_input(true);
+ }
+ virtual ~text_input_window() noexcept {
+ client::input::set_text_input(false);
+ }
+ virtual bool maybe_handle_event(const SDL_Event& event) noexcept override;
+
+ virtual void draw() noexcept override;
+};
+
+} // namespace window
+} // namespace client
+
+#endif
diff --git a/src/client/window/window.cc b/src/client/window/window.cc
new file mode 100644
index 0000000..648997f
--- /dev/null
+++ b/src/client/window/window.cc
@@ -0,0 +1,318 @@
+#include "window.hh"
+
+namespace client {
+namespace window {
+
+// TODO
+
+// Sliders are for numerical values of some type T.
+// TODO
+/*
+template <typename T>
+class slider_window : public basic_window {
+protected:
+ std::string name;
+ T min;
+ T cur;
+ T max;
+ T& var;
+
+private:
+ void handle_mousebuttondown(const SDL_Event& event) noexcept {}
+ void handle_mousebuttonup(const SDL_Event& event) noexcept {}
+
+public:
+ template <typename... Args>
+ slider_window(const std::string_view name, const T& min, const T& cur,
+ const T& max, T& var, Args&&... args) noexcept
+ : basic_window(std::forward<Args>(args)...), name(name), min(min),
+ cur(cur), max(max), var(var) {}
+
+ // slider_window(
+ virtual bool maybe_handle_event(const SDL_Event& event) noexcept
+override { switch (event.type) { case SDL_MOUSEBUTTONDOWN:
+ this->handle_mousebuttondown(event);
+ return true;
+ case SDL_MOUSEBUTTONUP:
+ this->handle_mousebuttonup(event);
+ return true;
+ }
+ return basic_window::maybe_handle_event(event);
+ }
+ virtual void draw() noexcept override { basic_window::draw(); }
+};
+*/
+
+static void handle_event(const SDL_Event& event) noexcept; // ignore
+
+static hud_window& get_hud_window() noexcept {
+ static hud_window ret = []() {
+ const auto& window = client::render::get_window_size();
+ const float size = std::min(static_cast<float>(window.x) / 2.0f,
+ static_cast<float>(window.y) / 2.0f);
+ client::window::hud_window ret{size};
+ return ret;
+ }();
+ return ret;
+}
+
+// All dynamic windows go in this list!
+using layer = std::forward_list<std::unique_ptr<basic_window>>;
+using layers = std::forward_list<layer>;
+static layers& get_layers() noexcept {
+ // We callbacks for our window manager are initialised here too.
+ static layers ret = []() -> layers {
+ client::input::register_event_handler(&handle_event);
+ client::input::set_text_input(false);
+ client::input::set_mouse_relative(true);
+ return {};
+ }();
+ return ret;
+}
+
+void pop_window() noexcept {
+ if (!get_layers().empty()) {
+ get_layers().pop_front();
+ }
+ // Our windows might be empty here, so set our mouse mode accordingly.
+ if (!client::window::is_open()) {
+ client::input::set_mouse_relative(true);
+ }
+}
+
+// Constants used for uniform ui sizes.
+constexpr glm::vec2 lsize_extent{0.4, 0.075};
+constexpr glm::vec2 ssize_extent{0.15, 0.075};
+
+static void center_mouse_position() noexcept {
+ const glm::vec2& window = client::render::get_window_size();
+ client::input::set_mouse_position({window.x / 2.0f, window.y / 2.0f});
+}
+
+template <typename T, typename... Args>
+void push_window(Args&&... args) noexcept {
+ get_layers().front().push_front(
+ std::make_unique<T>(std::forward<Args>(args)...));
+}
+
+constexpr glm::vec2 center_extent(const glm::vec2 pos,
+ const glm::vec2 size) noexcept {
+ return {pos.x, pos.y - size.y / 2.0f};
+}
+
+static void make_options_menu() noexcept {
+ get_layers().push_front({});
+
+ /*
+ push_window<::slider_window<float>>(
+ "Field of Vision", 0.0f,
+ settings::get(std::make_pair("gameplay", "fov"), 100.0f), 145.0f,
+ pop_window,
+ client::draw::relative_arg{.extent =
+ center_extent({0.3, 0.7},
+ lsize_extent)}, client::draw::relative_arg{.extent = lsize_extent});
+ */
+
+ push_window<button_window>(
+ "Back", pop_window,
+ client::render::relative_arg{
+ .extent = center_extent({0.3, 0.3}, ssize_extent)},
+ client::render::relative_arg{.extent = ssize_extent});
+}
+
+static void make_main_menu() noexcept {
+ get_layers().push_front({});
+
+ push_window<button_window>(
+ "Return to Game", pop_window,
+ client::render::relative_arg{
+ .extent = center_extent({0.3, 0.7}, lsize_extent)},
+ client::render::relative_arg{.extent = lsize_extent});
+
+ push_window<button_window>(
+ "Options", make_options_menu,
+ client::render::relative_arg{
+ .extent = center_extent({0.55, 0.6}, ssize_extent)},
+ client::render::relative_arg{.extent = ssize_extent});
+
+ push_window<button_window>(
+ "?", pop_window,
+ client::render::relative_arg{
+ .extent = center_extent({0.55, 0.5}, ssize_extent)},
+ client::render::relative_arg{.extent = ssize_extent});
+
+ push_window<button_window>(
+ "?", pop_window,
+ client::render::relative_arg{
+ .extent = center_extent({0.55, 0.4}, ssize_extent)},
+ client::render::relative_arg{.extent = ssize_extent});
+
+ push_window<button_window>(
+ "Exit Game", [] { shared::should_exit = true; },
+ client::render::relative_arg{
+ .extent = center_extent({0.3, 0.3}, lsize_extent)},
+ client::render::relative_arg{.extent = lsize_extent});
+
+ client::input::set_mouse_relative(false);
+ center_mouse_position();
+}
+
+static void make_chat_window() noexcept {
+ get_layers().push_front({});
+
+ push_window<text_input_window>(
+ client::render::relative_arg{
+ .extent = center_extent({0.3, 0.3}, lsize_extent)},
+ client::render::relative_arg{.extent = lsize_extent});
+
+ client::input::set_mouse_relative(false);
+ center_mouse_position();
+}
+
+static void make_inventory_window() noexcept {
+ get_layers().push_front({});
+
+ const glm::vec2& window = client::render::get_window_size();
+
+ const float size = std::min(window.x / 2.0f, window.y / 2.0f);
+ const glm::vec2 pos{(window - size) / 2.0f};
+
+ push_window<inventory_window>(
+ client::render::relative_arg{.offset = pos},
+ client::render::relative_arg{.offset = glm::vec2{size, size}});
+ client::input::set_mouse_relative(false);
+ center_mouse_position();
+}
+
+static void handle_meta_return() noexcept {
+ if (!is_open()) {
+ make_chat_window();
+ return;
+ }
+}
+
+static void handle_meta_escape() noexcept {
+ if (!is_open()) {
+ make_main_menu();
+ return;
+ }
+
+ pop_window();
+}
+
+static void handle_meta_e() noexcept {
+ if (!is_open()) {
+ make_inventory_window();
+ return;
+ }
+
+ // The inventory window must be open.
+ if (!dynamic_cast<inventory_window*>(&*get_layers().front().front())) {
+ return;
+ }
+
+ pop_window();
+}
+
+static void handle_meta_keydown(const SDL_Event& event) noexcept {
+ if (event.key.repeat) { // only handle keypresses
+ return;
+ }
+
+ switch (event.key.keysym.sym) {
+ case SDLK_ESCAPE:
+ handle_meta_escape();
+ break;
+ case SDLK_RETURN:
+ handle_meta_return();
+ break;
+ case SDLK_e:
+ handle_meta_e();
+ break;
+ case SDLK_0:
+ case SDLK_1:
+ case SDLK_2:
+ case SDLK_3:
+ case SDLK_4:
+ case SDLK_5:
+ case SDLK_6:
+ case SDLK_7:
+ case SDLK_8:
+ case SDLK_9:
+ get_hud_window().maybe_handle_event(event);
+ default:
+ break;
+ }
+}
+
+static void handle_meta_mousemotion(const SDL_Event& event) noexcept {
+ // We convert SDL's weird coordinates into useful ones (0,0 = bottom
+ // left).
+ client::input::state.mouse_pos = {
+ event.motion.x,
+ static_cast<int>(client::render::get_window_size().y) - event.motion.y};
+}
+
+static void handle_meta_windowevent(const SDL_Event& event) noexcept {
+ if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
+ if (!is_open()) {
+ make_main_menu();
+ return;
+ }
+ }
+}
+
+static void handle_meta_event(const SDL_Event& event) noexcept {
+ switch (event.type) {
+ case SDL_KEYDOWN:
+ handle_meta_keydown(event);
+ break;
+ case SDL_MOUSEMOTION:
+ handle_meta_mousemotion(event);
+ break;
+ case SDL_WINDOWEVENT:
+ handle_meta_windowevent(event);
+ break;
+ }
+}
+
+static void handle_event(const SDL_Event& event) noexcept {
+ // We ALWAYS update our mouse position.
+ if (event.type == SDL_MOUSEMOTION) {
+ handle_meta_mousemotion(event);
+ }
+
+ // Either a window consumes our event, or no window does - so we send
+ // the event to our "meta handler" which does things like closing
+ // windows etc.
+ if (is_open()) {
+ for (const auto& window : get_layers().front()) {
+ if (window->maybe_handle_event(event)) {
+ return;
+ }
+ }
+ }
+
+ handle_meta_event(event);
+}
+
+void draw() noexcept {
+ if (!is_open()) {
+
+ if (!client::input::is_key_toggled(SDLK_F1)) {
+ get_hud_window().draw();
+ }
+
+ return;
+ }
+
+ client::render::draw_colour({0.0f, 0.0f, 0.0f, 0.10f}); // very light shade
+ for (const auto& window : get_layers().front()) {
+ window->draw();
+ }
+}
+
+bool is_open() noexcept { return !get_layers().empty(); }
+
+} // namespace window
+} // namespace client
diff --git a/src/client/window/window.hh b/src/client/window/window.hh
new file mode 100644
index 0000000..d112d18
--- /dev/null
+++ b/src/client/window/window.hh
@@ -0,0 +1,38 @@
+#ifndef CLIENT_WINDOW_WINDOW_HH_
+#define CLIENT_WINDOW_WINDOW_HH_
+
+#include <algorithm>
+#include <forward_list>
+#include <functional>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+
+#include <SDL2/SDL.h>
+#include <glm/glm.hpp>
+
+#include "client/client.hh"
+#include "client/input.hh"
+#include "client/render/render.hh"
+#include "client/settings.hh"
+#include "client/window/basic_window.hh"
+#include "client/window/button_window.hh"
+#include "client/window/hud_window.hh"
+#include "client/window/inventory_window.hh"
+#include "client/window/text_input_window.hh"
+#include "shared/shared.hh"
+
+namespace client {
+namespace window {
+
+// removes the topmost window
+void pop_window() noexcept;
+bool is_open() noexcept;
+
+void draw() noexcept;
+
+} // namespace window
+} // namespace client
+
+#endif