diff --git a/CMakeLists.txt b/CMakeLists.txt index 018f5c866..60870e136 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() diff --git a/src/common/apple.h b/src/common/apple.h new file mode 100644 index 000000000..2c1d20c43 --- /dev/null +++ b/src/common/apple.h @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +namespace Common { + +void SetAppIcon(const std::filesystem::path& path); + +} // namespace Common diff --git a/src/common/apple.mm b/src/common/apple.mm new file mode 100644 index 000000000..2295d3628 --- /dev/null +++ b/src/common/apple.mm @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#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 diff --git a/src/emulator.cpp b/src/emulator.cpp index 963ef32f5..275ccb875 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -430,6 +430,11 @@ void Emulator::Run(std::filesystem::path file, std::vector 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"); diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index 2dc2e9ffa..9ea7b363a 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -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 +#include +#include +#include +#include +#include +#include + #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 +#include "common/apple.h" #endif -#include 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 buf(file_size); + const size_t bytes_read = file.ReadRaw(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(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; diff --git a/src/sdl_window.h b/src/sdl_window.h index 4fc750bbc..677c4c060 100644 --- a/src/sdl_window.h +++ b/src/sdl_window.h @@ -71,6 +71,8 @@ public: return window_info; } + void SetIcon(const std::filesystem::path& path); + void WaitEvent(); void InitTimers();