[feat] support minimizing to the system tray on macOS when closing, refs #87

This commit is contained in:
dijunkun
2026-06-23 00:39:44 +08:00
parent 009699b375
commit e026491b9f
8 changed files with 334 additions and 19 deletions
+31 -2
View File
@@ -1371,6 +1371,9 @@ int Render::CreateMainWindow() {
HICON tray_icon = LoadTrayIcon(); HICON tray_icon = LoadTrayIcon();
tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk", tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk",
localization_language_index_); localization_language_index_);
#elif defined(__APPLE__)
tray_ = std::make_unique<MacTray>(main_window_, "CrossDesk",
localization_language_index_);
#endif #endif
ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_); ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_);
@@ -2051,6 +2054,23 @@ void Render::MainLoop() {
} }
} }
bool Render::MinimizeMainWindowToTray() {
if (!enable_minimize_to_tray_ || !main_window_) {
return false;
}
#if defined(_WIN32) || defined(__APPLE__)
if (!tray_) {
return false;
}
tray_->MinimizeToTray();
return true;
#else
return false;
#endif
}
void Render::UpdateLabels() { void Render::UpdateLabels() {
if (!label_inited_ || if (!label_inited_ ||
localization_language_index_last_ != localization_language_index_) { localization_language_index_last_ != localization_language_index_) {
@@ -2910,9 +2930,18 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
break; break;
case SDL_EVENT_WINDOW_CLOSE_REQUESTED: case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
if (event.window.windowID != SDL_GetWindowID(stream_window_)) { if (stream_window_ &&
exit_ = true; event.window.windowID == SDL_GetWindowID(stream_window_)) {
break;
} }
if (main_window_ &&
event.window.windowID == SDL_GetWindowID(main_window_) &&
MinimizeMainWindowToTray()) {
break;
}
exit_ = true;
break; break;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
+5
View File
@@ -39,6 +39,8 @@
#if _WIN32 #if _WIN32
#include "win_tray.h" #include "win_tray.h"
#elif defined(__APPLE__)
#include "mac_tray.h"
#endif #endif
namespace crossdesk { namespace crossdesk {
@@ -414,6 +416,7 @@ class Render {
int AudioDeviceInit(); int AudioDeviceInit();
int AudioDeviceDestroy(); int AudioDeviceDestroy();
void HandleWindowsServiceIntegration(); void HandleWindowsServiceIntegration();
bool MinimizeMainWindowToTray();
#if _WIN32 #if _WIN32
void ResetLocalWindowsServiceState(bool clear_pending_sas); void ResetLocalWindowsServiceState(bool clear_pending_sas);
#if CROSSDESK_PORTABLE #if CROSSDESK_PORTABLE
@@ -510,6 +513,8 @@ class Render {
const int sdl_refresh_ms_ = 16; // ~60 FPS const int sdl_refresh_ms_ = 16; // ~60 FPS
#if _WIN32 #if _WIN32
std::unique_ptr<WinTray> tray_; std::unique_ptr<WinTray> tray_;
#elif defined(__APPLE__)
std::unique_ptr<MacTray> tray_;
#endif #endif
// main window properties // main window properties
+6 -10
View File
@@ -300,17 +300,13 @@ int Render::TitleBar(bool main_window) {
} }
if (close_button_clicked) { if (close_button_clicked) {
#if _WIN32 if (main_window && MinimizeMainWindowToTray()) {
if (enable_minimize_to_tray_) { return 0;
tray_->MinimizeToTray();
} else {
#endif
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
#if _WIN32
} }
#endif
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
} }
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f, draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
+34
View File
@@ -0,0 +1,34 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-06-23
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _MAC_TRAY_H_
#define _MAC_TRAY_H_
#include <memory>
#include <string>
struct SDL_Window;
namespace crossdesk {
struct MacTrayImpl;
class MacTray {
public:
MacTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index);
~MacTray();
void MinimizeToTray();
void RemoveTrayIcon();
private:
std::unique_ptr<MacTrayImpl> impl_;
};
} // namespace crossdesk
#endif // _MAC_TRAY_H_
+249
View File
@@ -0,0 +1,249 @@
#include "mac_tray.h"
#if defined(__APPLE__)
#include <SDL3/SDL.h>
#import <Cocoa/Cocoa.h>
#include "localization.h"
#include <utility>
@interface CrossDeskMacTrayTarget : NSObject
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner;
- (void)statusItemClicked:(id)sender;
- (void)exitApplication:(id)sender;
@end
namespace crossdesk {
struct MacTrayImpl {
explicit MacTrayImpl(::SDL_Window *window, std::string tray_tooltip,
int language_index_value)
: app_window(window),
tooltip(std::move(tray_tooltip)),
language_index(language_index_value),
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {}
~MacTrayImpl() {
RemoveTrayIcon();
target = nil;
}
void MinimizeToTray() {
EnsureStatusItem();
if (app_window) {
SDL_HideWindow(app_window);
}
}
void RemoveTrayIcon() {
if (!status_item) {
return;
}
[[NSStatusBar systemStatusBar] removeStatusItem:status_item];
status_item = nil;
}
void ShowWindow() {
if (!app_window) {
return;
}
SDL_ShowWindow(app_window);
SDL_RaiseWindow(app_window);
[NSApp activateIgnoringOtherApps:YES];
}
void ShowMenu() {
EnsureStatusItem();
if (!status_item) {
return;
}
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"CrossDesk"];
NSString *exit_title =
NSStringFromUtf8(localization::exit_program
[localization::detail::ClampLanguageIndex(
language_index)]);
NSMenuItem *exit_item = [[NSMenuItem alloc] initWithTitle:exit_title
action:@selector(exitApplication:)
keyEquivalent:@""];
[exit_item setTarget:target];
[menu addItem:exit_item];
NSStatusBarButton *button = [status_item button];
if (!button) {
return;
}
const NSRect bounds = [button bounds];
[menu popUpMenuPositioningItem:nil
atLocation:NSMakePoint(NSMinX(bounds), NSMinY(bounds))
inView:button];
}
void RequestExit() {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
private:
void EnsureStatusItem() {
if (status_item) {
return;
}
status_item = [[NSStatusBar systemStatusBar]
statusItemWithLength:NSSquareStatusItemLength];
NSStatusBarButton *button = [status_item button];
if (!button) {
return;
}
[button setToolTip:NSStringFromUtf8(tooltip)];
NSImage *crossdesk_icon = LoadCrossDeskIcon();
if (crossdesk_icon) {
NSImage *status_icon = [crossdesk_icon copy];
[status_icon setSize:NSMakeSize(18.0, 18.0)];
[status_icon setTemplate:NO];
[button setImage:status_icon];
[button setImagePosition:NSImageOnly];
} else {
[button setTitle:@"CD"];
}
[button setTarget:target];
[button setAction:@selector(statusItemClicked:)];
[button sendActionOn:NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp];
}
NSString *NSStringFromUtf8(const std::string &text) {
return [NSString stringWithUTF8String:text.c_str()];
}
NSImage *LoadCrossDeskIcon() {
NSImage *icon = LoadIconFromBundleResource(@"crossdesk");
if (!icon) {
icon = LoadIconFromBundleResource(@"crossedesk");
}
if (!icon) {
icon = LoadIconFromDevelopmentPath();
}
if (!icon) {
icon = [NSApp applicationIconImage];
}
return icon;
}
NSImage *LoadIconFromBundleResource(NSString *resource_name) {
NSString *icon_path =
[[NSBundle mainBundle] pathForResource:resource_name ofType:@"icns"];
return LoadIconFromPath(icon_path);
}
NSImage *LoadIconFromDevelopmentPath() {
NSMutableArray<NSString *> *candidate_paths = [NSMutableArray array];
NSString *current_directory =
[[NSFileManager defaultManager] currentDirectoryPath];
[candidate_paths
addObject:[current_directory
stringByAppendingPathComponent:
@"icons/macos/crossdesk.icns"]];
const char *base_path = SDL_GetBasePath();
if (base_path && base_path[0] != '\0') {
NSString *base_directory = NSStringFromUtf8(base_path);
[candidate_paths
addObject:[base_directory
stringByAppendingPathComponent:
@"icons/macos/crossdesk.icns"]];
[candidate_paths
addObject:[base_directory
stringByAppendingPathComponent:
@"../../../../icons/macos/crossdesk.icns"]];
}
for (NSString *candidate_path in candidate_paths) {
NSImage *icon = LoadIconFromPath(
[candidate_path stringByStandardizingPath]);
if (icon) {
return icon;
}
}
return nil;
}
NSImage *LoadIconFromPath(NSString *icon_path) {
if (![icon_path length]) {
return nil;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:icon_path]) {
return nil;
}
return [[NSImage alloc] initWithContentsOfFile:icon_path];
}
::SDL_Window *app_window = nullptr;
std::string tooltip;
int language_index = 0;
NSStatusItem *status_item = nil;
CrossDeskMacTrayTarget *target = nil;
};
MacTray::MacTray(::SDL_Window *app_window, const std::string &tooltip,
int language_index)
: impl_(
std::make_unique<MacTrayImpl>(app_window, tooltip, language_index)) {}
MacTray::~MacTray() = default;
void MacTray::MinimizeToTray() { impl_->MinimizeToTray(); }
void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
} // namespace crossdesk
@implementation CrossDeskMacTrayTarget {
crossdesk::MacTrayImpl *owner_;
}
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner {
self = [super init];
if (self) {
owner_ = owner;
}
return self;
}
- (void)statusItemClicked:(id)sender {
(void)sender;
if (!owner_) {
return;
}
NSEvent *event = [NSApp currentEvent];
if (event && [event type] == NSEventTypeRightMouseUp) {
owner_->ShowMenu();
return;
}
owner_->ShowWindow();
}
- (void)exitApplication:(id)sender {
(void)sender;
if (owner_) {
owner_->RequestExit();
}
}
@end
#endif // __APPLE__
+3 -3
View File
@@ -356,7 +356,7 @@ int Render::SettingWindow() {
ImGui::Separator(); ImGui::Separator();
{ {
#ifndef _WIN32 #if !defined(_WIN32) && !defined(__APPLE__)
ImGui::BeginDisabled(); ImGui::BeginDisabled();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
#endif #endif
@@ -375,7 +375,7 @@ int Render::SettingWindow() {
ImGui::Checkbox("##enable_minimize_to_tray_", ImGui::Checkbox("##enable_minimize_to_tray_",
&enable_minimize_to_tray_); &enable_minimize_to_tray_);
#ifndef _WIN32 #if !defined(_WIN32) && !defined(__APPLE__)
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::EndDisabled(); ImGui::EndDisabled();
#endif #endif
@@ -626,7 +626,7 @@ int Render::SettingWindow() {
} }
enable_daemon_last_ = enable_daemon_; enable_daemon_last_ = enable_daemon_;
#if _WIN32 #if defined(_WIN32) || defined(__APPLE__)
if (enable_minimize_to_tray_) { if (enable_minimize_to_tray_) {
config_center_->SetMinimizeToTray(true); config_center_->SetMinimizeToTray(true);
} else { } else {
+4 -3
View File
@@ -75,7 +75,8 @@ function setup_platform_settings()
add_links("SDL3") add_links("SDL3")
add_ldflags("-Wl,-ld_classic") add_ldflags("-Wl,-ld_classic")
add_cxflags("-Wno-unused-variable") add_cxflags("-Wno-unused-variable")
add_frameworks("OpenGL", "IOSurface", "ScreenCaptureKit", "AVFoundation", add_frameworks("Cocoa", "OpenGL", "IOSurface", "ScreenCaptureKit",
"CoreMedia", "CoreVideo", "CoreAudio", "AudioToolbox") "AVFoundation", "CoreMedia", "CoreVideo", "CoreAudio",
"AudioToolbox")
end end
end end
+2 -1
View File
@@ -218,7 +218,8 @@ function setup_targets()
add_includedirs("src/gui/tray", "src/service/windows", add_includedirs("src/gui/tray", "src/service/windows",
{public = true}) {public = true})
elseif is_os("macosx") then elseif is_os("macosx") then
add_files("src/gui/windows/*.mm") add_files("src/gui/windows/*.mm", "src/gui/tray/*.mm")
add_includedirs("src/gui/tray", {public = true})
end end
if is_os("windows") then if is_os("windows") then