[UI] Notification & Custom Font Support
This commit is contained in:
parent
069d33c03f
commit
0ec65be5ff
|
@ -10,6 +10,7 @@ project("xenia-cpu-ppc-tests")
|
|||
"capstone", -- cpu-backend-x64
|
||||
"fmt",
|
||||
"mspack",
|
||||
"imgui",
|
||||
"xenia-core",
|
||||
"xenia-cpu",
|
||||
"xenia-base",
|
||||
|
|
|
@ -5,6 +5,7 @@ test_suite("xenia-cpu-tests", project_root, ".", {
|
|||
links = {
|
||||
"capstone",
|
||||
"fmt",
|
||||
"imgui",
|
||||
"xenia-base",
|
||||
"xenia-core",
|
||||
"xenia-cpu",
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2023 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "achievement_manager.h"
|
||||
#include "xenia/emulator.h"
|
||||
#include "xenia/gpu/graphics_system.h"
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/kernel/util/shim_utils.h"
|
||||
#include "xenia/ui/imgui_notification.h"
|
||||
|
||||
DEFINE_bool(show_achievement_notification, true,
|
||||
"Show achievement notification on screen.", "UI");
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
|
||||
AchievementManager::AchievementManager(){};
|
||||
|
||||
void AchievementManager::EarnAchievement(uint64_t xuid, uint32_t title_id,
|
||||
uint32_t achievement_id) {
|
||||
const Emulator* emulator = kernel_state()->emulator();
|
||||
ui::WindowedAppContext& app_context =
|
||||
kernel_state()->emulator()->display_window()->app_context();
|
||||
ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer();
|
||||
|
||||
const util::XdbfGameData title_xdbf = kernel_state()->title_xdbf();
|
||||
const std::vector<util::XdbfAchievementTableEntry> achievements =
|
||||
title_xdbf.GetAchievements();
|
||||
|
||||
for (const util::XdbfAchievementTableEntry& entry : achievements) {
|
||||
if (entry.id == achievement_id) {
|
||||
const std::string label = title_xdbf.GetStringTableEntry(
|
||||
title_xdbf.default_language(), entry.label_id);
|
||||
const std::string desc = title_xdbf.GetStringTableEntry(
|
||||
title_xdbf.default_language(), entry.description_id);
|
||||
|
||||
XELOGI("Achievement unlocked: {}", label);
|
||||
const std::string description =
|
||||
fmt::format("{}G - {}", entry.gamerscore, label);
|
||||
|
||||
// Even if we disable popup we still should store info that this
|
||||
// achievement was earned.
|
||||
if (!cvars::show_achievement_notification) {
|
||||
continue;
|
||||
}
|
||||
|
||||
app_context.CallInUIThread([imgui_drawer, description]() {
|
||||
new xe::ui::AchievementNotificationWindow(
|
||||
imgui_drawer, "Achievement unlocked", description, 0,
|
||||
kernel_state()->notification_position_);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2023 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_KERNEL_ACHIEVEMENT_MANAGER_H_
|
||||
#define XENIA_KERNEL_ACHIEVEMENT_MANAGER_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
|
||||
class AchievementManager {
|
||||
public:
|
||||
AchievementManager();
|
||||
|
||||
void EarnAchievement(uint64_t xuid, uint32_t title_id,
|
||||
uint32_t achievement_id);
|
||||
|
||||
private:
|
||||
// void Load();
|
||||
// void Save();
|
||||
};
|
||||
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_KERNEL_ACHIEVEMENT_MANAGER_H_
|
|
@ -52,6 +52,7 @@ KernelState::KernelState(Emulator* emulator)
|
|||
file_system_ = emulator->file_system();
|
||||
|
||||
app_manager_ = std::make_unique<xam::AppManager>();
|
||||
achievement_manager_ = std::make_unique<AchievementManager>();
|
||||
user_profiles_.emplace(0, std::make_unique<xam::UserProfile>(0));
|
||||
|
||||
auto content_root = emulator_->content_root();
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
#include "xenia/memory.h"
|
||||
#include "xenia/vfs/virtual_file_system.h"
|
||||
#include "xenia/xbox.h"
|
||||
#include "achievement_manager.h"
|
||||
|
||||
namespace xe {
|
||||
class ByteStream;
|
||||
|
@ -103,6 +104,9 @@ class KernelState {
|
|||
util::XdbfGameData title_xdbf() const;
|
||||
util::XdbfGameData module_xdbf(object_ref<UserModule> exec_module) const;
|
||||
|
||||
AchievementManager* achievement_manager() const {
|
||||
return achievement_manager_.get();
|
||||
}
|
||||
xam::AppManager* app_manager() const { return app_manager_.get(); }
|
||||
xam::ContentManager* content_manager() const {
|
||||
return content_manager_.get();
|
||||
|
@ -229,6 +233,7 @@ class KernelState {
|
|||
bool Save(ByteStream* stream);
|
||||
bool Restore(ByteStream* stream);
|
||||
|
||||
uint32_t notification_position_ = 0;
|
||||
private:
|
||||
void LoadKernelModule(object_ref<KernelModule> kernel_module);
|
||||
|
||||
|
@ -240,6 +245,7 @@ class KernelState {
|
|||
std::unique_ptr<xam::AppManager> app_manager_;
|
||||
std::unique_ptr<xam::ContentManager> content_manager_;
|
||||
std::map<uint8_t, std::unique_ptr<xam::UserProfile>> user_profiles_;
|
||||
std::unique_ptr<AchievementManager> achievement_manager_;
|
||||
|
||||
xe::global_critical_region global_critical_region_;
|
||||
|
||||
|
|
|
@ -17,6 +17,11 @@ namespace kernel {
|
|||
namespace xam {
|
||||
namespace apps {
|
||||
|
||||
struct X_XUSER_ACHIEVEMENT {
|
||||
xe::be<uint32_t> user_idx;
|
||||
xe::be<uint32_t> achievement_id;
|
||||
};
|
||||
|
||||
XgiApp::XgiApp(KernelState* kernel_state) : App(kernel_state, 0xFB) {}
|
||||
|
||||
// http://mb.mirage.org/bugzilla/xliveless/main.c
|
||||
|
@ -55,6 +60,13 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
|||
uint32_t achievements_ptr = xe::load_and_swap<uint32_t>(buffer + 4);
|
||||
XELOGD("XGIUserWriteAchievements({:08X}, {:08X})", achievement_count,
|
||||
achievements_ptr);
|
||||
|
||||
auto* achievement =
|
||||
(X_XUSER_ACHIEVEMENT*)memory_->TranslateVirtual(achievements_ptr);
|
||||
for (uint32_t i = 0; i < achievement_count; i++, achievement++) {
|
||||
kernel_state_->achievement_manager()->EarnAchievement(
|
||||
achievement->user_idx, 0, achievement->achievement_id);
|
||||
}
|
||||
return X_E_SUCCESS;
|
||||
}
|
||||
case 0x000B0010: {
|
||||
|
|
|
@ -98,6 +98,7 @@ dword_result_t XNotifyDelayUI_entry(dword_t delay_ms) {
|
|||
DECLARE_XAM_EXPORT1(XNotifyDelayUI, kNone, kStub);
|
||||
|
||||
void XNotifyPositionUI_entry(dword_t position) {
|
||||
kernel_state()->notification_position_ = position;
|
||||
// Ignored.
|
||||
}
|
||||
DECLARE_XAM_EXPORT1(XNotifyPositionUI, kNone, kStub);
|
||||
|
|
|
@ -18,9 +18,23 @@
|
|||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/math.h"
|
||||
#include "xenia/ui/imgui_dialog.h"
|
||||
#include "xenia/ui/imgui_notification.h"
|
||||
#include "xenia/ui/resources.h"
|
||||
#include "xenia/ui/ui_event.h"
|
||||
#include "xenia/ui/window.h"
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "third_party/stb/stb_image.h"
|
||||
|
||||
#if XE_PLATFORM_WIN32
|
||||
#include <ShlObj_core.h>
|
||||
#endif
|
||||
|
||||
DEFINE_path(
|
||||
custom_font_path, "",
|
||||
"Allows user to load custom font and use it instead of default one.",
|
||||
"General");
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
|
@ -89,7 +103,32 @@ void ImGuiDrawer::RemoveDialog(ImGuiDialog* dialog) {
|
|||
}
|
||||
}
|
||||
dialogs_.erase(it);
|
||||
DetachIfLastDialogRemoved();
|
||||
DetachIfLastWindowRemoved();
|
||||
}
|
||||
|
||||
void ImGuiDrawer::AddNotification(ImGuiNotification* dialog) {
|
||||
assert_not_null(dialog);
|
||||
// Check if already added.
|
||||
if (std::find(notifications_.cbegin(), notifications_.cend(), dialog) !=
|
||||
notifications_.cend()) {
|
||||
return;
|
||||
}
|
||||
if (notifications_.empty()) {
|
||||
if (presenter_) {
|
||||
presenter_->AddUIDrawerFromUIThread(this, z_order_);
|
||||
}
|
||||
}
|
||||
notifications_.push_back(dialog);
|
||||
}
|
||||
|
||||
void ImGuiDrawer::RemoveNotification(ImGuiNotification* dialog) {
|
||||
assert_not_null(dialog);
|
||||
auto it = std::find(notifications_.cbegin(), notifications_.cend(), dialog);
|
||||
if (it == notifications_.cend()) {
|
||||
return;
|
||||
}
|
||||
notifications_.erase(it);
|
||||
DetachIfLastWindowRemoved();
|
||||
}
|
||||
|
||||
void ImGuiDrawer::Initialize() {
|
||||
|
@ -98,54 +137,7 @@ void ImGuiDrawer::Initialize() {
|
|||
internal_state_ = ImGui::CreateContext();
|
||||
ImGui::SetCurrentContext(internal_state_);
|
||||
|
||||
auto& io = ImGui::GetIO();
|
||||
|
||||
// TODO(gibbed): disable imgui.ini saving for now,
|
||||
// imgui assumes paths are char* so we can't throw a good path at it on
|
||||
// Windows.
|
||||
io.IniFilename = nullptr;
|
||||
|
||||
// Setup the font glyphs.
|
||||
ImFontConfig font_config;
|
||||
font_config.OversampleH = font_config.OversampleV = 1;
|
||||
font_config.PixelSnapH = true;
|
||||
|
||||
// https://jrgraphix.net/r/Unicode/
|
||||
static const ImWchar font_glyph_ranges[] = {
|
||||
0x0020, 0x00FF, // Basic Latin + Latin Supplement
|
||||
0x2000, 0x206F, // General Punctuation
|
||||
0,
|
||||
};
|
||||
io.Fonts->AddFontFromMemoryCompressedBase85TTF(
|
||||
kProggyTinyCompressedDataBase85, 10.0f, &font_config,
|
||||
io.Fonts->GetGlyphRangesDefault());
|
||||
|
||||
font_config.MergeMode = true;
|
||||
|
||||
const char* alt_font = "C:\\Windows\\Fonts\\segoeui.ttf";
|
||||
if (std::filesystem::exists(alt_font)) {
|
||||
io.Fonts->AddFontFromFileTTF(alt_font, 16.0f, &font_config,
|
||||
font_glyph_ranges);
|
||||
} else {
|
||||
XELOGW(
|
||||
"Unable to load Segoe UI; General Punctuation characters will be "
|
||||
"boxes");
|
||||
}
|
||||
|
||||
// TODO(benvanik): jp font on other platforms?
|
||||
// https://github.com/Koruri/kibitaki looks really good, but is 1.5MiB.
|
||||
const char* jp_font_path = "C:\\Windows\\Fonts\\msgothic.ttc";
|
||||
if (std::filesystem::exists(jp_font_path)) {
|
||||
ImFontConfig jp_font_config;
|
||||
jp_font_config.MergeMode = true;
|
||||
jp_font_config.OversampleH = jp_font_config.OversampleV = 1;
|
||||
jp_font_config.PixelSnapH = true;
|
||||
jp_font_config.FontNo = 0;
|
||||
io.Fonts->AddFontFromFileTTF(jp_font_path, 12.0f, &jp_font_config,
|
||||
io.Fonts->GetGlyphRangesJapanese());
|
||||
} else {
|
||||
XELOGW("Unable to load Japanese font; JP characters will be boxes");
|
||||
}
|
||||
InitializeFonts();
|
||||
|
||||
auto& style = ImGui::GetStyle();
|
||||
style.ScrollbarRounding = 0;
|
||||
|
@ -234,6 +226,101 @@ std::optional<ImGuiKey> ImGuiDrawer::VirtualKeyToImGuiKey(VirtualKey vkey) {
|
|||
}
|
||||
}
|
||||
|
||||
void ImGuiDrawer::SetupNotificationTextures() {
|
||||
if (!immediate_drawer_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGuiIO& io = GetIO();
|
||||
|
||||
// We're including 4th to include all visible
|
||||
for (uint8_t i = 0; i <= 4; i++) {
|
||||
if (notification_icons.size() < i) {
|
||||
break;
|
||||
}
|
||||
|
||||
unsigned char* image_data;
|
||||
int width, height, channels;
|
||||
const auto user_icon = notification_icons.at(i);
|
||||
image_data =
|
||||
stbi_load_from_memory(user_icon.first, user_icon.second, &width,
|
||||
&height, &channels, STBI_rgb_alpha);
|
||||
notification_icon_textures_.push_back(immediate_drawer_->CreateTexture(
|
||||
width, height, ImmediateTextureFilter::kLinear, true,
|
||||
reinterpret_cast<uint8_t*>(image_data)));
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiDrawer::InitializeFonts() {
|
||||
auto& io = ImGui::GetIO();
|
||||
|
||||
const float default_font_size = 12.0f;
|
||||
// TODO(gibbed): disable imgui.ini saving for now,
|
||||
// imgui assumes paths are char* so we can't throw a good path at it on
|
||||
// Windows.
|
||||
io.IniFilename = nullptr;
|
||||
|
||||
// Setup the font glyphs.
|
||||
ImFontConfig font_config;
|
||||
font_config.OversampleH = font_config.OversampleV = 2;
|
||||
font_config.PixelSnapH = true;
|
||||
|
||||
// https://jrgraphix.net/r/Unicode/
|
||||
static const ImWchar font_glyph_ranges[] = {
|
||||
0x0020, 0x00FF, // Basic Latin + Latin Supplement
|
||||
0x0370, 0x03FF, // Greek
|
||||
0x0400, 0x044F, // Cyrillic
|
||||
0x2000, 0x206F, // General Punctuation
|
||||
0,
|
||||
};
|
||||
|
||||
if (!cvars::custom_font_path.empty() &&
|
||||
std::filesystem::exists(cvars::custom_font_path)) {
|
||||
const std::string font_path = xe::path_to_utf8(cvars::custom_font_path);
|
||||
ImFont* font = io.Fonts->AddFontFromFileTTF(
|
||||
font_path.c_str(), default_font_size, &font_config, font_glyph_ranges);
|
||||
|
||||
io.Fonts->Build();
|
||||
// Something went wrong while loading custom font. Probably corrupted.
|
||||
if (!font->IsLoaded()) {
|
||||
XELOGE("Failed to load custom font: {}", font_path);
|
||||
io.Fonts->Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (io.Fonts->Fonts.empty()) {
|
||||
io.Fonts->AddFontFromMemoryCompressedBase85TTF(
|
||||
kProggyTinyCompressedDataBase85, default_font_size, &font_config,
|
||||
io.Fonts->GetGlyphRangesDefault());
|
||||
}
|
||||
|
||||
// TODO(benvanik): jp font on other platforms?
|
||||
#if XE_PLATFORM_WIN32
|
||||
PWSTR fonts_dir;
|
||||
HRESULT result = SHGetKnownFolderPath(FOLDERID_Fonts, 0, NULL, &fonts_dir);
|
||||
if (FAILED(result)) {
|
||||
XELOGW("Unable to find Windows fonts directory");
|
||||
return;
|
||||
}
|
||||
|
||||
std::filesystem::path jp_font_path = std::wstring(fonts_dir);
|
||||
jp_font_path.append("msgothic.ttc");
|
||||
if (std::filesystem::exists(jp_font_path)) {
|
||||
ImFontConfig jp_font_config;
|
||||
jp_font_config.MergeMode = true;
|
||||
jp_font_config.OversampleH = jp_font_config.OversampleV = 2;
|
||||
jp_font_config.PixelSnapH = true;
|
||||
jp_font_config.FontNo = 0;
|
||||
io.Fonts->AddFontFromFileTTF(xe::path_to_utf8(jp_font_path).c_str(),
|
||||
default_font_size, &jp_font_config,
|
||||
io.Fonts->GetGlyphRangesJapanese());
|
||||
} else {
|
||||
XELOGW("Unable to load Japanese font; JP characters will be boxes");
|
||||
}
|
||||
CoTaskMemFree(static_cast<void*>(fonts_dir));
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImGuiDrawer::SetupFontTexture() {
|
||||
if (font_texture_ || !immediate_drawer_) {
|
||||
return;
|
||||
|
@ -273,10 +360,13 @@ void ImGuiDrawer::SetImmediateDrawer(ImmediateDrawer* new_immediate_drawer) {
|
|||
if (immediate_drawer_) {
|
||||
GetIO().Fonts->TexID = static_cast<ImTextureID>(nullptr);
|
||||
font_texture_.reset();
|
||||
|
||||
notification_icon_textures_.clear();
|
||||
}
|
||||
immediate_drawer_ = new_immediate_drawer;
|
||||
if (immediate_drawer_) {
|
||||
SetupFontTexture();
|
||||
SetupNotificationTextures();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -289,7 +379,7 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (dialogs_.empty()) {
|
||||
if (dialogs_.empty() && notifications_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -323,6 +413,11 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) {
|
|||
}
|
||||
dialog_loop_next_index_ = SIZE_MAX;
|
||||
|
||||
if (!notifications_.empty()) {
|
||||
// We only care about drawing next notification.
|
||||
notifications_.at(0)->Draw();
|
||||
}
|
||||
|
||||
ImGui::Render();
|
||||
ImDrawData* draw_data = ImGui::GetDrawData();
|
||||
if (draw_data) {
|
||||
|
@ -336,9 +431,9 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) {
|
|||
|
||||
// Detaching is deferred if the last dialog is removed during drawing, perform
|
||||
// it now if needed.
|
||||
DetachIfLastDialogRemoved();
|
||||
DetachIfLastWindowRemoved();
|
||||
|
||||
if (!dialogs_.empty()) {
|
||||
if (!dialogs_.empty() || !notifications_.empty()) {
|
||||
// Repaint (and handle input) continuously if still active.
|
||||
presenter_->RequestUIPaintFromUIThread();
|
||||
}
|
||||
|
@ -571,12 +666,12 @@ void ImGuiDrawer::SwitchToPhysicalMouseAndUpdateMousePosition(
|
|||
UpdateMousePosition(float(e.x()), float(e.y()));
|
||||
}
|
||||
|
||||
void ImGuiDrawer::DetachIfLastDialogRemoved() {
|
||||
void ImGuiDrawer::DetachIfLastWindowRemoved() {
|
||||
// IsDrawingDialogs() is also checked because in a situation of removing the
|
||||
// only dialog, then adding a dialog, from within a dialog's Draw function,
|
||||
// re-registering the ImGuiDrawer may result in ImGui being drawn multiple
|
||||
// times in the current frame.
|
||||
if (!dialogs_.empty() || IsDrawingDialogs()) {
|
||||
if (!dialogs_.empty() || !notifications_.empty() || IsDrawingDialogs()) {
|
||||
return;
|
||||
}
|
||||
if (presenter_) {
|
||||
|
|
|
@ -29,6 +29,7 @@ namespace xe {
|
|||
namespace ui {
|
||||
|
||||
class ImGuiDialog;
|
||||
class ImGuiNotification;
|
||||
class Window;
|
||||
|
||||
class ImGuiDrawer : public WindowInputListener, public UIDrawer {
|
||||
|
@ -41,6 +42,9 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
|
|||
void AddDialog(ImGuiDialog* dialog);
|
||||
void RemoveDialog(ImGuiDialog* dialog);
|
||||
|
||||
void AddNotification(ImGuiNotification* notification);
|
||||
void RemoveNotification(ImGuiNotification* notification);
|
||||
|
||||
// SetPresenter may be called from the destructor.
|
||||
void SetPresenter(Presenter* new_presenter);
|
||||
void SetImmediateDrawer(ImmediateDrawer* new_immediate_drawer);
|
||||
|
@ -54,6 +58,13 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
|
|||
|
||||
void ClearDialogs();
|
||||
|
||||
ImmediateTexture* GetNotificationIcon(uint8_t user_index) {
|
||||
if (user_index >= notification_icon_textures_.size()) {
|
||||
user_index = 0;
|
||||
}
|
||||
return notification_icon_textures_.at(user_index).get();
|
||||
}
|
||||
|
||||
protected:
|
||||
void OnKeyDown(KeyEvent& e) override;
|
||||
void OnKeyUp(KeyEvent& e) override;
|
||||
|
@ -67,7 +78,9 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
|
|||
|
||||
private:
|
||||
void Initialize();
|
||||
void InitializeFonts();
|
||||
|
||||
void SetupNotificationTextures();
|
||||
void SetupFontTexture();
|
||||
|
||||
void RenderDrawLists(ImDrawData* data, UIDrawContext& ui_draw_context);
|
||||
|
@ -78,7 +91,7 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
|
|||
void SwitchToPhysicalMouseAndUpdateMousePosition(const MouseEvent& e);
|
||||
|
||||
bool IsDrawingDialogs() const { return dialog_loop_next_index_ != SIZE_MAX; }
|
||||
void DetachIfLastDialogRemoved();
|
||||
void DetachIfLastWindowRemoved();
|
||||
|
||||
std::optional<ImGuiKey> VirtualKeyToImGuiKey(VirtualKey vkey);
|
||||
|
||||
|
@ -89,6 +102,9 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
|
|||
|
||||
// All currently-attached dialogs that get drawn.
|
||||
std::vector<ImGuiDialog*> dialogs_;
|
||||
|
||||
// All queued notifications. Notification at index 0 is currently presented one.
|
||||
std::vector<ImGuiNotification*> notifications_;
|
||||
// Using an index, not an iterator, because after the erasure, the adjustment
|
||||
// must be done for the vector element indices that would be in the iterator
|
||||
// range that would be invalidated.
|
||||
|
@ -102,6 +118,7 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
|
|||
// detaching the presenter.
|
||||
std::unique_ptr<ImmediateTexture> font_texture_;
|
||||
|
||||
std::vector<std::unique_ptr<ImmediateTexture>> notification_icon_textures_;
|
||||
// If there's an active pointer, the ImGui mouse is controlled by this touch.
|
||||
// If it's TouchEvent::kPointerIDNone, the ImGui mouse is controlled by the
|
||||
// mouse.
|
||||
|
|
|
@ -0,0 +1,246 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2023 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/platform.h"
|
||||
#include "xenia/ui/imgui_notification.h"
|
||||
|
||||
#if XE_PLATFORM_WIN32
|
||||
#include <playsoundapi.h>
|
||||
#endif
|
||||
|
||||
DEFINE_string(notification_sound_path, "",
|
||||
"Path (including filename) to selected notification sound. Sound "
|
||||
"MUST be in wav format!",
|
||||
"General");
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
const NotificationAlignment GetNotificationAlignment(
|
||||
const uint8_t notification_position_id) {
|
||||
NotificationAlignment alignment = NotificationAlignment::kAlignUnknown;
|
||||
|
||||
if (notification_position_id >=
|
||||
notification_position_id_screen_offset.size()) {
|
||||
return alignment;
|
||||
}
|
||||
|
||||
const ImVec2 screen_offset =
|
||||
notification_position_id_screen_offset.at(notification_position_id);
|
||||
|
||||
if (screen_offset.x < 0.3f) {
|
||||
alignment = NotificationAlignment::kAlignLeft;
|
||||
} else if (screen_offset.x > 0.7f) {
|
||||
alignment = NotificationAlignment::kAlignRight;
|
||||
} else {
|
||||
alignment = NotificationAlignment::kAlignCenter;
|
||||
}
|
||||
|
||||
return alignment;
|
||||
}
|
||||
|
||||
const ImVec2 CalculateNotificationScreenPosition(
|
||||
ImVec2 screen_size, ImVec2 window_size, uint8_t notification_position_id) {
|
||||
ImVec2 result = {NAN, NAN};
|
||||
|
||||
if (window_size.x >= screen_size.x || window_size.y >= screen_size.y) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const NotificationAlignment alignment =
|
||||
GetNotificationAlignment(notification_position_id);
|
||||
|
||||
if (alignment == NotificationAlignment::kAlignUnknown) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const ImVec2 screen_offset =
|
||||
notification_position_id_screen_offset.at(notification_position_id);
|
||||
|
||||
switch (alignment) {
|
||||
case NotificationAlignment::kAlignLeft:
|
||||
result.x = std::roundf(screen_size.x * screen_offset.x);
|
||||
break;
|
||||
|
||||
case NotificationAlignment::kAlignRight:
|
||||
result.x = std::roundf((screen_size.x * screen_offset.x) - window_size.x);
|
||||
break;
|
||||
|
||||
case NotificationAlignment::kAlignCenter:
|
||||
result.x = std::roundf((screen_size.x * 0.5f) - (window_size.x * 0.5f));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
result.y = std::roundf(screen_size.y * screen_offset.y);
|
||||
return result;
|
||||
}
|
||||
|
||||
const ImVec2 CalculateNotificationSize(ImVec2 text_size, float scale) {
|
||||
const ImVec2 result =
|
||||
ImVec2(std::floorf((default_notification_icon_size.x +
|
||||
default_notification_margin_size.x) *
|
||||
scale) +
|
||||
text_size.x,
|
||||
std::floorf((default_notification_icon_size.y +
|
||||
default_notification_margin_size.y) *
|
||||
scale));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ImGuiNotification::ImGuiNotification(ui::ImGuiDrawer* imgui_drawer,
|
||||
std::string title, std::string description,
|
||||
uint8_t user_index, uint8_t position_id)
|
||||
: imgui_drawer_(imgui_drawer),
|
||||
title_(title),
|
||||
description_(description),
|
||||
user_index_(user_index),
|
||||
position_(position_id),
|
||||
creation_time_(0),
|
||||
current_stage_(NotificationStage::kAwaiting),
|
||||
notification_draw_progress_(0.0f) {
|
||||
imgui_drawer->AddNotification(this);
|
||||
}
|
||||
|
||||
ImGuiNotification::~ImGuiNotification() {
|
||||
imgui_drawer_->RemoveNotification(this);
|
||||
}
|
||||
|
||||
void ImGuiNotification::Draw() { OnDraw(imgui_drawer_->GetIO()); }
|
||||
|
||||
void ImGuiNotification::UpdateNotificationState() {
|
||||
switch (current_stage_) {
|
||||
case NotificationStage::kAwaiting:
|
||||
// TODO(Gliniak): Implement delayed notifications.
|
||||
current_stage_ = NotificationStage::kFazeIn;
|
||||
notification_draw_progress_ = 0.2f;
|
||||
#if XE_PLATFORM_WIN32
|
||||
if (!cvars::notification_sound_path.empty()) {
|
||||
auto notification_sound_path = cvars::notification_sound_path;
|
||||
if (std::filesystem::exists(notification_sound_path)) {
|
||||
PlaySound(std::wstring(notification_sound_path.begin(),
|
||||
notification_sound_path.end())
|
||||
.c_str(),
|
||||
NULL,
|
||||
SND_FILENAME | SND_NODEFAULT | SND_NOSTOP | SND_ASYNC);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
break;
|
||||
case NotificationStage::kFazeIn: {
|
||||
creation_time_ = Clock::QueryHostUptimeMillis();
|
||||
if (notification_draw_progress_ < 1.1f) {
|
||||
notification_draw_progress_ += 0.02f;
|
||||
}
|
||||
|
||||
// Mimics a bit original console behaviour when it makes window a bit
|
||||
// longer for few frames then decreases size
|
||||
if (notification_draw_progress_ >= 1.1f) {
|
||||
current_stage_ = NotificationStage::kPresent;
|
||||
notification_draw_progress_ = 1.0f;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NotificationStage::kPresent:
|
||||
if (IsNotificationClosingTime()) {
|
||||
current_stage_ = NotificationStage::kFazeOut;
|
||||
}
|
||||
break;
|
||||
case NotificationStage::kFazeOut: {
|
||||
if (notification_draw_progress_ > 0.2f) {
|
||||
notification_draw_progress_ -= 0.02f;
|
||||
} else {
|
||||
current_stage_ = NotificationStage::kFinished;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AchievementNotificationWindow::OnDraw(ImGuiIO& io) {
|
||||
UpdateNotificationState();
|
||||
|
||||
if (IsNotificationExpired()) {
|
||||
delete this;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string longest_notification_text_line =
|
||||
GetTitle().size() > GetDescription().size() ? GetTitle().c_str()
|
||||
: GetDescription().c_str();
|
||||
|
||||
const ImVec2 screen_size = io.DisplaySize;
|
||||
const float scale = std::fminf(screen_size.x / default_drawing_resolution.x,
|
||||
screen_size.y / default_drawing_resolution.y);
|
||||
const ImVec2 text_size = io.Fonts->Fonts[0]->CalcTextSizeA(
|
||||
io.Fonts->Fonts[0]->FontSize * default_notification_text_scale * scale,
|
||||
FLT_MAX, -1.0f, longest_notification_text_line.c_str());
|
||||
|
||||
const ImVec2 final_notification_size =
|
||||
CalculateNotificationSize(text_size, scale);
|
||||
|
||||
const ImVec2 notification_position = CalculateNotificationScreenPosition(
|
||||
screen_size, final_notification_size, GetPositionId());
|
||||
|
||||
if (isnan(notification_position.x) || isnan(notification_position.y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImVec2 current_notification_size = final_notification_size;
|
||||
current_notification_size.x *= notification_draw_progress_;
|
||||
current_notification_size.x = std::floorf(current_notification_size.x);
|
||||
|
||||
// Initialize position and window size
|
||||
ImGui::SetNextWindowSize(current_notification_size);
|
||||
ImGui::SetNextWindowPos(notification_position);
|
||||
|
||||
// Set new window style before drawing window
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 30.f * scale);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg,
|
||||
default_notification_background_color);
|
||||
ImGui::PushStyleColor(ImGuiCol_Separator,
|
||||
default_notification_background_color);
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, default_notification_border_color);
|
||||
|
||||
ImGui::Begin("Notification Window", NULL, NOTIFY_TOAST_FLAGS);
|
||||
{
|
||||
ImGui::SetWindowFontScale(default_notification_text_scale * scale);
|
||||
// Set offset to image to prevent it from being right on border.
|
||||
ImGui::SetCursorPos(ImVec2(final_notification_size.x * 0.005f,
|
||||
final_notification_size.y * 0.05f));
|
||||
// Elements of window
|
||||
ImGui::Image(reinterpret_cast<ImTextureID>(
|
||||
GetDrawer()->GetNotificationIcon(GetUserIndex())),
|
||||
ImVec2(default_notification_icon_size.x * scale,
|
||||
default_notification_icon_size.y * scale));
|
||||
|
||||
ImGui::SameLine();
|
||||
if (notification_draw_progress_ > 0.5f) {
|
||||
ImGui::TextColored(white_color, GetNotificationText().c_str());
|
||||
}
|
||||
}
|
||||
// Restore previous style
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2023 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_UI_IMGUI_NOTIFICATION_H_
|
||||
#define XENIA_UI_IMGUI_NOTIFICATION_H_
|
||||
|
||||
#include "third_party/imgui/imgui.h"
|
||||
#include "xenia/ui/imgui_dialog.h"
|
||||
#include "xenia/ui/imgui_drawer.h"
|
||||
|
||||
#define NOTIFY_TOAST_FLAGS \
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | \
|
||||
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav | \
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus | \
|
||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoResize
|
||||
|
||||
// Parameters based on 1280x720 resolution
|
||||
constexpr ImVec2 default_drawing_resolution = ImVec2(1280.f, 720.f);
|
||||
|
||||
constexpr ImVec2 default_notification_icon_size = ImVec2(58.0f, 58.0f);
|
||||
constexpr ImVec2 default_notification_margin_size = ImVec2(50.f, 5.f);
|
||||
constexpr float default_notification_text_scale = 2.3f;
|
||||
|
||||
constexpr ImVec4 white_color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
constexpr ImVec4 default_notification_background_color =
|
||||
ImVec4(0.215f, 0.215f, 0.215f, 1.0f);
|
||||
constexpr ImVec4 default_notification_border_color = white_color;
|
||||
|
||||
enum class NotificationAlignment : uint8_t {
|
||||
kAlignLeft = 0,
|
||||
kAlignRight = 1,
|
||||
kAlignCenter = 2,
|
||||
kAlignUnknown = 0xFF
|
||||
};
|
||||
|
||||
const static std::vector<ImVec2> notification_position_id_screen_offset = {
|
||||
{0.50f, 0.45f}, // CENTER-CENTER - 0
|
||||
{0.50f, 0.10f}, // CENTER-TOP - 1
|
||||
{0.50f, 0.80f}, // CENTER-BOTTOM - 2
|
||||
{NAN, NAN}, // NOT EXIST - 3
|
||||
{0.07f, 0.45f}, // LEFT-CENTER - 4
|
||||
{0.07f, 0.10f}, // LEFT-TOP - 5
|
||||
{0.07f, 0.80f}, // LEFT-BOTTOM - 6
|
||||
{NAN, NAN}, // NOT EXIST - 7
|
||||
{0.93f, 0.45f}, // RIGHT-CENTER - 8
|
||||
{0.93f, 0.10f}, // RIGHT-TOP - 9
|
||||
{0.93f, 0.80f} // RIGHT-BOTTOM - 10
|
||||
};
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
class ImGuiNotification {
|
||||
public:
|
||||
ImGuiNotification(ui::ImGuiDrawer* imgui_drawer, std::string title,
|
||||
std::string description, uint8_t user_index,
|
||||
uint8_t position_id = 0);
|
||||
|
||||
~ImGuiNotification();
|
||||
|
||||
void Draw();
|
||||
|
||||
protected:
|
||||
enum class NotificationStage : uint8_t {
|
||||
kAwaiting = 0,
|
||||
kFazeIn = 1,
|
||||
kPresent = 2,
|
||||
kFazeOut = 3,
|
||||
kFinished = 4
|
||||
};
|
||||
|
||||
ImGuiDrawer* GetDrawer() { return imgui_drawer_; }
|
||||
|
||||
const bool IsNotificationClosingTime() {
|
||||
return Clock::QueryHostUptimeMillis() - creation_time_ > time_to_close_;
|
||||
}
|
||||
|
||||
const bool IsNotificationExpired() {
|
||||
return current_stage_ == NotificationStage::kFinished;
|
||||
}
|
||||
|
||||
const std::string GetNotificationText() {
|
||||
std::string text = title_;
|
||||
|
||||
if (!description_.empty()) {
|
||||
text.append("\n" + description_);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
const std::string GetTitle() { return title_; }
|
||||
const std::string GetDescription() { return description_; }
|
||||
|
||||
const uint8_t GetPositionId() { return position_; }
|
||||
const uint8_t GetUserIndex() { return user_index_; }
|
||||
|
||||
void UpdateNotificationState();
|
||||
|
||||
virtual void OnDraw(ImGuiIO& io) {}
|
||||
|
||||
float notification_draw_progress_;
|
||||
|
||||
private:
|
||||
NotificationStage current_stage_;
|
||||
uint8_t position_;
|
||||
uint8_t user_index_;
|
||||
|
||||
uint32_t delay_ = 0;
|
||||
uint32_t time_to_close_ = 4500;
|
||||
|
||||
uint64_t creation_time_;
|
||||
|
||||
std::string title_;
|
||||
std::string description_;
|
||||
|
||||
ImGuiDrawer* imgui_drawer_ = nullptr;
|
||||
};
|
||||
|
||||
class AchievementNotificationWindow final : ImGuiNotification {
|
||||
public:
|
||||
AchievementNotificationWindow(ui::ImGuiDrawer* imgui_drawer,
|
||||
std::string title, std::string description,
|
||||
uint8_t user_index, uint8_t position_id = 0)
|
||||
: ImGuiNotification(imgui_drawer, title, description, user_index,
|
||||
position_id){};
|
||||
|
||||
void OnDraw(ImGuiIO& io) override;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -27,4 +27,5 @@ project("xenia-ui")
|
|||
links({
|
||||
"dwmapi",
|
||||
"dxgi",
|
||||
"winmm",
|
||||
})
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue