mirror of
https://github.com/zhenyan121/Cubed.git
synced 2026-08-09 02:07:04 +08:00
feat(ui): add ScrollView widget and dynamic layout on resize
Introduces a ScrollView widget with mouse wheel support and viewport clipping. Credits and screenshot screens now use it and recompute layout on window resize. Offset storage switched from ivec2 to vec2 with new accessors.
This commit is contained in:
@@ -9,9 +9,11 @@ class CreditsUI : public UIManager {
|
|||||||
public:
|
public:
|
||||||
CreditsUI(CreditsScene& m_scene);
|
CreditsUI(CreditsScene& m_scene);
|
||||||
void init() override;
|
void init() override;
|
||||||
|
void update_layout(int width, int height);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool handle_key_event(const KeyEvent& e) override;
|
bool handle_key_event(const KeyEvent& e) override;
|
||||||
|
bool handle_window_resize_event(const WindowResizeEvent& e) override;
|
||||||
CreditsScene& m_scene;
|
CreditsScene& m_scene;
|
||||||
};
|
};
|
||||||
} // namespace Cubed
|
} // namespace Cubed
|
||||||
35
include/Cubed/ui/scroll_view.hpp
Normal file
35
include/Cubed/ui/scroll_view.hpp
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Cubed/ui/widget.hpp"
|
||||||
|
namespace Cubed {
|
||||||
|
class ColumnLayout;
|
||||||
|
class ScrollView : public Widget {
|
||||||
|
public:
|
||||||
|
ScrollView(const ScrollView&) = delete;
|
||||||
|
ScrollView(ScrollView&&) = delete;
|
||||||
|
ScrollView& operator=(const ScrollView&) = delete;
|
||||||
|
ScrollView& operator=(ScrollView&&) = delete;
|
||||||
|
|
||||||
|
ScrollView(Widget* parent);
|
||||||
|
~ScrollView();
|
||||||
|
|
||||||
|
void render(Renderer& renderer) override;
|
||||||
|
|
||||||
|
float width() const override;
|
||||||
|
// visable height, you need notice offset
|
||||||
|
float height() const override;
|
||||||
|
|
||||||
|
ScrollView& set_child(std::unique_ptr<ColumnLayout> children);
|
||||||
|
|
||||||
|
bool handle_mouse_wheel_event(const MouseWheelEvent& e) override;
|
||||||
|
bool handle_mouse_move_event(const MouseMoveEvent& e) override;
|
||||||
|
Widget& set_fill_parent(bool fill) override;
|
||||||
|
Widget& set_fill_width(bool fill) override;
|
||||||
|
Widget& set_fill_height(bool fill) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ColumnLayout* m_child = nullptr;
|
||||||
|
bool m_hovered = false;
|
||||||
|
float m_scroll_speed = 50.0f;
|
||||||
|
};
|
||||||
|
} // namespace Cubed
|
||||||
@@ -32,7 +32,9 @@ public:
|
|||||||
virtual void update(float dt);
|
virtual void update(float dt);
|
||||||
virtual void render(Renderer& renderer);
|
virtual void render(Renderer& renderer);
|
||||||
virtual Widget& set_anchor(Anchor anchor);
|
virtual Widget& set_anchor(Anchor anchor);
|
||||||
virtual Widget& set_offset(glm::ivec2 offset);
|
virtual Widget& set_offset(glm::vec2 offset);
|
||||||
|
virtual Widget& add_offset(glm::vec2 offset);
|
||||||
|
virtual glm::vec2 get_offset() const;
|
||||||
static void set_window_size(int width, int height);
|
static void set_window_size(int width, int height);
|
||||||
virtual Widget& set_visible(bool visible);
|
virtual Widget& set_visible(bool visible);
|
||||||
// Returns the final display size
|
// Returns the final display size
|
||||||
@@ -78,7 +80,7 @@ protected:
|
|||||||
bool m_fill_width = false;
|
bool m_fill_width = false;
|
||||||
bool m_show_border = false;
|
bool m_show_border = false;
|
||||||
int m_border_size = 5;
|
int m_border_size = 5;
|
||||||
glm::ivec2 m_offset{0, 0};
|
glm::vec2 m_offset{0, 0};
|
||||||
|
|
||||||
std::vector<std::unique_ptr<Widget>>& children();
|
std::vector<std::unique_ptr<Widget>>& children();
|
||||||
|
|
||||||
|
|||||||
@@ -108,4 +108,5 @@ target_sources(${PROJECT_NAME}
|
|||||||
gameplay/block_manager.cpp
|
gameplay/block_manager.cpp
|
||||||
ui/screenshot_ui.cpp
|
ui/screenshot_ui.cpp
|
||||||
scene/screenshot_scene.cpp
|
scene/screenshot_scene.cpp
|
||||||
|
ui/scroll_view.cpp
|
||||||
)
|
)
|
||||||
@@ -8,12 +8,19 @@
|
|||||||
#include "Cubed/ui/column_layout.hpp"
|
#include "Cubed/ui/column_layout.hpp"
|
||||||
#include "Cubed/ui/image.hpp"
|
#include "Cubed/ui/image.hpp"
|
||||||
#include "Cubed/ui/rect.hpp"
|
#include "Cubed/ui/rect.hpp"
|
||||||
|
#include "Cubed/ui/scroll_view.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
namespace Cubed {
|
namespace Cubed {
|
||||||
CreditsUI::CreditsUI(CreditsScene& scene) : m_scene(scene) {}
|
CreditsUI::CreditsUI(CreditsScene& scene) : m_scene(scene) {}
|
||||||
|
|
||||||
void CreditsUI::init() {
|
void CreditsUI::init() {
|
||||||
|
auto& renderer = m_scene.scene_manager().app().renderer();
|
||||||
|
update_layout(renderer.window_width(), renderer.window_height());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CreditsUI::update_layout(int, int height) {
|
||||||
|
m_root_widget.reset();
|
||||||
auto image = std::make_unique<Image>(nullptr);
|
auto image = std::make_unique<Image>(nullptr);
|
||||||
image
|
image
|
||||||
->set_image("texture/ui/background.png",
|
->set_image("texture/ui/background.png",
|
||||||
@@ -23,25 +30,38 @@ void CreditsUI::init() {
|
|||||||
|
|
||||||
auto& rect = image->add_child<Rect>();
|
auto& rect = image->add_child<Rect>();
|
||||||
rect.set_color(Color::BLACK).set_alpha(0.7f).set_fill_parent(true);
|
rect.set_color(Color::BLACK).set_alpha(0.7f).set_fill_parent(true);
|
||||||
|
auto& title = rect.add_child<Label>();
|
||||||
|
title.set_color(Color::WHITE)
|
||||||
|
.set_text(tr("menu.main.credits"))
|
||||||
|
.set_anchor(Anchor::TOP_CENTER)
|
||||||
|
.set_offset({0, 10});
|
||||||
|
|
||||||
{
|
auto& return_button = rect.add_child<Button>();
|
||||||
auto& button = rect.add_child<Button>();
|
return_button.set_background_image(
|
||||||
button.set_background_image(
|
|
||||||
"texture/ui/button001.png",
|
"texture/ui/button001.png",
|
||||||
m_scene.scene_manager().app().texture_manager());
|
m_scene.scene_manager().app().texture_manager());
|
||||||
|
|
||||||
button.set_text(tr("button.return"));
|
return_button.set_text(tr("button.return"));
|
||||||
button.set_anchor(Anchor::BOTTOM_CENTER).set_offset({0, -20});
|
return_button.set_anchor(Anchor::BOTTOM_CENTER).set_offset({0, -20});
|
||||||
button.set_clicked([this]() { m_scene.scene_manager().request_pop(); });
|
return_button.set_clicked(
|
||||||
}
|
[this]() { m_scene.scene_manager().request_pop(); });
|
||||||
|
|
||||||
{
|
{
|
||||||
auto& layout = rect.add_child<ColumnLayout>();
|
auto& scroll = rect.add_child<ScrollView>();
|
||||||
layout.set_spacing(20)
|
scroll.set_anchor(Anchor::TOP_CENTER)
|
||||||
.set_anchor(Anchor::TOP_CENTER)
|
.set_offset({0, 20 + title.height()});
|
||||||
.set_offset({0, 20});
|
|
||||||
layout.add_child<Label>().set_text("Cubed");
|
float scroll_height = height - scroll.get_offset().y -
|
||||||
auto add_text = [&](std::string_view view, float scale = 0.5f) {
|
return_button.height() +
|
||||||
layout.add_child<Label>().set_text(view).set_scale(scale);
|
return_button.get_offset().y;
|
||||||
|
|
||||||
|
scroll.set_height(std::max(0.0f, scroll_height));
|
||||||
|
|
||||||
|
auto layout = std::make_unique<ColumnLayout>(&scroll);
|
||||||
|
layout->set_spacing(20).set_anchor(Anchor::TOP_CENTER);
|
||||||
|
layout->add_child<Label>().set_text("Cubed");
|
||||||
|
auto add_text = [&](std::string_view view, float scale = 0.8f) {
|
||||||
|
layout->add_child<Label>().set_text(view).set_scale(scale);
|
||||||
};
|
};
|
||||||
add_text("A cube game like Minecraft, using C++ and OpenGL.");
|
add_text("A cube game like Minecraft, using C++ and OpenGL.");
|
||||||
add_text("Author: zhenyan121");
|
add_text("Author: zhenyan121");
|
||||||
@@ -73,6 +93,8 @@ void CreditsUI::init() {
|
|||||||
add_text("SkyOnPole");
|
add_text("SkyOnPole");
|
||||||
add_text("free_w_cloud");
|
add_text("free_w_cloud");
|
||||||
add_text("Last but not least, thanks to you");
|
add_text("Last but not least, thanks to you");
|
||||||
|
|
||||||
|
scroll.set_child(std::move(layout));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_root_widget = std::move(image);
|
m_root_widget = std::move(image);
|
||||||
@@ -86,4 +108,9 @@ bool CreditsUI::handle_key_event(const KeyEvent& e) {
|
|||||||
return UIManager::handle_key_event(e);
|
return UIManager::handle_key_event(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CreditsUI::handle_window_resize_event(const WindowResizeEvent& e) {
|
||||||
|
update_layout(e.width, e.height);
|
||||||
|
return UIManager::handle_window_resize_event(e);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Cubed
|
} // namespace Cubed
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "Cubed/ui/button.hpp"
|
#include "Cubed/ui/button.hpp"
|
||||||
#include "Cubed/ui/column_layout.hpp"
|
#include "Cubed/ui/column_layout.hpp"
|
||||||
#include "Cubed/ui/row_layout.hpp"
|
#include "Cubed/ui/row_layout.hpp"
|
||||||
|
#include "Cubed/ui/scroll_view.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -42,10 +43,19 @@ void ScreenshotUI::update_layout(int width, int height) {
|
|||||||
int height = 0;
|
int height = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto& return_button = bg->add_child<Button>();
|
||||||
|
return_button.set_default_image(texture_manager);
|
||||||
|
return_button.set_anchor(Anchor::BOTTOM_CENTER);
|
||||||
|
return_button.set_offset({0, -30});
|
||||||
|
return_button.set_text(tr("button.return"));
|
||||||
|
return_button.set_clicked(
|
||||||
|
[this]() { m_scene.scene_manager().request_pop(); });
|
||||||
|
|
||||||
fs::path path = Renderer::SCREENSHOT_PATH;
|
fs::path path = Renderer::SCREENSHOT_PATH;
|
||||||
|
|
||||||
std::vector<ImageInfo> image_infos;
|
std::vector<ImageInfo> image_infos;
|
||||||
constexpr float IMAGE_SCALE = 0.25;
|
constexpr float IMAGE_SCALE = 0.25;
|
||||||
|
|
||||||
if (fs::exists(path)) {
|
if (fs::exists(path)) {
|
||||||
for (auto& entry : fs::recursive_directory_iterator(path)) {
|
for (auto& entry : fs::recursive_directory_iterator(path)) {
|
||||||
if (!fs::is_regular_file(entry)) {
|
if (!fs::is_regular_file(entry)) {
|
||||||
@@ -66,18 +76,26 @@ void ScreenshotUI::update_layout(int width, int height) {
|
|||||||
constexpr int SPACING = 10;
|
constexpr int SPACING = 10;
|
||||||
|
|
||||||
const int WIDGET_WIDTH = std::min(width, width - 2 * PADDING);
|
const int WIDGET_WIDTH = std::min(width, width - 2 * PADDING);
|
||||||
// const int WIDGET_HEIGHT = std::max(height, height - 2 * PADDING);
|
|
||||||
|
|
||||||
float cur_width = 0.0f;
|
float cur_width = 0.0f;
|
||||||
// float cur_height = 0.0f;
|
|
||||||
auto& root_column = bg->add_child<ColumnLayout>();
|
auto& scroll = bg->add_child<ScrollView>();
|
||||||
root_column.set_child_anchor(ColumnLayoutAnchor::LEFT)
|
|
||||||
.set_spacing(10)
|
scroll.set_anchor(Anchor::TOP_CENTER)
|
||||||
.set_anchor(Anchor::TOP_CENTER)
|
|
||||||
.set_offset({0, 10 + title.height()});
|
.set_offset({0, 10 + title.height()});
|
||||||
|
|
||||||
|
float scroll_height = height - scroll.get_offset().y +
|
||||||
|
return_button.get_offset().y -
|
||||||
|
return_button.height();
|
||||||
|
|
||||||
|
scroll.set_height(std::max(0.0f, scroll_height));
|
||||||
|
auto root_column = std::make_unique<ColumnLayout>(&scroll);
|
||||||
|
root_column->set_child_anchor(ColumnLayoutAnchor::LEFT)
|
||||||
|
.set_spacing(10)
|
||||||
|
.set_anchor(Anchor::TOP_CENTER);
|
||||||
RowLayout* row = nullptr;
|
RowLayout* row = nullptr;
|
||||||
auto create_row = [&row, &root_column]() {
|
auto create_row = [&row, &root_column]() {
|
||||||
row = &root_column.add_child<RowLayout>();
|
row = &root_column->add_child<RowLayout>();
|
||||||
row->set_anchor(Anchor::TOP_LEFT);
|
row->set_anchor(Anchor::TOP_LEFT);
|
||||||
row->set_spacing(SPACING);
|
row->set_spacing(SPACING);
|
||||||
};
|
};
|
||||||
@@ -95,6 +113,7 @@ void ScreenshotUI::update_layout(int width, int height) {
|
|||||||
auto& im = row->add_child<Image>();
|
auto& im = row->add_child<Image>();
|
||||||
im.set_texture(image.texture, true).set_scale(IMAGE_SCALE);
|
im.set_texture(image.texture, true).set_scale(IMAGE_SCALE);
|
||||||
}
|
}
|
||||||
|
scroll.set_child(std::move(root_column));
|
||||||
}
|
}
|
||||||
if (!fs::exists(path) || image_infos.empty()) {
|
if (!fs::exists(path) || image_infos.empty()) {
|
||||||
auto& label = bg->add_child<Label>();
|
auto& label = bg->add_child<Label>();
|
||||||
@@ -103,15 +122,6 @@ void ScreenshotUI::update_layout(int width, int height) {
|
|||||||
.set_anchor(Anchor::CENTER);
|
.set_anchor(Anchor::CENTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
auto& button = bg->add_child<Button>();
|
|
||||||
button.set_default_image(texture_manager);
|
|
||||||
button.set_anchor(Anchor::BOTTOM_CENTER);
|
|
||||||
button.set_offset({0, -50});
|
|
||||||
button.set_text(tr("button.return"));
|
|
||||||
button.set_clicked([this]() { m_scene.scene_manager().request_pop(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
m_root_widget = std::move(bg);
|
m_root_widget = std::move(bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
112
src/ui/scroll_view.cpp
Normal file
112
src/ui/scroll_view.cpp
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#include "Cubed/ui/scroll_view.hpp"
|
||||||
|
|
||||||
|
#include "Cubed/render/renderer.hpp"
|
||||||
|
#include "Cubed/tools/log.hpp"
|
||||||
|
#include "Cubed/ui/column_layout.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <glad/glad.h>
|
||||||
|
namespace Cubed {
|
||||||
|
ScrollView::ScrollView(Widget* parent) : Widget(parent) {}
|
||||||
|
ScrollView::~ScrollView() {}
|
||||||
|
|
||||||
|
void ScrollView::render(Renderer& renderer) {
|
||||||
|
if (!m_visible)
|
||||||
|
return;
|
||||||
|
auto p = pos();
|
||||||
|
float h = height();
|
||||||
|
float w = width();
|
||||||
|
|
||||||
|
// convert window coords to framebuffer coords for scissor.
|
||||||
|
float sx = renderer.window_width() > 0
|
||||||
|
? renderer.frame_width() / renderer.window_width()
|
||||||
|
: 1.0f;
|
||||||
|
float sy = renderer.window_height() > 0
|
||||||
|
? renderer.frame_height() / renderer.window_height()
|
||||||
|
: 1.0f;
|
||||||
|
|
||||||
|
GLint scissor_x = static_cast<GLint>(p.x * sx);
|
||||||
|
GLint scissor_y = static_cast<GLint>((m_window_height - p.y - h) * sy);
|
||||||
|
GLsizei scissor_w = static_cast<GLsizei>(std::max(0.0f, w * sx));
|
||||||
|
GLsizei scissor_h = static_cast<GLsizei>(std::max(0.0f, h * sy));
|
||||||
|
|
||||||
|
glEnable(GL_SCISSOR_TEST);
|
||||||
|
glScissor(scissor_x, scissor_y, scissor_w, scissor_h);
|
||||||
|
|
||||||
|
Widget::render(renderer);
|
||||||
|
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
float ScrollView::width() const {
|
||||||
|
if (Widget::width() > 0) {
|
||||||
|
return Widget::width();
|
||||||
|
}
|
||||||
|
if (m_child) {
|
||||||
|
return m_child->width();
|
||||||
|
}
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float ScrollView::height() const {
|
||||||
|
if (Widget::height() > 0) {
|
||||||
|
return Widget::height();
|
||||||
|
}
|
||||||
|
Logger::warn("You should set Scroll view height!");
|
||||||
|
if (m_parent) {
|
||||||
|
return m_parent->height();
|
||||||
|
}
|
||||||
|
return m_window_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView& ScrollView::set_child(std::unique_ptr<ColumnLayout> children) {
|
||||||
|
Widget::children().clear();
|
||||||
|
m_child = children.get();
|
||||||
|
Widget::children().emplace_back(std::move(children));
|
||||||
|
m_child->set_offset({0, 0});
|
||||||
|
m_child->set_fill_height(false);
|
||||||
|
m_child->set_fill_parent(false);
|
||||||
|
m_child->set_fill_width(false);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScrollView::handle_mouse_move_event(const MouseMoveEvent& e) {
|
||||||
|
if (Widget::handle_mouse_move_event(e)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
auto p = pos();
|
||||||
|
if (e.xpos >= p.x && e.xpos <= p.x + width() && e.ypos >= p.y &&
|
||||||
|
e.ypos <= p.y + height()) {
|
||||||
|
m_hovered = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
m_hovered = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScrollView::handle_mouse_wheel_event(const MouseWheelEvent& e) {
|
||||||
|
if (m_hovered && m_child) {
|
||||||
|
float c_height = m_child->height();
|
||||||
|
float offset = m_child->get_offset().y;
|
||||||
|
float viewport = height();
|
||||||
|
float max_offset = std::max(0.0f, c_height - viewport);
|
||||||
|
float new_offset =
|
||||||
|
std::clamp(offset + e.offset * m_scroll_speed, -max_offset, 0.0f);
|
||||||
|
m_child->set_offset({0, new_offset});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Widget::handle_mouse_wheel_event(e);
|
||||||
|
}
|
||||||
|
Widget& ScrollView::set_fill_parent(bool) {
|
||||||
|
Logger::warn("You should not use this function in scroll view!");
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
Widget& ScrollView::set_fill_width(bool) {
|
||||||
|
Logger::warn("You should not use this function in scroll view!");
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
Widget& ScrollView::set_fill_height(bool) {
|
||||||
|
Logger::warn("You should not use this function in scroll view!");
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
} // namespace Cubed
|
||||||
@@ -158,10 +158,16 @@ Widget& Widget::set_anchor(Anchor anchor) {
|
|||||||
m_anchor = anchor;
|
m_anchor = anchor;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
Widget& Widget::set_offset(glm::ivec2 offset) {
|
Widget& Widget::set_offset(glm::vec2 offset) {
|
||||||
m_offset = offset;
|
m_offset = offset;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
Widget& Widget::add_offset(glm::vec2 offset) {
|
||||||
|
m_offset += offset;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
glm::vec2 Widget::get_offset() const { return m_offset; }
|
||||||
|
|
||||||
void Widget::set_window_size(int width, int height) {
|
void Widget::set_window_size(int width, int height) {
|
||||||
m_window_height = height;
|
m_window_height = height;
|
||||||
m_window_width = width;
|
m_window_width = width;
|
||||||
|
|||||||
Reference in New Issue
Block a user