5 Commits

Author SHA1 Message Date
0919faa3e4 fix: correct argument typo and path/error handling
Fix --enable-consolelog flag, screenshot filter bug, paths to C APIs, add includes and gitignore entries, and improve ShellExecuteW failure detection.
2026-08-07 21:33:46 +08:00
d9b4b42f74 feat(screenshot-ui): add newest/oldest sort selector
Add a combo button to the screenshot gallery to sort files by modification time (newest or oldest first). Store timestamp per image and rebuild layout when sort order changes. Include translations for en_US and zh_CN.
2026-08-07 20:22:50 +08:00
d7cb178c2b feat(ui): add show in file manager button to screenshot view
- Add Tools::open_file_manager helper with Windows and Linux support.
- Ensure screenshot directory is created before scanning.
- Add localization keys for en_US and zh_CN.
2026-08-07 19:49:53 +08:00
4fef8e195a feat(ui): add screenshot button to pause menu
Add a button to open the screenshot scene from the pause menu.
Also enable linear filtering on screenshot textures for smoother scaling.
2026-08-07 18:21:26 +08:00
5f60910aeb feat(ui): add lightbox viewer for screenshots
Add a lightbox to preview screenshots on click. Introduce Button::set_texture for image-backed buttons, add widget getters for window size and visibility, and block background input while the lightbox is open. Close the lightbox with Escape or a left-click.
2026-08-07 18:09:30 +08:00
17 changed files with 319 additions and 41 deletions

1
.gitignore vendored
View File

@@ -14,6 +14,7 @@
*.pch
*.sbr
build/
build_debug/
Build/
bin/
Debug/

View File

@@ -48,5 +48,9 @@
"item.stone.name": "stone",
"item.water.name": "water",
"item.pig_spawn_egg.name": "pig spawn egg",
"screenshot.no_screenshot": "No Screenshot"
"screenshot.no_screenshot": "No Screenshot",
"button.show_in_file_manager": "Show In File Manager",
"file.sort_type": "File Sort Type: {type}",
"file.newest_first": "Newest First",
"file.oldest_first": "Oldest First"
}

View File

@@ -48,5 +48,9 @@
"item.stone.name": "石头",
"item.water.name": "水",
"item.pig_spawn_egg.name": "猪猪刷怪蛋",
"screenshot.no_screenshot": "没有截图"
"screenshot.no_screenshot": "没有截图",
"button.show_in_file_manager": "在文件管理器中显示",
"file.sort_type": "文件排序方式:{type}",
"file.newest_first": "最新在前",
"file.oldest_first": "最旧在前"
}

View File

