emulator: Set window icon to game icon. (#4586)

* emulator: Set window icon to game icon.

* window: Match system icon look-and-feel on macOS

* window: Log SDL error on failure to set icon
This commit is contained in:
squidbus 2026-06-17 09:52:46 -07:00 committed by GitHub
parent c7956d066e
commit 0d5f7e83b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 132 additions and 8 deletions

View File

@ -816,6 +816,10 @@ set(COMMON src/common/logging/classes.h
src/common/key_manager.h
)
if (APPLE)
list(APPEND COMMON src/common/apple.h src/common/apple.mm)
endif()
if (ENABLE_DISCORD_RPC)
list(APPEND COMMON src/common/discord_rpc_handler.cpp src/common/discord_rpc_handler.h)
endif()

12
src/common/apple.h Normal file
View File

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
namespace Common {
void SetAppIcon(const std::filesystem::path& path);
} // namespace Common

47
src/common/apple.mm Normal file
View File

@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <filesystem>
#include <Cocoa/Cocoa.h>
#include "common/apple.h"
namespace Common {
void SetAppIcon(const std::filesystem::path& path) {
@autoreleasepool {
NSString* path_str = [NSString stringWithUTF8String:path.native().c_str()];
NSImage* base_icon = [[[NSImage alloc] initWithContentsOfFile:path_str] autorelease];
// Transform the icon to match native look-and-feel.
constexpr double ScaleFactor = 13.0 / 16.0;
constexpr double CornerRadiusFactor = 22.0 / 100.0;
const double base_icon_width = base_icon.size.width;
const double base_icon_height = base_icon.size.height;
const double icon_width = base_icon_width * ScaleFactor;
const double icon_height = base_icon_height * ScaleFactor;
const double icon_x = (base_icon_width - icon_width) / 2.0;
const double icon_y = (base_icon_height - icon_height) / 2.0;
const double corner_radius_x = icon_width * CornerRadiusFactor;
const double corner_radius_y = icon_height * CornerRadiusFactor;
NSRect bounds = NSMakeRect(icon_x, icon_y, icon_width, icon_height);
NSBezierPath* mask_path = [NSBezierPath bezierPathWithRoundedRect:bounds
xRadius:corner_radius_x
yRadius:corner_radius_y];
NSImage* rounded_icon = [[[NSImage alloc] initWithSize:base_icon.size] autorelease];
[rounded_icon lockFocus];
[mask_path addClip];
[base_icon drawInRect:bounds
fromRect:NSZeroRect
operation:NSCompositingOperationSourceOver
fraction:1.0f];
[rounded_icon unlockFocus];
[NSApp setApplicationIconImage:rounded_icon];
}
}
} // namespace Common

View File

@ -430,6 +430,11 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
g_window = window.get();
std::filesystem::path icon_path = mnt->GetHostPath("/app0/sce_sys/icon0.png");
if (std::filesystem::exists(icon_path)) {
window->SetIcon(icon_path);
}
const auto& mount_data_dir = Common::FS::GetUserPath(Common::FS::PathType::GameDataDir);
mnt->Mount(mount_data_dir, "/data");

View File

@ -1,14 +1,19 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_hints.h"
#include "SDL3/SDL_init.h"
#include "SDL3/SDL_properties.h"
#include "SDL3/SDL_timer.h"
#include "SDL3/SDL_video.h"
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_hints.h>
#include <SDL3/SDL_init.h>
#include <SDL3/SDL_properties.h>
#include <SDL3/SDL_timer.h>
#include <SDL3/SDL_video.h>
#include <stb_image.h>
#include "common/assert.h"
#include "common/elf_info.h"
#include "common/io_file.h"
#include "common/logging/formatter.h"
#include "common/scope_exit.h"
#include "core/debug_state.h"
#include "core/devtools/layer.h"
#include "core/emulator_settings.h"
@ -24,9 +29,9 @@
#include "video_core/renderdoc.h"
#ifdef __APPLE__
#include "SDL3/SDL_metal.h"
#include <SDL3/SDL_metal.h>
#include "common/apple.h"
#endif
#include <core/emulator_settings.h>
namespace Frontend {
@ -172,6 +177,55 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameControllers* controller
WindowSDL::~WindowSDL() = default;
void WindowSDL::SetIcon(const std::filesystem::path& path) {
#ifdef __APPLE__
// Use native path which matches system icon look-and-feel.
Common::SetAppIcon(path);
#else
Common::FS::IOFile file{path, Common::FS::FileAccessMode::Read,
Common::FS::FileType::BinaryFile,
Common::FS::FileShareFlag::ShareReadWrite};
if (!file.IsOpen()) {
LOG_ERROR(Core, "Failed to open window icon file '{}'.", fmt::UTF(path.u8string()));
return;
}
const u64 file_size = file.GetSize();
std::vector<u8> buf(file_size);
const size_t bytes_read = file.ReadRaw<u8>(buf.data(), file_size);
file.Close();
if (bytes_read < file_size) {
LOG_ERROR(Core, "Failed to read window icon file '{}'.", fmt::UTF(path.u8string()));
return;
}
int image_width = 0;
int image_height = 0;
constexpr int num_channels = 4;
unsigned char* image_data =
stbi_load_from_memory(buf.data(), static_cast<int>(buf.size()), &image_width, &image_height,
nullptr, num_channels);
if (image_data == nullptr) {
LOG_ERROR(Core, "Failed to load window icon image '{}': {}", fmt::UTF(path.u8string()),
stbi_failure_reason());
return;
}
SCOPE_EXIT {
stbi_image_free(image_data);
};
SDL_Surface* surface = SDL_CreateSurfaceFrom(image_width, image_height, SDL_PIXELFORMAT_RGBA32,
image_data, image_width * num_channels);
if (surface == nullptr) {
LOG_ERROR(Core, "Failed to create SDL surface for window icon: {}", SDL_GetError());
}
if (!SDL_SetWindowIcon(window, surface)) {
LOG_ERROR(Core, "Failed to set SDL window icon: {}", SDL_GetError());
}
SDL_DestroySurface(surface);
#endif
}
void WindowSDL::WaitEvent() {
// Called on main thread
SDL_Event event;

View File

@ -71,6 +71,8 @@ public:
return window_info;
}
void SetIcon(const std::filesystem::path& path);
void WaitEvent();
void InitTimers();