5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-20 10:59:30 +08:00

Fix macOS systray DPI scaling and offset.

Added new systray examples.
This commit is contained in:
Lea Anthony 2024-12-13 23:46:27 +11:00
parent a0ff53b629
commit beacf06c7d
No known key found for this signature in database
GPG Key ID: 33DAF7BB90A58405
14 changed files with 308 additions and 66 deletions

2
v3/.gitignore vendored
View File

@ -1,6 +1,6 @@
examples/kitchensink/kitchensink
cmd/wails3/wails
/examples/systray/systray
/examples/systray-menu/systray
/examples/window/window
/examples/dialogs/dialogs
/examples/menu/menu

View File

@ -0,0 +1,16 @@
# Systray Basic Example
This example creates a simple system tray with an attached window.
The window is hidden by default and toggled by left-clicking on the systray icon.
The window will hide automatically when it loses focus.
On Windows, if the icon is in the notification flyout,
then the window will be shown in the bottom right corner.
# Status
| Platform | Status |
|----------|---------|
| Mac | Working |
| Windows | Working |
| Linux | |

View File

@ -0,0 +1,56 @@
package main
import (
_ "embed"
"log"
"runtime"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/icons"
)
func main() {
app := application.New(application.Options{
Name: "Systray Demo",
Description: "A demo of the Systray API",
Assets: application.AlphaAssets,
Mac: application.MacOptions{
ActivationPolicy: application.ActivationPolicyAccessory,
},
})
systemTray := app.NewSystemTray()
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Width: 500,
Height: 500,
Name: "Systray Demo Window",
Frameless: true,
AlwaysOnTop: true,
Hidden: true,
DisableResize: true,
ShouldClose: func(window *application.WebviewWindow) bool {
window.Hide()
return false
},
Windows: application.WindowsWindow{
HiddenOnTaskbar: true,
},
KeyBindings: map[string]func(window *application.WebviewWindow){
"F12": func(window *application.WebviewWindow) {
systemTray.OpenMenu()
},
},
})
if runtime.GOOS == "darwin" {
systemTray.SetTemplateIcon(icons.SystrayMacTemplate)
}
systemTray.AttachWindow(window).WindowOffset(5)
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

View File

@ -0,0 +1,16 @@
# Systray Custom Example
This example creates a simple system tray and uses hooks to attach a custom window.
The window is hidden by default and toggled by left-clicking on the systray icon.
The window will hide automatically when it loses focus.
On Windows, if the icon is in the notification flyout,
then the window will be shown in the bottom right corner.
# Status
| Platform | Status |
|----------|---------|
| Mac | Working |
| Windows | |
| Linux | |

View File

@ -0,0 +1,68 @@
package main
import (
_ "embed"
"log"
"runtime"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/icons"
)
func main() {
app := application.New(application.Options{
Name: "Systray Demo",
Description: "A demo of the Systray API",
Assets: application.AlphaAssets,
Mac: application.MacOptions{
ActivationPolicy: application.ActivationPolicyAccessory,
},
})
systemTray := app.NewSystemTray()
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Width: 500,
Height: 500,
Name: "Systray Demo Window",
Frameless: true,
AlwaysOnTop: true,
Hidden: true,
DisableResize: true,
ShouldClose: func(window *application.WebviewWindow) bool {
window.Hide()
return false
},
Windows: application.WindowsWindow{
HiddenOnTaskbar: true,
},
})
if runtime.GOOS == "darwin" {
systemTray.SetTemplateIcon(icons.SystrayMacTemplate)
}
systemTray.OnClick(func() {
println("System tray clicked!")
if window.IsVisible() {
window.Hide()
} else {
window.Show()
}
})
systemTray.OnDoubleClick(func() {
println("System tray double clicked!")
})
systemTray.OnRightClick(func() {
println("System tray right clicked!")
})
systemTray.AttachWindow(window).WindowOffset(5)
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

View File

@ -1,4 +1,4 @@
# Systray Example
# Systray Menu Example
This example creates a system tray with an attached window and a menu.
The window is hidden by default and toggled by left-clicking on the systray icon.
@ -12,6 +12,6 @@ then the window will be shown in the bottom right corner.
| Platform | Status |
|----------|---------|
| Mac | |
| Mac | Working |
| Windows | Working |
| Linux | |

View File