@@ -7,6 +7,7 @@
#include <tbb/concurrent_hash_map.h>
#include <tbb/concurrent_queue.h>
#include <tbb/concurrent_vector.h>
#include <variant>
namespace Cubed {
class ServerWorld;
class Session;

View File

@@ -0,0 +1,66 @@
#pragma once
#include "Cubed/tools/log.hpp"
#include <filesystem>
#include <string>
#ifdef _WIN32
// clang-format off
#include <windows.h>
#include <shellapi.h>
// clang-format on
#elif __linux__
#include <cstdlib>
#endif
namespace Cubed::Tools {
inline void open_file_manager(const std::filesystem::path& path) {
#ifdef _WIN32
auto abs_path = std::filesystem::absolute(path);
if (!std::filesystem::exists(abs_path)) {
Logger::warn("Path does not exist: {}", abs_path.string());
return;
}
HINSTANCE result =
ShellExecuteW(nullptr, L"open", abs_path.wstring().c_str(), nullptr,
nullptr, SW_SHOWNORMAL);
if ((INT_PTR)result <= 32) {
Logger::warn("ShellExecuteW failed for path: {}, error code: {}",
path.string(), (int)(INT_PTR)result);
}
#elif __linux__
constexpr const char* FILE_MANAGERS[] = {"nautilus", "dolphin", "thunar",
"nemo", "pcmanfm"};
auto is_available = [](const char* exe) {
std::string cmd = std::string("command -v ") + exe + " >/dev/null 2>&1";
return std::system(cmd.c_str()) == 0;
};
const std::string QUOTED = "\"" + path.string() + "\"";
for (const char* fm : FILE_MANAGERS) {
if (!is_available(fm)) {
continue;
}
std::string cmd = std::string(fm) + " " + QUOTED + " >/dev/null 2>&1 &";
if (std::system(cmd.c_str()) == 0) {
return;
}
}
// Fallback: let the system choose a handler.
if (is_available("xdg-open")) {
std::string cmd = "xdg-open " + QUOTED + " >/dev/null 2>&1 &";
if (std::system(cmd.c_str()) == 0) {
return;
}
}
Logger::warn("Failed to open file manager for {}", path.string());
#endif
}
} // namespace Cubed::Tools

View File

@@ -1,8 +1,10 @@
#pragma once
#include <rapidjson/document.h>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace Cubed {
class SensitiveFilter {
public:

View File

@@ -28,6 +28,7 @@ public:
Button& set_background_image(const std::string& path,
TextureManager& texture_manager);
Button& set_default_image(TextureManager& texture_manager);
Button& set_texture(const Texture* texture, bool change_button_size);
virtual Button& set_text(const std::string& text);
Button& set_auto_scale(bool auto_scale);

View File

@@ -1,7 +1,11 @@
#pragma once
#include "Cubed/ui/image.hpp"
#include "Cubed/ui/ui_manager.hpp"
#include <chrono>
namespace Cubed {
class Texture;
class ScreenshotScene;
class ScreenshotUI : public UIManager {
public:
@@ -17,9 +21,29 @@ public:
void update_layout(int width, int height);
void open_lightbox(const Texture* image);
void update(float dt) override;
private:
bool handle_window_resize_event(const WindowResizeEvent& e) override;
struct ImageInfo {
const Texture* texture = nullptr;
int width = 0;
int height = 0;
std::chrono::nanoseconds time{0};
};
enum class SortType { NEWEST_FIRST, OLDEST_FIRST };
ScreenshotScene& m_scene;
std::vector<ImageInfo> m_image_infos;
SortType m_sort_type = SortType::NEWEST_FIRST;
bool m_need_rebuild = false;
Widget* m_lightbox = nullptr;
Image* m_lightbox_image = nullptr;
bool handle_window_resize_event(const WindowResizeEvent& e) override;
bool handle_key_event(const KeyEvent& e) override;
bool handle_mouse_button_event(const MouseButtonEvent& e) override;
bool handle_mouse_move_event(const MouseMoveEvent& e) override;
bool handle_mouse_wheel_event(const MouseWheelEvent& e) override;
};
} // namespace Cubed

View File

@@ -28,6 +28,11 @@ public:
Widget& operator=(Widget&&) = delete;
Widget(Widget* parent);
static void set_window_size(int width, int height);
static int get_window_height();
static int get_window_width();
virtual ~Widget() = default;
virtual void update(float dt);
virtual void render(Renderer& renderer);
@@ -35,7 +40,7 @@ public:
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);
virtual Widget& set_visible(bool visible);
// Returns the final display size
@@ -58,6 +63,8 @@ public:
virtual bool handle_window_resize_event(const WindowResizeEvent& e);
virtual bool handle_mouse_move_event(const MouseMoveEvent& e);
virtual bool handle_text_input_event(const TextInputEvent& e);
virtual bool is_visible() const;
void set_order(TraversalOrder order);
template <typename T, typename... Args> T& add_child(Args&&... args) {

View File

@@ -187,7 +187,7 @@ void App::handle_argument(int argc, char** argv) {
}},
{"--enable-filelog",
[&](ArgParser&) { m_argument.enable_filelog = true; }},
{"--enale-consolelog",
{"--enable-consolelog",
[&](ArgParser&) { m_argument.enable_consolelog = true; }},
{"--direct-enter",
[&](ArgParser&) { m_argument.direct_enter = true; }}

View File

@@ -88,7 +88,7 @@ ModelManager::Handle ModelManager::load_model(std::string_view model_name) {
auto model = m_loader.load(path);
fs::path anim_path = path;
anim_path = anim_path.parent_path() / "animation.json";
load_anim_config(model, anim_path);
load_anim_config(model, anim_path.string());
ModelMap::accessor acc;
if (m_models.insert(acc, m_next++)) {
acc->second = std::move(model);

View File

@@ -311,11 +311,11 @@ void Renderer::handle_screenshot() {
fs::create_directories(path);
fs::path image =
path / std::format("screenshot {}.png", Tools::get_time_date_str());
if (!stbi_write_png(image.c_str(), width, height, 4, flipped_pixels.data(),
width * 4)) {
Logger::error("Failed to write {} image", image.c_str());
if (!stbi_write_png(image.string().c_str(), width, height, 4,
flipped_pixels.data(), width * 4)) {
Logger::error("Failed to write {} image", image.string());
} else {
Logger::info("save {} success!", image.c_str());
Logger::info("save {} success!", image.string());
}
}

View File

@@ -184,7 +184,7 @@ ImageData load_image_data(const std::string& tex_image_path, bool check_exist,
path = ASSETS_PATH + tex_image_path;
}
if (check_exist) {
ASSERT_MSG(fs::is_regular_file(path), path.c_str());
ASSERT_MSG(fs::is_regular_file(path), path.string().c_str());
}
unsigned char* data = nullptr;
int width, height, channels;

View File

@@ -103,7 +103,18 @@ Button& Button::set_background_image(const std::string& path,
Button& Button::set_default_image(TextureManager& texture_manager) {
return set_background_image(DEFAULT_BUTTON_IMAGE, texture_manager);
}
Button& Button::set_texture(const Texture* texture, bool change_button_size) {
if (!texture) {
Logger::error("Button Background Image is nullptr");
return *this;
}
m_background->set_texture(texture, false);
if (change_button_size) {
set_width(texture->width());
set_height(texture->height());
}
return *this;
}
Button& Button::set_text(const std::string& text) {
m_foreground->set_text(text);
update_text_scale();

View File

@@ -41,7 +41,16 @@ void PauseMenuUIManager::init() {
});
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& back_main = layout.add_child<Button>();

View File

@@ -4,14 +4,17 @@
#include "Cubed/localization.hpp"
#include "Cubed/render/renderer.hpp"
#include "Cubed/scene/screenshot_scene.hpp"
#include "Cubed/tools/file_utils.hpp"
#include "Cubed/ui/button.hpp"
#include "Cubed/ui/column_layout.hpp"
#include "Cubed/ui/combo_button.hpp"
#include "Cubed/ui/row_layout.hpp"
#include "Cubed/ui/scroll_view.hpp"
#include <algorithm>
#include <array>
#include <filesystem>
namespace fs = std::filesystem;
namespace {
constexpr std::array<std::string_view, 4> IMAGE_EXT{".jpg", ".jpeg", ".png",
@@ -27,6 +30,9 @@ void ScreenshotUI::init() {
update_layout(renderer.window_width(), renderer.window_height());
}
void ScreenshotUI::update_layout(int width, int height) {
m_image_infos.clear();
auto& texture_manager = m_scene.scene_manager().app().texture_manager();
m_root_widget.reset();
auto bg = std::make_unique<Rect>(nullptr);
@@ -37,40 +43,39 @@ void ScreenshotUI::update_layout(int width, int height) {
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;
};
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;
std::vector<ImageInfo> image_infos;
constexpr float IMAGE_SCALE = 0.25;
ScrollView* sv = nullptr;
fs::create_directories(path);
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())) {
if (std::ranges::find(IMAGE_EXT, entry.path().extension()) ==
IMAGE_EXT.end()) {
continue;
}
ImageInfo info;
Logger::info("Path: {}", entry.path().string());
info.texture =
texture_manager.get_image_texture(entry.path().string(), true);
info.texture->set_linear();
info.height = info.texture->height() * IMAGE_SCALE;
info.width = info.texture->width() * IMAGE_SCALE;
image_infos.emplace_back(std::move(info));
std::error_code ec;
auto ftime = fs::last_write_time(entry, ec);
if (!ec) {
auto sys_time =
std::chrono::clock_cast<std::chrono::system_clock>(ftime);
info.time =
std::chrono::time_point_cast<std::chrono::nanoseconds>(
sys_time)
.time_since_epoch();
}
m_image_infos.emplace_back(std::move(info));
}
constexpr int PADDING = 50;
constexpr int SPACING = 10;
@@ -81,14 +86,11 @@ void ScreenshotUI::update_layout(int width, int height) {
auto& scroll = bg->add_child<ScrollView>();
sv = &scroll;
scroll.set_anchor(Anchor::TOP_CENTER)
.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)
@@ -100,7 +102,19 @@ void ScreenshotUI::update_layout(int width, int height) {
row->set_spacing(SPACING);
};
for (auto& image : image_infos) {
std::sort(m_image_infos.begin(), m_image_infos.end(),
[this](const ImageInfo& a, const ImageInfo& b) {
switch (m_sort_type) {
case SortType::NEWEST_FIRST:
return a.time > b.time;
case SortType::OLDEST_FIRST:
return a.time < b.time;
default:
return a.time > b.time;
}
});
for (auto& image : m_image_infos) {
if (!row) {
create_row();
}
@@ -110,24 +124,154 @@ void ScreenshotUI::update_layout(int width, int height) {
create_row();
}
cur_width += image.width;
auto& im = row->add_child<Image>();
im.set_texture(image.texture, true).set_scale(IMAGE_SCALE);
auto& button = row->add_child<Button>();
button.set_texture(image.texture, true).set_scale(IMAGE_SCALE);
button.set_clicked(
[image, this]() { open_lightbox(image.texture); });
}
scroll.set_child(std::move(root_column));
}
if (!fs::exists(path) || image_infos.empty()) {
if (!fs::exists(path) || m_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& combo = bg->add_child<ComboButton>();
combo.set_anchor(Anchor::TOP_RIGHT);
combo.set_offset(title.get_offset());
switch (m_sort_type) {
case SortType::NEWEST_FIRST:
combo.set_index(0);
break;
case SortType::OLDEST_FIRST:
combo.set_index(1);
break;
default:
combo.set_index(0);
}
combo.set_combo_text("file.sort_type", "type");
std::vector<ComboPair> combos;
combos.emplace_back(tr("file.newest_first"), [this]() {
m_sort_type = SortType::NEWEST_FIRST;
m_need_rebuild = true;
});
combos.emplace_back(tr("file.oldest_first"), [this]() {
m_sort_type = SortType::OLDEST_FIRST;
m_need_rebuild = true;
});
combo.set_default_image(texture_manager);
combo.set_combos(combos);
}
auto& bottom_row = bg->add_child<RowLayout>();
bottom_row.set_spacing(10);
bottom_row.set_anchor(Anchor::BOTTOM_CENTER).set_offset({0, -30});
auto& open_file = bottom_row.add_child<Button>();
open_file.set_default_image(texture_manager)
.set_text(tr("button.show_in_file_manager"))
.set_clicked(
[]() { Tools::open_file_manager(Renderer::SCREENSHOT_PATH); });
auto& return_button = bottom_row.add_child<Button>();
return_button.set_default_image(texture_manager);
return_button.set_text(tr("button.return"));
return_button.set_clicked(
[this]() { m_scene.scene_manager().request_pop(); });
if (sv) {
float scroll_height = height - sv->get_offset().y +
bottom_row.get_offset().y -
return_button.height();
sv->set_height(std::max(0.0f, scroll_height));
}
auto& rect = bg->add_child<Rect>();
rect.set_fill_parent(true);
rect.set_color(Color::BLACK).set_alpha(0.5f);
m_lightbox = &rect;
auto& lightbox_image = rect.add_child<Image>();
m_lightbox_image = &lightbox_image;
m_lightbox->set_visible(false);
m_root_widget = std::move(bg);
}
void ScreenshotUI::open_lightbox(const Texture* image) {
if (image) {
constexpr float PADDING = 50.0f;
m_lightbox_image->set_texture(image, true);
float target_height =
std::max(50.0f, Widget::get_window_height() - PADDING * 2);
float target_width =
std::max(50.0f, Widget::get_window_width() - PADDING * 2);
float image_width = image->width();
float image_height = image->height();
float scale_w = target_width / image_width;
float scale_h = target_height / image_height;
float scale = std::min(scale_h, scale_w);
m_lightbox_image->set_scale(scale);
m_lightbox_image->set_anchor(Anchor::CENTER);
}
m_lightbox->set_visible(true);
}
void ScreenshotUI::update(float dt) {
if (m_need_rebuild) {
m_need_rebuild = false;
update_layout(Widget::get_window_width(), Widget::get_window_height());
}
UIManager::update(dt);
}
bool ScreenshotUI::handle_window_resize_event(const WindowResizeEvent& e) {
update_layout(e.width, e.height);
return UIManager::handle_window_resize_event(e);
}
bool ScreenshotUI::handle_key_event(const KeyEvent& e) {
if (UIManager::handle_key_event(e)) {
return true;
}
if (e.key == Key::ESCAPE && e.action == KeyAction::PRESS) {
if (m_lightbox && m_lightbox->is_visible()) {
m_lightbox->set_visible(false);
return true;
}
m_scene.scene_manager().request_pop();
return true;
}
return false;
}
bool ScreenshotUI::handle_mouse_button_event(const MouseButtonEvent& e) {
if (m_lightbox && m_lightbox->is_visible()) {
if (e.key == MouseKey::LEFT_BUTTON && e.action == KeyAction::PRESS) {
m_lightbox->set_visible(false);
}
return true;
}
return UIManager::handle_mouse_button_event(e);
}
bool ScreenshotUI::handle_mouse_move_event(const MouseMoveEvent& e) {
if (m_lightbox && m_lightbox->is_visible()) {
return true;
}
return UIManager::handle_mouse_move_event(e);
}
bool ScreenshotUI::handle_mouse_wheel_event(const MouseWheelEvent& e) {
if (m_lightbox && m_lightbox->is_visible()) {
return true;
}
return UIManager::handle_mouse_wheel_event(e);
}
} // namespace Cubed

View File

@@ -174,6 +174,9 @@ void Widget::set_window_size(int width, int height) {
return;
}
int Widget::get_window_height() { return m_window_height; }
int Widget::get_window_width() { return m_window_width; }
Widget& Widget::set_visible(bool visible) {
m_visible = visible;
return *this;
@@ -335,4 +338,5 @@ bool Widget::handle_text_input_event(const TextInputEvent& e) {
return false;
}
void Widget::set_order(TraversalOrder order) { m_order = order; }
bool Widget::is_visible() const { return m_visible; }
} // namespace Cubed