feat(screenshot): add screenshot viewer scene

Add SCREENSHOT scene and UI to browse screenshots from the main menu,
with localization for English and Chinese. Also allow image loading
by full path and replace scene-operation asserts with error logs.
This commit is contained in:
2026-08-07 14:34:58 +08:00
parent 607be871b1
commit ef0fc7c5c0
12 changed files with 234 additions and 13 deletions

View File

@@ -4,6 +4,7 @@
"menu.main.join_game": "Join Game", "menu.main.join_game": "Join Game",
"menu.main.credits": "Credits", "menu.main.credits": "Credits",
"menu.main.quit": "Quit", "menu.main.quit": "Quit",
"menu.screenshot": "Screenshots",
"menu.main.player_name": "Player: {name}", "menu.main.player_name": "Player: {name}",
"menu.main.cubed_version": "Cubed: {version}", "menu.main.cubed_version": "Cubed: {version}",
"menu.pause.pause_menu": "Pause Menu", "menu.pause.pause_menu": "Pause Menu",
@@ -46,5 +47,6 @@
"item.snowy_grass_block.name": "snowy grass block", "item.snowy_grass_block.name": "snowy grass block",
"item.stone.name": "stone", "item.stone.name": "stone",
"item.water.name": "water", "item.water.name": "water",
"item.pig_spawn_egg.name": "pig spawn egg" "item.pig_spawn_egg.name": "pig spawn egg",
"screenshot.no_screenshot": "No Screenshot"
} }

View File

@@ -4,6 +4,7 @@
"menu.main.join_game": "加入游戏", "menu.main.join_game": "加入游戏",
"menu.main.credits": "制作人员", "menu.main.credits": "制作人员",
"menu.main.quit": "退出", "menu.main.quit": "退出",
"menu.screenshot": "屏幕截图",
"menu.main.player_name": "玩家:{name}", "menu.main.player_name": "玩家:{name}",
"menu.main.cubed_version": "Cubed{version}", "menu.main.cubed_version": "Cubed{version}",
"menu.pause.pause_menu": "暂停菜单", "menu.pause.pause_menu": "暂停菜单",
@@ -46,5 +47,6 @@
"item.snowy_grass_block.name": "覆雪草方块", "item.snowy_grass_block.name": "覆雪草方块",
"item.stone.name": "石头", "item.stone.name": "石头",
"item.water.name": "水", "item.water.name": "水",
"item.pig_spawn_egg.name": "猪猪刷怪蛋" "item.pig_spawn_egg.name": "猪猪刷怪蛋",
"screenshot.no_screenshot": "没有截图"
} }

View File