@ -21,7 +21,7 @@ func main() {
systemTray := app.NewSystemTray()
_ = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Width: 500,
Height: 500,
Name: "Systray Demo Window",
@ -92,19 +92,8 @@ func main() {
})
systemTray.SetMenu(myMenu)
systemTray.OnClick(func() {
println("System tray clicked!")
})
systemTray.OnDoubleClick(func() {
println("System tray double clicked!")
})
systemTray.OnRightClick(func() {
println("System tray right clicked!")
})
//systemTray.AttachWindow(window).WindowOffset(5)
systemTray.AttachWindow(window).WindowOffset(2)
err := app.Run()
if err != nil {

View File

@ -36,6 +36,7 @@ require (
golang.org/x/sys v0.28.0
golang.org/x/term v0.26.0
golang.org/x/tools v0.23.0
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.21.0
)

View File

@ -407,6 +407,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=

View File

@ -14,6 +14,7 @@ import "C"
import (
"unsafe"
"fmt"
"github.com/leaanthony/go-ansi-parser"
)
@ -29,6 +30,7 @@ type macosSystemTray struct {
iconPosition int
isTemplateIcon bool
parent *SystemTray
lastClickedScreen unsafe.Pointer
}
func (s *macosSystemTray) openMenu() {
@ -68,60 +70,48 @@ func (s *macosSystemTray) setMenu(menu *Menu) {
}
func (s *macosSystemTray) positionWindow(window *WebviewWindow, offset int) error {
// Get the window's native window
impl := window.impl.(*macosWebviewWindow)
// Get the trayBounds of this system tray
trayBounds, err := s.bounds()
if err != nil {
return err
}
// Get the current screen trayBounds
currentScreen, err := s.getScreen()
if err != nil {
return err
}
screenBounds := currentScreen.Bounds
// Get the center height of the window
windowWidthCenter := window.Width() / 2
// Get the center height of the system tray
systemTrayWidthCenter := trayBounds.Width / 2
// The Y will be 0 and the X will make the center of the window line up with the center of the system tray
windowX := trayBounds.X + systemTrayWidthCenter - windowWidthCenter
// If the end of the window goes off-screen, move it back enough to be on screen
if windowX+window.Width() > screenBounds.Width {
windowX = screenBounds.Width - window.Width()
}
window.SetRelativePosition(windowX, int(C.statusBarHeight())+offset)
// Position the window relative to the systray
C.systemTrayPositionWindow(s.nsStatusItem, impl.nsWindow, C.int(offset))
return nil
}
func (s *macosSystemTray) getScreen() (*Screen, error) {
return getScreenForSystray(s)
if s.lastClickedScreen != nil {
// Get the screen frame
frame := C.NSScreen_frame(s.lastClickedScreen)
result := &Screen{
Bounds: Rect{
X: int(frame.origin.x),
Y: int(frame.origin.y),
Width: int(frame.size.width),
Height: int(frame.size.height),
},
}
return result, nil
}
return nil, fmt.Errorf("no screen available")
}
func (s *macosSystemTray) bounds() (*Rect, error) {
var rect C.NSRect
C.systemTrayGetBounds(s.nsStatusItem, &rect)
// Get the screen height for the screen that the systray is on
screen, err := getScreenForSystray(s)
if err != nil {
return nil, err
}
var screen unsafe.Pointer
C.systemTrayGetBounds(s.nsStatusItem, &rect, &screen)
// Invert Y axis based on screen height
rect.origin.y = C.double(screen.Bounds.Height) - rect.origin.y - rect.size.height
// Store the screen for use in positionWindow
s.lastClickedScreen = screen
return &Rect{
// Return the screen-relative coordinates
result := &Rect{
X: int(rect.origin.x),
Y: int(rect.origin.y),
Width: int(rect.size.width),
Height: int(rect.size.height),
}, nil
}
return result, nil
}
func (s *macosSystemTray) run() {

View File

@ -1,5 +1,7 @@
//go:build darwin
#include <Cocoa/Cocoa.h>
@interface StatusItemController : NSObject
@property long id;
- (void)statusItemClicked:(id)sender;
@ -8,11 +10,15 @@
void* systemTrayNew(long id);
void systemTraySetLabel(void* nsStatusItem, char *label);
void systemTraySetANSILabel(void* nsStatusItem, void* attributedString);
void systemTraySetLabelColor(void* nsStatusItem, char *fg, char *bg);
void* createAttributedString(char *title, char *FG, char *BG);
void* appendAttributedString(void* original, char* label, char* fg, char* bg);
NSImage* imageFromBytes(const unsigned char *bytes, int length);
void systemTraySetIcon(void* nsStatusItem, void* nsImage, int position, bool isTemplate);
void systemTrayDestroy(void* nsStatusItem);
void showMenu(void* nsStatusItem, void *nsMenu);
void systemTrayGetBounds(void* nsStatusItem, NSRect *rect);
void systemTrayGetBounds(void* nsStatusItem, NSRect *rect, void **screen);
NSRect NSScreen_frame(void* screen);
void windowSetScreen(void* window, void* screen, int yOffset);
int statusBarHeight();
void systemTrayPositionWindow(void* nsStatusItem, void* nsWindow, int offset);

View File

@ -160,10 +160,36 @@ void showMenu(void* nsStatusItem, void *nsMenu) {
});
}
void systemTrayGetBounds(void* nsStatusItem, NSRect *rect) {
void systemTrayGetBounds(void* nsStatusItem, NSRect *rect, void **outScreen) {
NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem;
NSRect buttonFrame = statusItem.button.frame;
*rect = [statusItem.button.window convertRectToScreen:buttonFrame];
NSStatusBarButton *button = statusItem.button;
// Get mouse location and find the screen it's on
NSPoint mouseLocation = [NSEvent mouseLocation];
NSScreen *screen = nil;
NSArray *screens = [NSScreen screens];
for (NSScreen *candidate in screens) {
NSRect frame = [candidate frame];
if (NSPointInRect(mouseLocation, frame)) {
screen = candidate;
break;
}
}
if (!screen) {
screen = [NSScreen mainScreen];
}
// Get button frame in screen coordinates
NSRect buttonFrame = button.frame;
NSRect buttonFrameScreen = [button.window convertRectToScreen:buttonFrame];
*rect = buttonFrameScreen;
*outScreen = (void*)screen;
}
NSRect NSScreen_frame(void* screen) {
return [(NSScreen*)screen frame];
}
int statusBarHeight() {
@ -171,3 +197,48 @@ int statusBarHeight() {
CGFloat menuBarHeight = [mainMenu menuBarHeight];
return (int)menuBarHeight;
}
void systemTrayPositionWindow(void* nsStatusItem, void* nsWindow, int offset) {
// Get the status item's button
NSStatusBarButton *button = [(NSStatusItem*)nsStatusItem button];
// Get the frame in screen coordinates
NSRect frame = [button.window convertRectToScreen:button.frame];
// Get the screen that contains the status item
NSScreen *screen = [button.window screen];
if (screen == nil) {
screen = [NSScreen mainScreen];
}
// Get screen's backing scale factor (DPI)
CGFloat scaleFactor = [screen backingScaleFactor];
// Get the window's frame
NSRect windowFrame = [(NSWindow*)nsWindow frame];
// Calculate the horizontal position (centered under the status item)
CGFloat windowX = frame.origin.x + (frame.size.width - windowFrame.size.width) / 2;
// If the window would go off the right edge of the screen, adjust it
if (windowX + windowFrame.size.width > screen.frame.origin.x + screen.frame.size.width) {
windowX = screen.frame.origin.x + screen.frame.size.width - windowFrame.size.width;
}
// If the window would go off the left edge of the screen, adjust it
if (windowX < screen.frame.origin.x) {
windowX = screen.frame.origin.x;
}
// Get screen metrics
NSRect screenFrame = [screen frame];
NSRect visibleFrame = [screen visibleFrame];
// Calculate the vertical position
CGFloat scaledOffset = offset * scaleFactor;
CGFloat windowY = visibleFrame.origin.y + visibleFrame.size.height - windowFrame.size.height - scaledOffset;
// Set the window's frame
windowFrame.origin.x = windowX;
windowFrame.origin.y = windowY;
[(NSWindow*)nsWindow setFrame:windowFrame display:YES animate:NO];
}

View File

@ -32,5 +32,6 @@
@end
void windowSetScreen(void* window, void* screen, int yOffset);
#endif /* WebviewWindowDelegate_h */

View File

@ -719,3 +719,29 @@ extern bool hasListeners(unsigned int);
// GENERATED EVENTS END
@end
void windowSetScreen(void* window, void* screen, int yOffset) {
WebviewWindow* nsWindow = (WebviewWindow*)window;
NSScreen* nsScreen = (NSScreen*)screen;
// Get current frame
NSRect frame = [nsWindow frame];
// Convert frame to screen coordinates
NSRect screenFrame = [nsScreen frame];
NSRect currentScreenFrame = [[nsWindow screen] frame];
// Calculate the menubar height for the target screen
NSRect visibleFrame = [nsScreen visibleFrame];
CGFloat menubarHeight = screenFrame.size.height - visibleFrame.size.height;
// Calculate the distance from the top of the current screen
CGFloat topOffset = currentScreenFrame.origin.y + currentScreenFrame.size.height - frame.origin.y;
// Position relative to new screen's top, accounting for menubar
frame.origin.x = screenFrame.origin.x + (frame.origin.x - currentScreenFrame.origin.x);
frame.origin.y = screenFrame.origin.y + screenFrame.size.height - topOffset - menubarHeight - yOffset;
// Set the frame which moves the window to the new screen
[nsWindow setFrame:frame display:YES];
}