ui: add main menu scene and interactive button widget

- Implement MainMenuScene with a "Start Game" button that pushes the WorldScene.
- Extend Button class with background/foreground, hover detection, and size queries.
- Add virtual width/height and event handlers to Widget base class.
- Update WorldUIManager to propagate mouse move events.
- Adjust Window mouse mode based on game running state.
This commit is contained in:
2026-07-13 10:38:30 +08:00
parent 8bbed7b7b9
commit 01c9f61d1c
20 changed files with 453 additions and 18 deletions

View File

@@ -65,4 +65,6 @@ target_sources(${PROJECT_NAME}
scene/world_scene.cpp
scene/scene_manager.cpp
ui/world_ui_manager.cpp
ui/main_menu_ui_manager.cpp
scene/main_menu_scene.cpp
)

View File

@@ -77,7 +77,7 @@ void App::init(int argc, char** argv) {
m_server.start_server(m_argument.port);
}
m_scene_manager.request_push(SceneType::WORLD);
m_scene_manager.request_push(SceneType::MAIN_MENU);
{
int w, h;

View File

@@ -0,0 +1,21 @@
#include "Cubed/scene/main_menu_scene.hpp"
namespace Cubed {
MainMenuScene::MainMenuScene(SceneManager& scene_manager)
: m_scene_manager(scene_manager), m_ui_manager(*this) {}
MainMenuScene::~MainMenuScene() {}
void MainMenuScene::update(float dt) { m_ui_manager.update(dt); }
void MainMenuScene::render(Renderer& renderer) {
m_ui_manager.render(renderer);
}
bool MainMenuScene::handle_event(const Event& e) {
if (m_ui_manager.handle_event(e)) {
return true;
}
return false;
}
void MainMenuScene::on_enter() { m_ui_manager.init(); }
void MainMenuScene::on_leave() {}
SceneManager& MainMenuScene::scene_manager() { return m_scene_manager; }
} // namespace Cubed

View File

@@ -1,5 +1,6 @@
#include "Cubed/scene/scene_manager.hpp"
#include "Cubed/scene/main_menu_scene.hpp"
#include "Cubed/scene/world_scene.hpp"
namespace Cubed {
@@ -8,11 +9,10 @@ SceneManager::~SceneManager() {}
void SceneManager::update(float dt) {
m_pending_delete_scene.clear();
process_operation();
if (!m_scenes.empty()) {
m_scenes.top()->update(dt);
}
process_operation();
}
void SceneManager::render(Renderer& renderer) {
if (m_scenes.empty()) {
@@ -87,7 +87,7 @@ std::unique_ptr<Scene> SceneManager::create_scene(SceneType type) {
case SceneType::WORLD:
return std::make_unique<WorldScene>(*this);
case SceneType::MAIN_MENU:
return nullptr;
return std::make_unique<MainMenuScene>(*this);
}
std::string err = std::format("Unknown Scene");

View File

@@ -72,11 +72,14 @@ void WorldScene::on_enter() {
m_scene_manager.app().window().set_camera(&m_camera);
m_dev_panel.init();
m_ui_manager.init();
m_scene_manager.app().window().set_game_running(true);
}
void WorldScene::on_leave() {
m_client_world.request_exit();
m_scene_manager.app().window().set_camera(nullptr);
m_scene_manager.app().window().set_game_running(false);
}
Camera& WorldScene::camera() { return m_camera; }

View File

@@ -0,0 +1,67 @@
#include "Cubed/ui/button.hpp"
#include "Cubed/tools/log.hpp"
namespace Cubed {
Button::Button() {}
void Button::update(float dt) {
if (m_background) {
m_background->update(dt);
}
if (m_foreground) {
m_foreground->update(dt);
}
}
void Button::render(Renderer& renderer) {
if (m_background) {
m_background->render(renderer);
}
if (m_foreground) {
m_foreground->render(renderer);
}
}
bool Button::handle_mouse_move_event(const MouseMoveEvent& e) {
if (e.xpos >= m_min_pos.x && e.xpos <= m_max_pos.x) {
if (e.ypos >= m_min_pos.y && e.ypos <= m_max_pos.y) {
m_hovered = true;
Logger::info("Hovered {}", m_hovered);
return true;
}
}
m_hovered = false;
Logger::info("Hovered {}", m_hovered);
return false;
}
bool Button::handle_mouse_button_event(const MouseButtonEvent& e) {
if (e.action == KeyAction::PRESS && e.key == MouseKey::LEFT_BUTTON) {
if (m_hovered && m_clicked) {
m_clicked();
Logger::info("Button click!");
return true;
}
}
return false;
}
Widget& Button::set_position(const glm::vec2& pos) {
m_pos = pos;
m_background->set_position(pos);
m_foreground->set_position(pos);
m_min_pos = pos;
m_max_pos.x = m_min_pos.x + m_background->width();
m_max_pos.y = m_min_pos.y + m_background->height();
return *this;
}
Widget& Button::set_position(float x, float y) {
return set_position(glm::vec2{x, y});
}
float Button::width() const { return m_background->width(); }
float Button::height() const { return m_background->height(); }
} // namespace Cubed

View File

@@ -4,7 +4,7 @@
#include "Cubed/tools/font.hpp"
namespace Cubed {
Label::Label(const std::string& id) : Widget(id) {}
Label::Label() {}
Label& Label::set_text(std::string_view text) {
m_text.text = text;
m_dirty = true;

View File

@@ -0,0 +1,133 @@
#include "Cubed/ui/main_menu_ui_manager.hpp"
#include "Cubed/app.hpp"
#include "Cubed/render/renderer.hpp"
#include "Cubed/scene/main_menu_scene.hpp"
#include "Cubed/scene/scene_manager.hpp"
#include "Cubed/ui/button.hpp"
namespace Cubed {
MainMenuUIManager::MainMenuUIManager(MainMenuScene& scene) : m_scene(scene) {}
MainMenuUIManager::~MainMenuUIManager() {}
void MainMenuUIManager::init() {
auto start_game_button = std::make_unique<Button>();
auto& back = start_game_button->set_background<Image>();
back.set_image("texture/ui/button001.png",
m_scene.scene_manager().app().texture_manager());
back.set_scale(3.0f);
auto& fore = start_game_button->set_foreground<Label>();
fore.set_text("Start Game");
start_game_button->set_clicked(
[this]() { m_scene.scene_manager().request_push(SceneType::WORLD); });
start_game_button->set_position(
m_scene.scene_manager().app().renderer().window_width() / 2.0f +
back.width() / 2.0f,
m_scene.scene_manager().app().renderer().window_width() / 2.0f +
back.height() / 2.0f);
m_widgets.try_emplace("start game", std::move(start_game_button));
}
void MainMenuUIManager::update(float dt) {
for (auto& w : m_widgets) {
w.second->update(dt);
}
}
void MainMenuUIManager::render(Renderer& renderer) {
renderer.begin_render_ui();
for (auto& widget : m_widgets) {
widget.second->render(renderer);
}
renderer.end_render_ui();
}
bool MainMenuUIManager::handle_event(const Event& e) {
return std::visit(
Overloaded{[this](const MouseMoveEvent& e) {
if (handle_mouse_move_event(e)) {
return true;
}
return false;
},
[this](const MouseButtonEvent& e) {
if (handle_mouse_button_event(e)) {
return true;
}
return false;
},
[this](const MouseWheelEvent& e) {
if (handle_mouse_wheel_event(e)) {
return true;
}
return false;
},
[this](const KeyEvent& e) {
if (handle_key_event(e)) {
return true;
}
return false;
},
[](const TextInputEvent&) { return false; },
[this](const WindowResizeEvent& e) {
handle_window_resize_event(e);
return false;
},
[](const FrameBufferResizeEvent&) { return false; }
},
e);
}
bool MainMenuUIManager::handle_mouse_move_event(const MouseMoveEvent& e) {
for (auto& w : m_widgets) {
if (w.second->handle_mouse_move_event(e)) {
return true;
}
}
return false;
}
bool MainMenuUIManager::handle_mouse_button_event(const MouseButtonEvent& e) {
for (auto& w : m_widgets) {
if (w.second->handle_mouse_button_event(e)) {
return true;
}
}
return false;
}
bool MainMenuUIManager::handle_window_resize_event(const WindowResizeEvent& e) {
auto it = m_widgets.find("start game");
if (it != m_widgets.end()) {
auto* start_game = dynamic_cast<Button*>(it->second.get());
if (start_game) {
start_game->set_position(e.width / 2 + start_game->width() / 2,
e.height / 2 + start_game->height() / 2);
}
}
for (auto& w : m_widgets) {
if (w.second->handle_window_resize_event(e)) {
return true;
}
}
return false;
}
bool MainMenuUIManager::handle_mouse_wheel_event(const MouseWheelEvent& e) {
for (auto& w : m_widgets) {
if (w.second->handle_mouse_wheel_event(e)) {
return true;
}
}
return false;
}
bool MainMenuUIManager::handle_key_event(const KeyEvent& e) {
for (auto& w : m_widgets) {
if (w.second->handle_key_event(e)) {
return true;
}
}
return false;
}
} // namespace Cubed

View File

@@ -18,8 +18,8 @@ void Widget::render(Renderer& renderer) {
}
}
void Widget::on_update(float dt) {}
void Widget::on_render(Renderer& renderer) {}
void Widget::on_update(float) {}
void Widget::on_render(Renderer&) {}
Widget& Widget::set_position(const glm::vec2& pos) {
return set_position(pos.x, pos.y);
@@ -35,8 +35,53 @@ Widget& Widget::set_scale(float scale) {
m_scale = scale;
return *this;
}
float Widget::width() const { return 0.0f; }
float Widget::height() const { return 0.0f; }
const glm::vec2& Widget::pos() const { return m_pos; }
float Widget::scale() const { return m_scale; }
const std::string& Widget::id() const { return m_id; }
bool Widget::handle_key_event(const KeyEvent& e) {
for (auto it = m_children.rbegin(); it != m_children.rend(); ++it) {
if ((*it)->handle_key_event(e)) {
return true;
}
}
return false;
}
bool Widget::handle_mouse_button_event(const MouseButtonEvent& e) {
for (auto it = m_children.rbegin(); it != m_children.rend(); ++it) {
if ((*it)->handle_mouse_button_event(e)) {
return true;
}
}
return false;
}
bool Widget::handle_mouse_wheel_event(const MouseWheelEvent& e) {
for (auto it = m_children.rbegin(); it != m_children.rend(); ++it) {
if ((*it)->handle_mouse_wheel_event(e)) {
return true;
}
}
return false;
}
bool Widget::handle_window_resize_event(const WindowResizeEvent& e) {
for (auto it = m_children.rbegin(); it != m_children.rend(); ++it) {
if ((*it)->handle_window_resize_event(e)) {
return true;
}
}
return false;
}
bool Widget::handle_mouse_move_event(const MouseMoveEvent& e) {
for (auto it = m_children.rbegin(); it != m_children.rend(); ++it) {
if ((*it)->handle_mouse_move_event(e)) {
return true;
}
}
return false;
}
} // namespace Cubed

View File

@@ -22,7 +22,12 @@ void WorldUIManager::init() {
.set_scale(3.0f);
m_widgets.try_emplace("crosshair", std::move(crosshair));
}
void WorldUIManager::update(float) {}
void WorldUIManager::update(float dt) {
for (auto& w : m_widgets) {
w.second->update(dt);
}
}
void WorldUIManager::render(Renderer& renderer) {
renderer.begin_render_ui();
@@ -36,7 +41,12 @@ void WorldUIManager::render(Renderer& renderer) {
}
bool WorldUIManager::handle_event(const Event& e) {
return std::visit(
Overloaded{[](const MouseMoveEvent&) { return false; },
Overloaded{[this](const MouseMoveEvent& e) {
if (handle_mouse_move_event(e)) {
return true;
}
return false;
},
[this](const MouseButtonEvent& e) {
if (handle_mouse_button_event(e)) {
return true;
@@ -65,8 +75,20 @@ bool WorldUIManager::handle_event(const Event& e) {
},
e);
}
bool WorldUIManager::handle_mouse_button_event(const MouseButtonEvent&) {
bool WorldUIManager::handle_mouse_move_event(const MouseMoveEvent& e) {
for (auto& w : m_widgets) {
if (w.second->handle_mouse_move_event(e)) {
return true;
}
}
return false;
}
bool WorldUIManager::handle_mouse_button_event(const MouseButtonEvent& e) {
for (auto& w : m_widgets) {
if (w.second->handle_mouse_button_event(e)) {
return true;
}
}
return false;
}
bool WorldUIManager::handle_window_resize_event(const WindowResizeEvent& e) {
@@ -78,11 +100,29 @@ bool WorldUIManager::handle_window_resize_event(const WindowResizeEvent& e) {
e.height / 2 + crosshair->height() / 2);
}
}
for (auto& w : m_widgets) {
if (w.second->handle_window_resize_event(e)) {
return true;
}
}
return false;
}
bool WorldUIManager::handle_mouse_wheel_event(const MouseWheelEvent&) {
bool WorldUIManager::handle_mouse_wheel_event(const MouseWheelEvent& e) {
for (auto& w : m_widgets) {
if (w.second->handle_mouse_wheel_event(e)) {
return true;
}
}
return false;
}
bool WorldUIManager::handle_key_event(const KeyEvent& e) {
for (auto& w : m_widgets) {
if (w.second->handle_key_event(e)) {
return true;
}
}
return false;
}
bool WorldUIManager::handle_key_event(const KeyEvent&) { return false; }
} // namespace Cubed

View File

@@ -129,8 +129,10 @@ void Window::init() {
} else {
glfwSwapInterval(0);
}
if (m_game_running) {
glfwSetInputMode(m_window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
}
glfwSetInputMode(m_window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
if (glfwRawMouseMotionSupported()) {
glfwSetInputMode(m_window, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
} else {
@@ -206,6 +208,9 @@ void Window::toggle_fullscreen() {
}
void Window::toggle_mouse_able() {
if (!m_game_running) {
m_mouse_enable = true;
}
// auto& io = ImGui::GetIO();
if (m_mouse_enable) {
// io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
@@ -227,6 +232,11 @@ void Window::toggle_mouse_able() {
}
void Window::set_camera(Camera* camera) { m_camera = camera; }
Camera* Window::camera() { return m_camera; }
void Window::set_game_running(bool running) {
m_game_running = running;
m_mouse_enable = running;
toggle_mouse_able();
}
void Window::imgui_init() {
float dpi_scale_x, dpi_scale_y;
glfwGetWindowContentScale(m_window, &dpi_scale_x, &dpi_scale_y);