@@ -9,7 +9,8 @@ enum class SceneType {
CREDITS, CREDITS,
SETTINGS, SETTINGS,
HOST_GAME, HOST_GAME,
JOIN_GAME JOIN_GAME,
SCREENSHOT
}; };
class Scene { class Scene {

View File

@@ -0,0 +1,29 @@
#pragma once
#include "Cubed/scene/scene.hpp"
#include "Cubed/ui/screenshot_ui.hpp"
namespace Cubed {
class SceneManager;
class ScreenshotScene : public Scene {
public:
ScreenshotScene(SceneManager& scene_manager);
~ScreenshotScene();
ScreenshotScene(const ScreenshotScene&) = delete;
ScreenshotScene(ScreenshotScene&&) = delete;
ScreenshotScene& operator=(const ScreenshotScene&) = delete;
ScreenshotScene& operator=(ScreenshotScene&&) = delete;
void update(float dt) override;
void render(Renderer& renderer) override;
bool handle_event(const Event& e) override;
void on_enter() override;
void on_leave() override;
void on_re_enter() override;
SceneManager& scene_manager();
private:
SceneManager& m_scene_manager;
ScreenshotUI m_ui;
};
} // namespace Cubed

View File

@@ -20,7 +20,8 @@ public:
const Texture* get_block_status_array() const; const Texture* get_block_status_array() const;
const Texture* get_texture_array() const; const Texture* get_texture_array() const;
const Texture* get_cross_plane_array() const; const Texture* get_cross_plane_array() const;
const Texture* get_image_texture(const std::string& path); const Texture* get_image_texture(const std::string& path,
bool full_path = false);
const Texture* get_pbr_texture() const; const Texture* get_pbr_texture() const;
const ItemTextureMap& get_item_textures() const; const ItemTextureMap& get_item_textures() const;
const Texture* get_skin() const; const Texture* get_skin() const;
@@ -49,7 +50,8 @@ private:
void load_block_texture(unsigned block_id); void load_block_texture(unsigned block_id);
void init_item_texture(); void init_item_texture();
void load_cross_plane_texture(unsigned id); void load_cross_plane_texture(unsigned id);
const Texture* load_image_texture(const std::string& path); const Texture* load_image_texture(const std::string& path,
bool full_path = false);
void load_pbr_texture(unsigned id); void load_pbr_texture(unsigned id);
void init_item(); void init_item();
void init_block(); void init_block();

View File

@@ -0,0 +1,23 @@
#pragma once
#include "Cubed/ui/ui_manager.hpp"
namespace Cubed {
class ScreenshotScene;
class ScreenshotUI : public UIManager {
public:
ScreenshotUI(ScreenshotScene& m_scene);
~ScreenshotUI();
ScreenshotUI(const ScreenshotUI&) = delete;
ScreenshotUI(ScreenshotUI&&) = delete;
ScreenshotUI& operator=(const ScreenshotUI&) = delete;
ScreenshotUI& operator=(ScreenshotUI&&) = delete;
void init();
void update_layout(int width, int height);
private:
ScreenshotScene& m_scene;
};
} // namespace Cubed

View File

@@ -106,4 +106,6 @@ target_sources(${PROJECT_NAME}
tools/json_utils.cpp tools/json_utils.cpp
gameplay/item_manager.cpp gameplay/item_manager.cpp
gameplay/block_manager.cpp gameplay/block_manager.cpp
ui/screenshot_ui.cpp
scene/screenshot_scene.cpp
) )

View File

@@ -4,6 +4,7 @@
#include "Cubed/scene/host_game_scene.hpp" #include "Cubed/scene/host_game_scene.hpp"
#include "Cubed/scene/join_game_scene.hpp" #include "Cubed/scene/join_game_scene.hpp"
#include "Cubed/scene/main_menu_scene.hpp" #include "Cubed/scene/main_menu_scene.hpp"
#include "Cubed/scene/screenshot_scene.hpp"
#include "Cubed/scene/settings_scene.hpp" #include "Cubed/scene/settings_scene.hpp"
#include "Cubed/scene/world_scene.hpp" #include "Cubed/scene/world_scene.hpp"
@@ -33,15 +34,24 @@ bool SceneManager::handle_event(const Event& e) {
} }
void SceneManager::request_change(SceneType type) { void SceneManager::request_change(SceneType type) {
ASSERT(!m_operation.has_value()); if (m_operation.has_value()) {
Logger::error("Scene operation has_value");
return;
}
m_operation = {OperationType::CHANGE, type}; m_operation = {OperationType::CHANGE, type};
} }
void SceneManager::request_push(SceneType type) { void SceneManager::request_push(SceneType type) {
ASSERT(!m_operation.has_value()); if (m_operation.has_value()) {
Logger::error("Scene operation has_value");
return;
}
m_operation = {OperationType::PUSH, type}; m_operation = {OperationType::PUSH, type};
} }
void SceneManager::request_pop() { void SceneManager::request_pop() {
ASSERT(!m_operation.has_value()); if (m_operation.has_value()) {
Logger::error("Scene operation has_value");
return;
}
m_operation = {OperationType::POP, std::nullopt}; m_operation = {OperationType::POP, std::nullopt};
} }
@@ -103,6 +113,8 @@ std::unique_ptr<Scene> SceneManager::create_scene(SceneType type) {
return std::make_unique<HostGameScene>(*this); return std::make_unique<HostGameScene>(*this);
case SceneType::JOIN_GAME: case SceneType::JOIN_GAME:
return std::make_unique<JoinGameScene>(*this); return std::make_unique<JoinGameScene>(*this);
case SceneType::SCREENSHOT:
return std::make_unique<ScreenshotScene>(*this);
} }
std::string err = std::format("Unknown Scene"); std::string err = std::format("Unknown Scene");

View File

@@ -0,0 +1,20 @@
#include "Cubed/scene/screenshot_scene.hpp"
namespace Cubed {
ScreenshotScene::ScreenshotScene(SceneManager& scene_manager)
: m_scene_manager(scene_manager), m_ui(*this) {}
ScreenshotScene::~ScreenshotScene() {}
void ScreenshotScene::update(float dt) { m_ui.update(dt); }
void ScreenshotScene::render(Renderer& renderer) { m_ui.render(renderer); }
bool ScreenshotScene::handle_event(const Event& e) {
return m_ui.handle_event(e);
}
void ScreenshotScene::on_enter() { m_ui.init(); }
void ScreenshotScene::on_leave() {}
void ScreenshotScene::on_re_enter() {}
SceneManager& ScreenshotScene::scene_manager() { return m_scene_manager; }
} // namespace Cubed

View File

