refactor: rebuild desktop client with Slint

This commit is contained in:
dijunkun
2026-07-21 16:38:51 +08:00
parent eeb6a2a1ae
commit 59f77c2820
79 changed files with 7373 additions and 1009 deletions
+36 -7
View File
@@ -190,6 +190,18 @@ struct LinuxTrayImpl {
menu_label(GetMenuLabel(language_index_value)),
menu_ascii_label(IsAsciiPrintable(menu_label) ? menu_label : "Exit") {}
explicit LinuxTrayImpl(std::function<void()> show_window_callback,
std::function<void()> hide_window_callback,
std::function<void()> exit_callback,
std::string tray_tooltip, int language_index_value)
: show_window(std::move(show_window_callback)),
hide_window(std::move(hide_window_callback)),
exit_app(std::move(exit_callback)),
tooltip(std::move(tray_tooltip)),
language_index(language_index_value),
menu_label(GetMenuLabel(language_index_value)),
menu_ascii_label(IsAsciiPrintable(menu_label) ? menu_label : "Exit") {}
~LinuxTrayImpl() { RemoveTrayIcon(); }
bool MinimizeToTray() {
@@ -197,7 +209,9 @@ struct LinuxTrayImpl {
return false;
}
if (app_window) {
if (hide_window) {
hide_window();
} else if (app_window) {
SDL_HideWindow(app_window);
}
return true;
@@ -939,16 +953,20 @@ struct LinuxTrayImpl {
}
void ShowWindow() {
if (!app_window) {
return;
if (show_window) {
show_window();
} else if (app_window) {
SDL_ShowWindow(app_window);
SDL_RestoreWindow(app_window);
SDL_RaiseWindow(app_window);
}
SDL_ShowWindow(app_window);
SDL_RestoreWindow(app_window);
SDL_RaiseWindow(app_window);
}
void RequestExit() {
if (exit_app) {
exit_app();
return;
}
SDL_Event event{};
event.type =
exit_event_type == 0 || exit_event_type == static_cast<uint32_t>(-1)
@@ -958,6 +976,9 @@ struct LinuxTrayImpl {
}
::SDL_Window* app_window = nullptr;
std::function<void()> show_window;
std::function<void()> hide_window;
std::function<void()> exit_app;
std::string tooltip;
int language_index = 0;
uint32_t exit_event_type = 0;
@@ -1005,6 +1026,14 @@ LinuxTray::LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
: impl_(std::make_unique<LinuxTrayImpl>(app_window, tooltip, language_index,
exit_event_type)) {}
LinuxTray::LinuxTray(std::function<void()> show_window,
std::function<void()> hide_window,
std::function<void()> exit_app,
const std::string& tooltip, int language_index)
: impl_(std::make_unique<LinuxTrayImpl>(
std::move(show_window), std::move(hide_window), std::move(exit_app),
tooltip, language_index)) {}
LinuxTray::~LinuxTray() = default;
bool LinuxTray::MinimizeToTray() { return impl_->MinimizeToTray(); }
+4
View File
@@ -10,6 +10,7 @@
#if defined(__linux__) && !defined(__APPLE__)
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
@@ -23,6 +24,9 @@ class LinuxTray {
public:
LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index, uint32_t exit_event_type);
LinuxTray(std::function<void()> show_window,
std::function<void()> hide_window, std::function<void()> exit_app,
const std::string& tooltip, int language_index);
~LinuxTray();
bool MinimizeToTray();
+6
View File
@@ -7,6 +7,7 @@
#ifndef _MAC_TRAY_H_
#define _MAC_TRAY_H_
#include <functional>
#include <memory>
#include <string>
@@ -20,6 +21,11 @@ class MacTray {
public:
MacTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index);
MacTray(std::function<void()> show_window,
std::function<void()> hide_window,
std::function<void()> open_settings,
std::function<void()> exit_app,
const std::string& tooltip, int language_index);
~MacTray();
void MinimizeToTray();
+74 -7
View File
@@ -13,6 +13,7 @@
@interface CrossDeskMacTrayTarget : NSObject
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner;
- (void)statusItemClicked:(id)sender;
- (void)openSettings:(id)sender;
- (void)exitApplication:(id)sender;
@end
@@ -24,7 +25,24 @@ struct MacTrayImpl {
: app_window(window),
tooltip(std::move(tray_tooltip)),
language_index(language_index_value),
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {}
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {
EnsureStatusItem();
}
explicit MacTrayImpl(std::function<void()> show_window_callback,
std::function<void()> hide_window_callback,
std::function<void()> open_settings_callback,
std::function<void()> exit_callback,
std::string tray_tooltip, int language_index_value)
: show_window(std::move(show_window_callback)),
hide_window(std::move(hide_window_callback)),
open_settings(std::move(open_settings_callback)),
exit_app(std::move(exit_callback)),
tooltip(std::move(tray_tooltip)),
language_index(language_index_value),
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {
EnsureStatusItem();
}
~MacTrayImpl() {
RemoveTrayIcon();
@@ -33,7 +51,9 @@ struct MacTrayImpl {
void MinimizeToTray() {
EnsureStatusItem();
if (app_window) {
if (hide_window) {
hide_window();
} else if (app_window) {
SDL_HideWindow(app_window);
}
}
@@ -48,12 +68,12 @@ struct MacTrayImpl {
}
void ShowWindow() {
if (!app_window) {
return;
if (show_window) {
show_window();
} else if (app_window) {
SDL_ShowWindow(app_window);
SDL_RaiseWindow(app_window);
}
SDL_ShowWindow(app_window);
SDL_RaiseWindow(app_window);
[NSApp activateIgnoringOtherApps:YES];
}
@@ -64,6 +84,18 @@ struct MacTrayImpl {
}
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"CrossDesk"];
NSString *settings_title =
NSStringFromUtf8(localization::settings
[localization::detail::ClampLanguageIndex(
language_index)]);
NSMenuItem *settings_item =
[[NSMenuItem alloc] initWithTitle:settings_title
action:@selector(openSettings:)
keyEquivalent:@""];
[settings_item setTarget:target];
[menu addItem:settings_item];
[menu addItem:[NSMenuItem separatorItem]];
NSString *exit_title =
NSStringFromUtf8(localization::exit_program
[localization::detail::ClampLanguageIndex(
@@ -86,11 +118,25 @@ struct MacTrayImpl {
}
void RequestExit() {
if (exit_app) {
exit_app();
return;
}
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
void OpenSettings() {
if (open_settings) {
open_settings();
} else {
ShowWindow();
return;
}
[NSApp activateIgnoringOtherApps:YES];
}
private:
void EnsureStatusItem() {
if (status_item) {
@@ -191,6 +237,10 @@ struct MacTrayImpl {
}
::SDL_Window *app_window = nullptr;
std::function<void()> show_window;
std::function<void()> hide_window;
std::function<void()> open_settings;
std::function<void()> exit_app;
std::string tooltip;
int language_index = 0;
NSStatusItem *status_item = nil;
@@ -202,6 +252,16 @@ MacTray::MacTray(::SDL_Window *app_window, const std::string &tooltip,
: impl_(
std::make_unique<MacTrayImpl>(app_window, tooltip, language_index)) {}
MacTray::MacTray(std::function<void()> show_window,
std::function<void()> hide_window,
std::function<void()> open_settings,
std::function<void()> exit_app, const std::string &tooltip,
int language_index)
: impl_(std::make_unique<MacTrayImpl>(
std::move(show_window), std::move(hide_window),
std::move(open_settings), std::move(exit_app), tooltip,
language_index)) {}
MacTray::~MacTray() = default;
void MacTray::MinimizeToTray() { impl_->MinimizeToTray(); }
@@ -244,6 +304,13 @@ void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
}
}
- (void)openSettings:(id)sender {
(void)sender;
if (owner_) {
owner_->OpenSettings();
}
}
@end
#endif // __APPLE__
+35 -10
View File
@@ -4,6 +4,8 @@
#include "localization.h"
#include <utility>
namespace crossdesk {
// callback for the message-only window that handles tray icon messages
@@ -60,6 +62,16 @@ WinTray::WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
wcsncpy_s(nid_.szTip, tip_.c_str(), _TRUNCATE);
}
WinTray::WinTray(std::function<void()> show_window,
std::function<void()> hide_window,
std::function<void()> exit_app, HICON icon,
const std::wstring& tooltip, int language_index)
: WinTray(nullptr, icon, tooltip, language_index) {
show_window_ = std::move(show_window);
hide_window_ = std::move(hide_window);
exit_app_ = std::move(exit_app);
}
WinTray::~WinTray() {
RemoveTrayIcon();
if (hwnd_message_only_) DestroyWindow(hwnd_message_only_);
@@ -67,20 +79,31 @@ WinTray::~WinTray() {
void WinTray::MinimizeToTray() {
Shell_NotifyIcon(NIM_ADD, &nid_);
// hide application window
ShowWindow(app_hwnd_, SW_HIDE);
if (hide_window_) {
hide_window_();
} else if (app_hwnd_) {
ShowWindow(app_hwnd_, SW_HIDE);
}
}
void WinTray::RemoveTrayIcon() { Shell_NotifyIcon(NIM_DELETE, &nid_); }
void WinTray::ShowApplicationWindow() {
if (show_window_) {
show_window_();
} else if (app_hwnd_) {
ShowWindow(app_hwnd_, SW_SHOW);
SetForegroundWindow(app_hwnd_);
}
}
bool WinTray::HandleTrayMessage(MSG* msg) {
if (!msg || msg->message != WM_TRAY_CALLBACK) return false;
switch (LOWORD(msg->lParam)) {
case WM_LBUTTONDBLCLK:
case WM_LBUTTONUP: {
ShowWindow(app_hwnd_, SW_SHOW);
SetForegroundWindow(app_hwnd_);
ShowApplicationWindow();
break;
}
@@ -99,13 +122,15 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
// handle menu command
if (cmd == 1001) {
// exit application
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
if (exit_app_) {
exit_app_();
} else {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
} else if (cmd == 1002) {
ShowWindow(app_hwnd_, SW_SHOW); // show main window
SetForegroundWindow(app_hwnd_);
ShowApplicationWindow();
}
break;
}
+10 -1
View File
@@ -10,6 +10,7 @@
#include <Windows.h>
#include <shellapi.h>
#include <functional>
#include <string>
#define WM_TRAY_CALLBACK (WM_USER + 1)
@@ -20,6 +21,9 @@ class WinTray {
public:
WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
int language_index);
WinTray(std::function<void()> show_window,
std::function<void()> hide_window, std::function<void()> exit_app,
HICON icon, const std::wstring& tooltip, int language_index);
~WinTray();
void MinimizeToTray();
@@ -27,12 +31,17 @@ class WinTray {
bool HandleTrayMessage(MSG* msg);
private:
void ShowApplicationWindow();
HWND app_hwnd_;
HWND hwnd_message_only_;
HICON icon_;
std::wstring tip_;
int language_index_;
NOTIFYICONDATA nid_;
std::function<void()> show_window_;
std::function<void()> hide_window_;
std::function<void()> exit_app_;
};
} // namespace crossdesk
#endif
#endif
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <string>
namespace crossdesk {
// Starts the platform's native move loop for the window receiving the current
// left-mouse-down event. Returns false when no suitable native event exists.
bool StartNativeWindowDrag();
// Hides the disabled native zoom button on CrossDesk's fixed-size main
// window. Returns true once the matching AppKit window has been configured.
bool HideDisabledMainWindowZoomButton();
// Configures live resize and replaces the stream window's native Space
// fullscreen action with an immediate, single-window fullscreen transition.
bool ConfigureStreamWindowLiveResize();
// Enters or leaves the stream window's animation-free fullscreen mode.
bool SetStreamWindowFullscreen(bool fullscreen);
bool IsStreamWindowFullscreen();
// Opens an application-modal macOS file picker. The panel is made key before
// entering its modal loop so it receives keyboard input immediately.
std::string OpenNativeFileDialog(const std::string &title);
} // namespace crossdesk
+265
View File
@@ -0,0 +1,265 @@
#include "platform/window_drag.h"
#import <AppKit/AppKit.h>
@interface CrossDeskStreamFullscreenTarget : NSObject
- (void)toggleStreamFullscreen:(id)sender;
@end
@implementation CrossDeskStreamFullscreenTarget
- (void)toggleStreamFullscreen:(id)sender {
(void)sender;
crossdesk::SetStreamWindowFullscreen(
!crossdesk::IsStreamWindowFullscreen());
}
@end
namespace crossdesk {
namespace {
NSWindow *stream_window = nil;
CrossDeskStreamFullscreenTarget *stream_fullscreen_target = nil;
bool stream_fullscreen = false;
NSRect stream_restore_frame = NSZeroRect;
NSWindowStyleMask stream_restore_style_mask = NSWindowStyleMaskTitled;
NSWindowTitleVisibility stream_restore_title_visibility =
NSWindowTitleVisible;
BOOL stream_restore_titlebar_transparent = NO;
BOOL stream_restore_movable = YES;
BOOL stream_restore_close_hidden = NO;
BOOL stream_restore_miniaturize_hidden = NO;
BOOL stream_restore_zoom_hidden = NO;
NSApplicationPresentationOptions stream_restore_presentation_options =
NSApplicationPresentationDefault;
void InstallStreamFullscreenButton(NSWindow *window) {
if (window == nil) {
return;
}
if (stream_fullscreen_target == nil) {
stream_fullscreen_target =
[[CrossDeskStreamFullscreenTarget alloc] init];
}
NSButton *zoom_button =
[window standardWindowButton:NSWindowZoomButton];
zoom_button.target = stream_fullscreen_target;
zoom_button.action = @selector(toggleStreamFullscreen:);
}
void SetTitlebarButtonsHidden(NSWindow *window, BOOL hidden) {
[window standardWindowButton:NSWindowCloseButton].hidden = hidden;
[window standardWindowButton:NSWindowMiniaturizeButton].hidden = hidden;
[window standardWindowButton:NSWindowZoomButton].hidden = hidden;
}
void CenterWindowOnScreen(NSWindow *window, NSScreen *screen) {
if (window == nil || screen == nil) {
return;
}
const NSRect available_frame = screen.visibleFrame;
const NSRect window_frame = window.frame;
const NSPoint centered_origin = NSMakePoint(
NSMidX(available_frame) - NSWidth(window_frame) * 0.5,
NSMidY(available_frame) - NSHeight(window_frame) * 0.5);
[window setFrameOrigin:centered_origin];
}
} // namespace
bool StartNativeWindowDrag() {
@autoreleasepool {
NSEvent *event = [[NSApplication sharedApplication] currentEvent];
if (event == nil || event.type != NSEventTypeLeftMouseDown ||
event.window == nil) {
return false;
}
[event.window performWindowDragWithEvent:event];
return true;
}
}
bool HideDisabledMainWindowZoomButton() {
@autoreleasepool {
for (NSWindow *window in [NSApp windows]) {
if (![window.title isEqualToString:@"CrossDesk"]) {
continue;
}
NSButton *zoom_button =
[window standardWindowButton:NSWindowZoomButton];
// The main window has a fixed size, so AppKit disables its zoom button.
// Stream windows remain resizable and must keep their native button.
if (zoom_button == nil || zoom_button.enabled) {
continue;
}
zoom_button.hidden = YES;
return true;
}
return false;
}
}
bool ConfigureStreamWindowLiveResize() {
@autoreleasepool {
for (NSWindow *window in [NSApp windows]) {
if (![window.title isEqualToString:@"CrossDesk"]) {
continue;
}
NSButton *zoom_button =
[window standardWindowButton:NSWindowZoomButton];
// The stream window is resizable and therefore keeps an enabled native
// zoom button. The fixed-size main window's zoom button is disabled.
if (zoom_button == nil || !zoom_button.enabled) {
continue;
}
NSView *content_view = window.contentView;
if (content_view == nil) {
return false;
}
// During an ordinary edge resize, scale one cached frame and redraw once
// the new window size has settled.
window.preservesContentDuringLiveResize = YES;
content_view.layerContentsRedrawPolicy =
NSViewLayerContentsRedrawBeforeViewResize;
content_view.layerContentsPlacement =
NSViewLayerContentsPlacementScaleProportionallyToFit;
stream_window = window;
// Native Space fullscreen cross-fades an AppKit source snapshot over a
// separately rendered destination window. Disable that path for the
// stream window and route the green button to the immediate transition.
window.collectionBehavior =
(window.collectionBehavior &
~(NSWindowCollectionBehaviorFullScreenPrimary |
NSWindowCollectionBehaviorFullScreenAuxiliary |
NSWindowCollectionBehaviorFullScreenAllowsTiling)) |
NSWindowCollectionBehaviorFullScreenNone;
InstallStreamFullscreenButton(window);
content_view.needsDisplay = YES;
return true;
}
return false;
}
}
bool SetStreamWindowFullscreen(bool fullscreen) {
@autoreleasepool {
if (stream_window == nil || stream_fullscreen == fullscreen) {
return stream_window != nil;
}
if (fullscreen) {
NSScreen *screen = stream_window.screen ?: NSScreen.mainScreen;
if (screen == nil) {
return false;
}
stream_restore_frame = stream_window.frame;
stream_restore_style_mask = stream_window.styleMask;
stream_restore_title_visibility = stream_window.titleVisibility;
stream_restore_titlebar_transparent =
stream_window.titlebarAppearsTransparent;
stream_restore_movable = stream_window.movable;
stream_restore_close_hidden =
[stream_window standardWindowButton:NSWindowCloseButton].hidden;
stream_restore_miniaturize_hidden =
[stream_window standardWindowButton:NSWindowMiniaturizeButton].hidden;
stream_restore_zoom_hidden =
[stream_window standardWindowButton:NSWindowZoomButton].hidden;
stream_restore_presentation_options = NSApp.presentationOptions;
stream_fullscreen = true;
stream_window.styleMask =
stream_restore_style_mask | NSWindowStyleMaskFullSizeContentView;
stream_window.titleVisibility = NSWindowTitleHidden;
stream_window.titlebarAppearsTransparent = YES;
stream_window.movable = NO;
SetTitlebarButtonsHidden(stream_window, YES);
NSApplicationPresentationOptions presentation_options =
stream_restore_presentation_options;
if ((presentation_options & (NSApplicationPresentationHideDock |
NSApplicationPresentationAutoHideDock)) ==
0) {
presentation_options |= NSApplicationPresentationAutoHideDock;
}
if ((presentation_options & (NSApplicationPresentationHideMenuBar |
NSApplicationPresentationAutoHideMenuBar)) ==
0) {
presentation_options |= NSApplicationPresentationAutoHideMenuBar;
}
NSApp.presentationOptions = presentation_options;
[stream_window setFrame:screen.frame display:YES animate:NO];
[stream_window makeKeyAndOrderFront:nil];
} else {
stream_fullscreen = false;
NSApp.presentationOptions = stream_restore_presentation_options;
stream_window.styleMask = stream_restore_style_mask;
stream_window.titleVisibility = stream_restore_title_visibility;
stream_window.titlebarAppearsTransparent =
stream_restore_titlebar_transparent;
stream_window.movable = stream_restore_movable;
[stream_window setFrame:stream_restore_frame display:YES animate:NO];
[stream_window standardWindowButton:NSWindowCloseButton].hidden =
stream_restore_close_hidden;
[stream_window standardWindowButton:NSWindowMiniaturizeButton].hidden =
stream_restore_miniaturize_hidden;
[stream_window standardWindowButton:NSWindowZoomButton].hidden =
stream_restore_zoom_hidden;
InstallStreamFullscreenButton(stream_window);
[stream_window makeKeyAndOrderFront:nil];
}
stream_window.contentView.alphaValue = 1.0;
stream_window.contentView.needsDisplay = YES;
[stream_window displayIfNeeded];
return true;
}
}
bool IsStreamWindowFullscreen() {
return stream_fullscreen;
}
std::string OpenNativeFileDialog(const std::string &title) {
@autoreleasepool {
NSOpenPanel *panel = [NSOpenPanel openPanel];
panel.canChooseFiles = YES;
panel.canChooseDirectories = NO;
panel.allowsMultipleSelection = NO;
panel.resolvesAliases = YES;
NSString *prompt =
[[NSString alloc] initWithBytes:title.data()
length:title.size()
encoding:NSUTF8StringEncoding];
if (prompt != nil && prompt.length > 0) {
panel.title = prompt;
panel.message = prompt;
}
// tinyfiledialogs uses an external osascript process on macOS, so its
// chooser can appear without becoming the active window. Keep the picker
// inside CrossDesk and explicitly activate both the app and panel.
[NSApp activateIgnoringOtherApps:YES];
NSWindow *owner = stream_window ?: NSApp.keyWindow ?: NSApp.mainWindow;
if (owner != nil) {
[owner makeKeyAndOrderFront:nil];
}
NSScreen *target_screen = owner.screen ?: NSScreen.mainScreen;
CenterWindowOnScreen(panel, target_screen);
[panel makeKeyAndOrderFront:nil];
if ([panel runModal] != NSModalResponseOK || panel.URL == nil) {
return {};
}
const char *path = panel.URL.fileSystemRepresentation;
return path != nullptr ? std::string(path) : std::string{};
}
}
} // namespace crossdesk