From 5a21a6c8dfa0bda3e9641ad29e5b567985a8dc8c Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 27 May 2026 18:00:57 +1000 Subject: [PATCH] Host: Add icon to ConfirmMessageAsync() And use it for achievements. --- src/core/achievements.cpp | 2 +- src/core/fullscreenui_widgets.cpp | 38 ++++++++------ src/core/fullscreenui_widgets.h | 9 ++-- src/core/host.h | 5 +- src/core/system.cpp | 2 +- src/duckstation-qt/qthost.cpp | 33 ++++++++---- src/duckstation-qt/svgwidget.cpp | 65 +++++++++++++++++++++++- src/duckstation-qt/svgwidget.h | 4 ++ src/duckstation-regtest/regtest_host.cpp | 5 +- 9 files changed, 127 insertions(+), 36 deletions(-) diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index bdddb2625..4da6093f2 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -2396,7 +2396,7 @@ void Achievements::Logout() void Achievements::ConfirmHardcoreModeDisableAsync(std::string_view trigger, std::function callback) { Host::ConfirmMessageAsync( - TRANSLATE_STR("Achievements", "Confirm Hardcore Mode Disable"), + RA_LOGO_SVG_ICON_NAME, TRANSLATE_STR("Achievements", "Confirm Hardcore Mode Disable"), fmt::format(TRANSLATE_FS("Achievements", "{0} cannot be performed while hardcore mode is active. Do you want to " "disable hardcore mode? {0} will be cancelled if you select No."), trigger), diff --git a/src/core/fullscreenui_widgets.cpp b/src/core/fullscreenui_widgets.cpp index 8c1cf0876..9f2863572 100644 --- a/src/core/fullscreenui_widgets.cpp +++ b/src/core/fullscreenui_widgets.cpp @@ -207,7 +207,7 @@ public: MessageDialog(); ~MessageDialog(); - void Open(std::string_view icon, std::string_view title, std::string message, CallbackVariant callback, + void Open(std::string icon, std::string_view title, std::string message, CallbackVariant callback, std::string first_button_text, std::string second_button_text, std::string third_button_text); void ClearState(); @@ -350,7 +350,7 @@ private: std::string_view no_text = {}) override; protected: - static std::string_view GetIconString(PromptIcon icon); + static std::string GetIconString(PromptIcon icon); void StateChanged(StateChange changed) override; }; @@ -5438,12 +5438,12 @@ FullscreenUI::MessageDialog::MessageDialog() = default; FullscreenUI::MessageDialog::~MessageDialog() = default; -void FullscreenUI::MessageDialog::Open(std::string_view icon, std::string_view title, std::string message, +void FullscreenUI::MessageDialog::Open(std::string icon, std::string_view title, std::string message, CallbackVariant callback, std::string first_button_text, std::string second_button_text, std::string third_button_text) { SetTitleAndOpen(fmt::format("{}##message_dialog", title)); - m_icon = icon; + m_icon = std::move(icon); m_message = std::move(message); m_callback = std::move(callback); m_buttons[0] = std::move(first_button_text); @@ -5476,9 +5476,17 @@ void FullscreenUI::MessageDialog::Draw() if (!m_icon.empty()) { - ImGui::PushFont(nullptr, LayoutScale(50.0f), 0.0f); - ImGui::TextUnformatted(IMSTR_START_END(m_icon)); - ImGui::PopFont(); + const ImVec2 icon_size = LayoutScale(52.0f, 52.0f); + if (Path::IsAbsolute(m_icon)) + { + ImGui::Image(GetCachedTexture(m_icon, icon_size), icon_size); + } + else + { + ImGui::PushFont(nullptr, LayoutScale(52.0f), 0.0f); + ImGui::TextUnformatted(IMSTR_START_END(m_icon)); + ImGui::PopFont(); + } ImGui::SameLine(); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + LayoutScale(10.0f)); } @@ -5553,26 +5561,26 @@ bool FullscreenUI::IsMessageBoxDialogOpen() return s_state.message_dialog.IsOpen(); } -void FullscreenUI::OpenConfirmMessageDialog(std::string_view icon, std::string_view title, std::string message, +void FullscreenUI::OpenConfirmMessageDialog(std::string icon, std::string_view title, std::string message, ConfirmMessageDialogCallback callback, std::string yes_button_text, std::string no_button_text) { - s_state.message_dialog.Open(icon, std::move(title), std::move(message), std::move(callback), + s_state.message_dialog.Open(std::move(icon), std::move(title), std::move(message), std::move(callback), std::move(yes_button_text), std::move(no_button_text), std::string()); } -void FullscreenUI::OpenInfoMessageDialog(std::string_view icon, std::string_view title, std::string message, +void FullscreenUI::OpenInfoMessageDialog(std::string icon, std::string_view title, std::string message, InfoMessageDialogCallback callback, std::string button_text) { - s_state.message_dialog.Open(icon, std::move(title), std::move(message), std::move(callback), std::move(button_text), - std::string(), std::string()); + s_state.message_dialog.Open(std::move(icon), std::move(title), std::move(message), std::move(callback), + std::move(button_text), std::string(), std::string()); } -void FullscreenUI::OpenMessageDialog(std::string_view icon, std::string_view title, std::string message, +void FullscreenUI::OpenMessageDialog(std::string icon, std::string_view title, std::string message, MessageDialogCallback callback, std::string first_button_text, std::string second_button_text, std::string third_button_text) { - s_state.message_dialog.Open(icon, std::move(title), std::move(message), std::move(callback), + s_state.message_dialog.Open(std::move(icon), std::move(title), std::move(message), std::move(callback), std::move(first_button_text), std::move(second_button_text), std::move(third_button_text)); } @@ -5744,7 +5752,7 @@ bool FullscreenUI::ProgressDialog::ProgressCallbackImpl::IsCancelled() const return s_state.progress_dialog.m_cancelled.load(std::memory_order_acquire); } -std::string_view FullscreenUI::ProgressDialog::ProgressCallbackImpl::GetIconString(PromptIcon icon) +std::string FullscreenUI::ProgressDialog::ProgressCallbackImpl::GetIconString(PromptIcon icon) { switch (icon) { diff --git a/src/core/fullscreenui_widgets.h b/src/core/fullscreenui_widgets.h index eff3f5639..f5e13fe23 100644 --- a/src/core/fullscreenui_widgets.h +++ b/src/core/fullscreenui_widgets.h @@ -566,16 +566,15 @@ using ConfirmMessageDialogCallback = std::function; using InfoMessageDialogCallback = std::function; using MessageDialogCallback = std::function; bool IsMessageBoxDialogOpen(); -void OpenConfirmMessageDialog(std::string_view icon, std::string_view title, std::string message, +void OpenConfirmMessageDialog(std::string icon, std::string_view title, std::string message, ConfirmMessageDialogCallback callback, std::string yes_button_text = FSUI_ICONSTR(ICON_FA_CHECK, "Yes"), std::string no_button_text = FSUI_ICONSTR(ICON_FA_XMARK, "No")); -void OpenInfoMessageDialog(std::string_view icon, std::string_view title, std::string message, +void OpenInfoMessageDialog(std::string icon, std::string_view title, std::string message, InfoMessageDialogCallback callback = {}, std::string button_text = FSUI_ICONSTR(ICON_FA_SQUARE_XMARK, "Close")); -void OpenMessageDialog(std::string_view icon, std::string_view title, std::string message, - MessageDialogCallback callback, std::string first_button_text, std::string second_button_text, - std::string third_button_text); +void OpenMessageDialog(std::string icon, std::string_view title, std::string message, MessageDialogCallback callback, + std::string first_button_text, std::string second_button_text, std::string third_button_text); void CloseMessageDialog(); std::unique_ptr OpenModalProgressDialog(std::string title, diff --git a/src/core/host.h b/src/core/host.h index fcb9af02c..5fd650807 100644 --- a/src/core/host.h +++ b/src/core/host.h @@ -49,8 +49,9 @@ void ReportStatusMessage(std::string_view message); /// Displays an asynchronous confirmation on the UI thread, but does not block the caller. /// The callback may be executed on a different thread. Use RunOnCoreThread() in the callback to ensure safety. using ConfirmMessageAsyncCallback = std::function; -void ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback, - std::string_view yes_text = std::string_view(), std::string_view no_text = std::string_view()); +void ConfirmMessageAsync(std::string_view icon, std::string_view title, std::string_view message, + ConfirmMessageAsyncCallback callback, std::string_view yes_text = std::string_view(), + std::string_view no_text = std::string_view()); /// Opens a URL, using the default application. void OpenURL(std::string_view url); diff --git a/src/core/system.cpp b/src/core/system.cpp index 3854e0cac..5ed49ae33 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -1684,7 +1684,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) if (Core::GetBoolSettingValue("CDROM", "AllowBootingWithoutSBIFile", false)) { Host::ConfirmMessageAsync( - "Confirm Unsupported Configuration", + "images/warning.svg", "Confirm Unsupported Configuration", fmt::format(TRANSLATE_FS("System", "You are attempting to run a libcrypt protected game without an SBI file:\n\n{0}: " "{1}\n\nThe game will likely not run properly.\n\nPlease check the README for " diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 01167e669..48919f895 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -11,6 +11,7 @@ #include "qtwindowinfo.h" #include "settingswindow.h" #include "setupwizarddialog.h" +#include "svgwidget.h" #include "core/achievements.h" #include "core/bus.h" @@ -2351,8 +2352,9 @@ void Host::ReportStatusMessage(std::string_view message) emit g_core_thread->statusMessage(QtUtils::StringViewToQString(message)); } -void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback, - std::string_view yes_text, std::string_view no_text) +void Host::ConfirmMessageAsync(std::string_view icon, std::string_view title, std::string_view message, + ConfirmMessageAsyncCallback callback, std::string_view yes_text, + std::string_view no_text) { INFO_LOG("ConfirmMessageAsync({}, {})", title, message); @@ -2365,9 +2367,10 @@ void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, // Ensure it always comes from the CPU thread. if (!g_core_thread->isCurrentThread()) { - Host::RunOnCoreThread([title = std::string(title), message = std::string(message), callback = std::move(callback), - yes_text = std::string(yes_text), no_text = std::string(no_text)]() mutable { - ConfirmMessageAsync(title, message, std::move(callback)); + Host::RunOnCoreThread([title = std::string(title), icon = std::string(icon), message = std::string(message), + callback = std::move(callback), yes_text = std::string(yes_text), + no_text = std::string(no_text)]() mutable { + ConfirmMessageAsync(icon, title, message, std::move(callback), yes_text, no_text); }); return; } @@ -2380,7 +2383,7 @@ void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, // Use FSUI if we're ingame. if (System::IsValid() || g_core_thread->isFullscreenUIStarted()) { - VideoThread::RunOnThread([title = std::string(title), message = std::string(message), + VideoThread::RunOnThread([title = std::string(title), icon = std::string(icon), message = std::string(message), callback = std::move(callback), yes_text = std::string(yes_text), no_text = std::string(no_text), needs_pause]() mutable { // Need to reset run idle state _again_ after displaying. @@ -2396,8 +2399,13 @@ void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, callback(result); }; + if (icon.empty()) + icon = ICON_EMOJI_QUESTION_MARK; + else if (StringUtil::GetUTF8CharacterCount(icon) > 1 && !Path::IsAbsolute(icon)) + icon = QtHost::GetResourcePath(icon, true); + FullscreenUI::Initialize(); - FullscreenUI::OpenConfirmMessageDialog(ICON_EMOJI_QUESTION_MARK, std::move(title), std::move(message), + FullscreenUI::OpenConfirmMessageDialog(std::move(icon), std::move(title), std::move(message), std::move(final_callback), fmt::format(ICON_FA_CHECK " {}", yes_text), fmt::format(ICON_FA_XMARK " {}", no_text)); FullscreenUI::UpdateRunIdleState(); @@ -2405,15 +2413,22 @@ void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, } else { + QString qicon; + if (!icon.empty()) + qicon = Path::IsAbsolute(icon) ? QtUtils::StringViewToQString(icon) : QtHost::GetResourceQPath(icon, true); + // Otherwise, use the desktop UI. - Host::RunOnUIThread([title = QtUtils::StringViewToQString(title), message = QtUtils::StringViewToQString(message), - callback = std::move(callback), yes_text = QtUtils::StringViewToQString(yes_text), + Host::RunOnUIThread([qicon = std::move(qicon), title = QtUtils::StringViewToQString(title), + message = QtUtils::StringViewToQString(message), callback = std::move(callback), + yes_text = QtUtils::StringViewToQString(yes_text), no_text = QtUtils::StringViewToQString(no_text), needs_pause]() mutable { auto lock = g_main_window->pauseAndLockSystem(); QWidget* const dialog_parent = lock.getDialogParent(); QMessageBox* const msgbox = QtUtils::NewMessageBox(dialog_parent, QMessageBox::Question, title, message, QMessageBox::NoButton); + if (!qicon.isEmpty()) + msgbox->setIconPixmap(SVGWidget::renderSVGToPixmap(qicon, QSize(64, 64), msgbox->devicePixelRatio(), QColor())); QPushButton* const yes_button = msgbox->addButton(yes_text, QMessageBox::AcceptRole); msgbox->addButton(no_text, QMessageBox::RejectRole); diff --git a/src/duckstation-qt/svgwidget.cpp b/src/duckstation-qt/svgwidget.cpp index e8d678b97..01f021a62 100644 --- a/src/duckstation-qt/svgwidget.cpp +++ b/src/duckstation-qt/svgwidget.cpp @@ -179,4 +179,67 @@ void SVGWidget::changeEvent(QEvent* event) rasterize(); update(); } -} \ No newline at end of file +} + +QPixmap SVGWidget::renderSVGToPixmap(const QString& resource_path, const QSize& size, qreal device_pixel_ratio, + const QColor& color) +{ + DynamicHeapArray svg_data; + QFile file(resource_path); + if (!file.open(QFile::ReadOnly) || !QtUtils::ReadFileToByteArray(&file, svg_data)) + { + qCritical() << "Failed to open SVG file: " << resource_path; + return {}; + } + + // plutosvg borrows the raw pointer; svg_data must outlive m_document. + plutosvg_document* const document = plutosvg_document_load_from_data( + reinterpret_cast(svg_data.data()), static_cast(svg_data.size()), -1.0f, -1.0f, nullptr, nullptr); + if (!document) + { + qCritical() << "PlutoSVGWidget: failed to parse SVG" << resource_path; + return {}; + } + + // Determine SVG intrinsic size and compute a uniform scale that fits inside physical, + // preserving aspect ratio. + const float svg_w = plutosvg_document_get_width(document); + const float svg_h = plutosvg_document_get_height(document); + + const QSize physical = QtUtils::ApplyDevicePixelRatioToSize(size, device_pixel_ratio); + int render_w = physical.width(); + int render_h = physical.height(); + + if (svg_w > 0.0f && svg_h > 0.0f) + { + // Scale to fit, preserving aspect ratio (letterbox / pillarbox). + const float scale_x = static_cast(physical.width()) / svg_w; + const float scale_y = static_cast(physical.height()) / svg_h; + const float scale = std::min(scale_x, scale_y); + render_w = std::max(1, static_cast(svg_w * scale)); + render_h = std::max(1, static_cast(svg_h * scale)); + } + + // Use white as currentColor so the SVG's own colours are preserved. + // Swap in a palette colour here if tinting is ever desired. + const plutovg_color_t current_color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = 1.0f}; + + plutovg_surface_t* const surface = + plutosvg_document_render_to_surface(document, nullptr, render_w, render_h, ¤t_color, nullptr, nullptr); + if (!surface) + { + qCritical() << "PlutoSVGWidget: render failed for" << resource_path; + plutosvg_document_destroy(document); + return {}; + } + + // plutovg surfaces are premultiplied ARGB (native-endian) == QImage::Format_ARGB32_Premultiplied. + const QImage img(plutovg_surface_get_data(surface), plutovg_surface_get_width(surface), + plutovg_surface_get_height(surface), plutovg_surface_get_stride(surface), + QImage::Format_ARGB32_Premultiplied, CleanupSurface, surface); + + QPixmap pm = QPixmap::fromImage(img); + pm.setDevicePixelRatio(device_pixel_ratio); + plutosvg_document_destroy(document); + return pm; +} diff --git a/src/duckstation-qt/svgwidget.h b/src/duckstation-qt/svgwidget.h index 074591442..33ae68baf 100644 --- a/src/duckstation-qt/svgwidget.h +++ b/src/duckstation-qt/svgwidget.h @@ -40,6 +40,10 @@ public: /// Returns the path that was passed to setSource(). const QString& source() const { return m_resource_path; } + /// Renders a SVG for the specified widget and returns it. + static QPixmap renderSVGToPixmap(const QString& resource_path, const QSize& size, qreal device_pixel_ratio, + const QColor& color = QColor()); + protected: void paintEvent(QPaintEvent* event) override; void resizeEvent(QResizeEvent* event) override; diff --git a/src/duckstation-regtest/regtest_host.cpp b/src/duckstation-regtest/regtest_host.cpp index 246344af4..f48a9adba 100644 --- a/src/duckstation-regtest/regtest_host.cpp +++ b/src/duckstation-regtest/regtest_host.cpp @@ -131,8 +131,9 @@ void Host::ReportStatusMessage(std::string_view message) INFO_LOG("ReportStatusMessage: {}", message); } -void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback, - std::string_view yes_text, std::string_view no_text) +void Host::ConfirmMessageAsync(std::string_view icon, std::string_view title, std::string_view message, + ConfirmMessageAsyncCallback callback, std::string_view yes_text, + std::string_view no_text) { if (!title.empty() && !message.empty()) ERROR_LOG("ConfirmMessage: {}: {}", title, message);