@@ -57,12 +57,13 @@ const Texture* TextureManager::get_texture_array() const {
const Texture* TextureManager::get_cross_plane_array() const { const Texture* TextureManager::get_cross_plane_array() const {
return m_cross_plane_array.get(); return m_cross_plane_array.get();
} }
const Texture* TextureManager::get_image_texture(const std::string& path) { const Texture* TextureManager::get_image_texture(const std::string& path,
bool full_path) {
auto it = m_ui_map.find(path); auto it = m_ui_map.find(path);
if (it != m_ui_map.end()) { if (it != m_ui_map.end()) {
return it->second.get(); return it->second.get();
} }
return load_image_texture(path); return load_image_texture(path, full_path);
} }
const Texture* TextureManager::get_pbr_texture() const { const Texture* TextureManager::get_pbr_texture() const {
@@ -151,9 +152,10 @@ void TextureManager::load_cross_plane_texture(unsigned id) {
CROSS_PLANE_SIZE, CROSS_PLANE_SIZE); CROSS_PLANE_SIZE, CROSS_PLANE_SIZE);
} }
const Texture* TextureManager::load_image_texture(const std::string& path) { const Texture* TextureManager::load_image_texture(const std::string& path,
bool full_path) {
auto image_data = (Tools::load_image_data(path)); auto image_data = (Tools::load_image_data(path, true, full_path));
std::unique_ptr<Texture> image = std::unique_ptr<Texture> image =
std::make_unique<Texture>(TextureType::TEXTURE_2D); std::make_unique<Texture>(TextureType::TEXTURE_2D);
image->tex_image_2d(RGBA, RGBA, GL_UNSIGNED_BYTE, image_data.data, image->tex_image_2d(RGBA, RGBA, GL_UNSIGNED_BYTE, image_data.data,

View File

@@ -58,7 +58,16 @@ void MainMenuUIManager::init() {
}); });
m_pending_enable.emplace_back(&button); m_pending_enable.emplace_back(&button);
} }
{
auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager);
button.set_text(tr("menu.screenshot"));
button.set_clicked([this, &button]() {
button.set_enable(false);
m_scene.scene_manager().request_push(SceneType::SCREENSHOT);
});
m_pending_enable.emplace_back(&button);
}
{ {
auto& button = layout.add_child<Button>(); auto& button = layout.add_child<Button>();

117
src/ui/screenshot_ui.cpp Normal file
View File

@@ -0,0 +1,117 @@
#include "Cubed/ui/screenshot_ui.hpp"
#include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/render/renderer.hpp"
#include "Cubed/scene/screenshot_scene.hpp"
#include "Cubed/ui/button.hpp"
#include "Cubed/ui/column_layout.hpp"
#include "Cubed/ui/row_layout.hpp"
#include <algorithm>
#include <array>
#include <filesystem>
namespace fs = std::filesystem;
namespace {
constexpr std::array<std::string_view, 4> IMAGE_EXT{".jpg", ".jpeg", ".png",
".bmp"};
}
namespace Cubed {
ScreenshotUI::ScreenshotUI(ScreenshotScene& scene) : m_scene(scene) {}
ScreenshotUI::~ScreenshotUI() {}
void ScreenshotUI::init() {
auto& renderer = m_scene.scene_manager().app().renderer();
update_layout(renderer.window_width(), renderer.window_height());
}
void ScreenshotUI::update_layout(int width, int height) {
auto& texture_manager = m_scene.scene_manager().app().texture_manager();
m_root_widget.reset();
auto bg = std::make_unique<Rect>(nullptr);
bg->set_color(Color::BLACK).set_fill_parent(true);
auto& title = bg->add_child<Label>();
title.set_text(tr("menu.screenshot")).set_anchor(Anchor::TOP_CENTER);
title.set_offset({0, 5});
struct ImageInfo {
const Texture* texture = nullptr;
int width = 0;
int height = 0;
};
fs::path path = Renderer::SCREENSHOT_PATH;
std::vector<ImageInfo> image_infos;
constexpr float IMAGE_SCALE = 0.25;
if (fs::exists(path)) {
for (auto& entry : fs::recursive_directory_iterator(path)) {
if (!fs::is_regular_file(entry)) {
continue;
}
if (!std::ranges::find(IMAGE_EXT, entry.path().extension())) {
continue;
}
ImageInfo info;
Logger::info("Path: {}", entry.path().string());
info.texture =
texture_manager.get_image_texture(entry.path().string(), true);
info.height = info.texture->height() * IMAGE_SCALE;
info.width = info.texture->width() * IMAGE_SCALE;
image_infos.emplace_back(std::move(info));
}
constexpr int PADDING = 50;
constexpr int SPACING = 10;
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_height = 0.0f;
auto& root_column = bg->add_child<ColumnLayout>();
root_column.set_child_anchor(ColumnLayoutAnchor::LEFT)
.set_spacing(10)
.set_anchor(Anchor::TOP_CENTER)
.set_offset({0, 10 + title.height()});
RowLayout* row = nullptr;
auto create_row = [&row, &root_column]() {
row = &root_column.add_child<RowLayout>();
row->set_anchor(Anchor::TOP_LEFT);
row->set_spacing(SPACING);
};
for (auto& image : image_infos) {
if (!row) {
create_row();
}
if (cur_width + image.width > WIDGET_WIDTH && cur_width > 0) {
cur_width = 0.0f;
create_row();
}
cur_width += image.width;
auto& im = row->add_child<Image>();
im.set_texture(image.texture, true).set_scale(IMAGE_SCALE);
}
}
if (!fs::exists(path) || image_infos.empty()) {
auto& label = bg->add_child<Label>();
label.set_text(tr("screenshot.no_screenshot"))
.set_color(Color::WHITE)
.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);
}
} // namespace Cubed