Skip to content

Commit 909ed71

Browse files
author
Ricardo Wagemaker
committed
Mac: Compile and Position
1 parent f4c124c commit 909ed71

13 files changed

Lines changed: 350 additions & 9 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Build artifacts
22
weatherwidget.exe
3+
weatherwidget-darwin-amd64
4+
weatherwidget-darwin-arm64
35
weatherwidget
46
build/
57

Makefile

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# Weather Widget Build Configuration
22
BINARY_NAME=weatherwidget
33
CMD_PATH=./cmd/weatherwidget/
4-
GO_CMD=/usr/bin/go
4+
ifeq ($(shell test -x /usr/local/go/bin/go && echo yes),yes)
5+
GO_CMD=/usr/local/go/bin/go
6+
else
7+
GO_CMD=/usr/bin/go
8+
endif
59

610
# Detect OS
711
ifeq ($(OS),Windows_NT)
@@ -14,16 +18,30 @@ else
1418
GOOS_VAL=$($(GO_CMD) env GOOS)
1519
endif
1620

17-
.PHONY: build test clean vet
21+
# Detect host OS for build target selection
22+
UNAME_S := $(shell uname -s)
23+
24+
.PHONY: build build-linux build-darwin test clean vet
25+
26+
ifeq ($(UNAME_S),Darwin)
27+
build: build-darwin
28+
else
29+
build: build-linux
30+
endif
31+
32+
build-linux:
33+
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(GO_CMD) build -v -ldflags="-s -w" -o $(BINARY_NAME)-linux-amd64 $(CMD_PATH)
34+
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 $(GO_CMD) build -v -ldflags="-s -w" -o $(BINARY_NAME)-linux-arm64 $(CMD_PATH)
1835

19-
build:
20-
GOOS=$(GOOS_VAL) $(GO_CMD) build -v -ldflags="$(LDFLAGS)" -o $(BINARY_NAME) $(CMD_PATH)
36+
build-darwin:
37+
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 $(GO_CMD) build -v -ldflags="-s -w" -o $(BINARY_NAME)-darwin-amd64 $(CMD_PATH)
38+
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 $(GO_CMD) build -v -ldflags="-s -w" -o $(BINARY_NAME)-darwin-arm64 $(CMD_PATH)
2139

2240
test:
2341
$(GO_CMD) test ./...
2442

2543
clean:
26-
rm -f $(BINARY_NAME)
44+
rm -f $(BINARY_NAME)-linux-amd64 $(BINARY_NAME)-linux-arm64 $(BINARY_NAME)-darwin-amd64 $(BINARY_NAME)-darwin-arm64
2745

2846
vet:
2947
$(GO_CMD) vet ./...

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.25.0
44

55
require (
66
fyne.io/fyne/v2 v2.7.3
7+
github.com/bradfitz/latlong v0.0.0-20170410180902-f3db6d0dff40
78
github.com/jackc/pgx/v5 v5.9.1
89
golang.org/x/sys v0.43.0
910
pgregory.net/rapid v1.2.0
@@ -12,7 +13,6 @@ require (
1213
require (
1314
fyne.io/systray v1.12.0 // indirect
1415
github.com/BurntSushi/toml v1.5.0 // indirect
15-
github.com/bradfitz/latlong v0.0.0-20170410180902-f3db6d0dff40 // indirect
1616
github.com/davecgh/go-spew v1.1.1 // indirect
1717
github.com/fredbi/uri v1.1.1 // indirect
1818
github.com/fsnotify/fsnotify v1.9.0 // indirect
@@ -31,6 +31,7 @@ require (
3131
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
3232
github.com/jackc/puddle/v2 v2.2.2 // indirect
3333
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
34+
github.com/jonas-p/go-shp v0.1.1 // indirect
3435
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
3536
github.com/kr/text v0.2.0 // indirect
3637
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCsc
3636
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
3737
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
3838
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
39+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
40+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
3941
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
4042
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
4143
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
@@ -52,6 +54,8 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
5254
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
5355
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
5456
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
57+
github.com/jonas-p/go-shp v0.1.1 h1:LY81nN67DBCz6VNFn2kS64CjmnDo9IP8rmSkTvhO9jE=
58+
github.com/jonas-p/go-shp v0.1.1/go.mod h1:MRIhyxDQ6VVp0oYeD7yPGr5RSTNScUFKCDsI5DR7PtI=
5559
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
5660
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
5761
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=

internal/ui/drag_darwin.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//go:build darwin
2+
3+
package ui
4+
5+
import (
6+
"log"
7+
"sync"
8+
"time"
9+
)
10+
11+
var (
12+
darwinDragMu sync.Mutex
13+
darwinDragStop chan struct{}
14+
darwinDragCallback func()
15+
darwinLastX int
16+
darwinLastY int
17+
darwinMovedByUs bool
18+
)
19+
20+
// enableWindowDrag enables drag-to-reposition detection on macOS.
21+
//
22+
// Since the widget uses a borderless splash window, there is no native
23+
// title-bar drag. We poll the window position and detect when the user
24+
// has moved the window (e.g. via scripting or accessibility tools).
25+
func enableWindowDrag(onDragEnd func()) {
26+
darwinDragMu.Lock()
27+
defer darwinDragMu.Unlock()
28+
29+
if darwinDragStop != nil {
30+
close(darwinDragStop)
31+
}
32+
33+
darwinDragCallback = onDragEnd
34+
darwinDragStop = make(chan struct{})
35+
darwinLastX, darwinLastY = getWindowPosition()
36+
37+
go darwinPollPosition(darwinDragStop)
38+
log.Println("macOS: drag position poller started")
39+
}
40+
41+
// notifyDarwinMoveByUs should be called before programmatic moves so the
42+
// poller doesn't treat them as user drags.
43+
func notifyDarwinMoveByUs() {
44+
darwinDragMu.Lock()
45+
darwinMovedByUs = true
46+
darwinDragMu.Unlock()
47+
}
48+
49+
// darwinPollPosition checks the window position periodically and fires the
50+
// callback when it detects a change not initiated by the application.
51+
func darwinPollPosition(stop chan struct{}) {
52+
ticker := time.NewTicker(500 * time.Millisecond)
53+
defer ticker.Stop()
54+
55+
for {
56+
select {
57+
case <-stop:
58+
return
59+
case <-ticker.C:
60+
x, y := getWindowPosition()
61+
62+
darwinDragMu.Lock()
63+
movedByUs := darwinMovedByUs
64+
darwinMovedByUs = false
65+
lastX, lastY := darwinLastX, darwinLastY
66+
cb := darwinDragCallback
67+
darwinDragMu.Unlock()
68+
69+
if movedByUs {
70+
darwinDragMu.Lock()
71+
darwinLastX = x
72+
darwinLastY = y
73+
darwinDragMu.Unlock()
74+
continue
75+
}
76+
77+
if x != lastX || y != lastY {
78+
darwinDragMu.Lock()
79+
darwinLastX = x
80+
darwinLastY = y
81+
darwinDragMu.Unlock()
82+
83+
if cb != nil {
84+
log.Printf("macOS: position changed from (%d,%d) to (%d,%d), saving", lastX, lastY, x, y)
85+
cb()
86+
}
87+
}
88+
}
89+
}
90+
}

internal/ui/drag_other.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !windows && !linux
1+
//go:build !windows && !linux && !darwin
22

33
package ui
44

internal/ui/manager.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type UIManager struct {
2929
// by applyToolWindowStyle using Win32 API calls.
3030
func NewUIManager(app fyne.App, lm *i18n.LocaleManager) *UIManager {
3131
w := createWidgetWindow(app, widgetTitle)
32+
initPlatformWindow(w)
3233

3334
return &UIManager{
3435
app: app,

internal/ui/platform_darwin.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//go:build darwin
2+
3+
package ui
4+
5+
import "fyne.io/fyne/v2"
6+
7+
// initPlatformWindow performs darwin-specific window initialisation.
8+
// It registers the window reference so the native NSWindow handle can be
9+
// retrieved later for positioning.
10+
func initPlatformWindow(w fyne.Window) {
11+
registerDarwinWindow(w)
12+
}

internal/ui/platform_other.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//go:build !darwin
2+
3+
package ui
4+
5+
import "fyne.io/fyne/v2"
6+
7+
// initPlatformWindow is a no-op on non-darwin platforms.
8+
func initPlatformWindow(_ fyne.Window) {}

internal/ui/win32_darwin.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
//go:build darwin
2+
3+
package ui
4+
5+
/*
6+
#cgo CFLAGS: -x objective-c
7+
#cgo LDFLAGS: -framework Cocoa
8+
9+
#import <Cocoa/Cocoa.h>
10+
11+
// moveNSWindowTo moves the given NSWindow to (x, y) using top-left origin.
12+
// macOS uses bottom-left origin, so we flip Y relative to the screen that
13+
// contains the window.
14+
static void moveNSWindowTo(uintptr_t winHandle, int x, int y) {
15+
dispatch_async(dispatch_get_main_queue(), ^{
16+
NSWindow *w = (__bridge NSWindow*)(void*)winHandle;
17+
NSScreen *screen = [w screen];
18+
if (screen == nil) screen = [NSScreen mainScreen];
19+
CGFloat screenHeight = [screen frame].size.height;
20+
CGFloat windowHeight = [w frame].size.height;
21+
CGFloat flippedY = screenHeight - (CGFloat)y - windowHeight;
22+
[w setFrameOrigin:NSMakePoint((CGFloat)x, flippedY)];
23+
});
24+
}
25+
26+
// getNSWindowPos returns the current top-left position of the NSWindow.
27+
static void getNSWindowPos(uintptr_t winHandle, int* outX, int* outY) {
28+
*outX = 0;
29+
*outY = 0;
30+
NSWindow *w = (__bridge NSWindow*)(void*)winHandle;
31+
NSScreen *screen = [w screen];
32+
if (screen == nil) screen = [NSScreen mainScreen];
33+
CGFloat screenHeight = [screen frame].size.height;
34+
NSRect frame = [w frame];
35+
*outX = (int)frame.origin.x;
36+
*outY = (int)(screenHeight - frame.origin.y - frame.size.height);
37+
}
38+
39+
// getMainScreenSize returns the main screen dimensions.
40+
static void getMainScreenSize(int* w, int* h) {
41+
NSScreen *screen = [NSScreen mainScreen];
42+
NSRect frame = [screen frame];
43+
*w = (int)frame.size.width;
44+
*h = (int)frame.size.height;
45+
}
46+
47+
// getScreenCount returns the number of screens.
48+
static int getScreenCount(void) {
49+
return (int)[[NSScreen screens] count];
50+
}
51+
52+
// getScreenBounds returns the visible frame (excluding menu bar/dock) for the
53+
// screen at the given index. Coordinates use top-left origin.
54+
static void getScreenBounds(int index, int* outX, int* outY, int* outW, int* outH) {
55+
NSArray *screens = [NSScreen screens];
56+
if (index < 0 || index >= (int)[screens count]) {
57+
index = 0;
58+
}
59+
NSScreen *screen = [screens objectAtIndex:index];
60+
NSRect visible = [screen visibleFrame];
61+
NSRect full = [screen frame];
62+
63+
*outX = (int)visible.origin.x;
64+
// Convert Y from bottom-left to top-left origin.
65+
*outY = (int)(full.size.height - visible.origin.y - visible.size.height);
66+
*outW = (int)visible.size.width;
67+
*outH = (int)visible.size.height;
68+
}
69+
*/
70+
import "C"
71+
72+
import (
73+
"log"
74+
"time"
75+
76+
"fyne.io/fyne/v2"
77+
"fyne.io/fyne/v2/driver"
78+
)
79+
80+
// darwinWidgetWindow holds a reference to the widget window so we can
81+
// retrieve its native NSWindow pointer without relying on title search.
82+
var darwinWidgetWindow fyne.Window
83+
84+
// registerDarwinWindow stores the widget window reference for later use
85+
// by moveWindow and getWindowPosition. Must be called once after the window
86+
// is created.
87+
func registerDarwinWindow(w fyne.Window) {
88+
darwinWidgetWindow = w
89+
}
90+
91+
// getNSWindowHandle returns the native NSWindow handle (uintptr) for the
92+
// stored widget window using Fyne's driver.NativeWindow interface.
93+
func getNSWindowHandle() C.uintptr_t {
94+
if darwinWidgetWindow == nil {
95+
log.Println("macOS: getNSWindowHandle — darwinWidgetWindow is nil")
96+
return 0
97+
}
98+
nativeWin, ok := darwinWidgetWindow.(driver.NativeWindow)
99+
if !ok {
100+
log.Printf("macOS: window type %T does not implement driver.NativeWindow", darwinWidgetWindow)
101+
return 0
102+
}
103+
var handle uintptr
104+
nativeWin.RunNative(func(ctx any) {
105+
// Fyne passes MacWindowContext by value, not pointer.
106+
if macCtx, ok := ctx.(driver.MacWindowContext); ok {
107+
handle = macCtx.NSWindow
108+
}
109+
})
110+
return C.uintptr_t(handle)
111+
}
112+
113+
// applyToolWindowStyle is a no-op on macOS.
114+
// Window decorations are already removed via CreateSplashWindow.
115+
func applyToolWindowStyle(_ string) {}
116+
117+
// getScreenSize returns the main screen dimensions on macOS.
118+
func getScreenSize() (int, int) {
119+
var w, h C.int
120+
C.getMainScreenSize(&w, &h)
121+
return int(w), int(h)
122+
}
123+
124+
// moveWindow repositions the widget window to the given screen coordinates.
125+
// It moves immediately and retries after short delays to handle the case where
126+
// Fyne/GLFW repositions the window after Show().
127+
func moveWindow(_ fyne.Window, x, y int) {
128+
notifyDarwinMoveByUs()
129+
handle := getNSWindowHandle()
130+
if handle == 0 {
131+
log.Println("macOS: moveWindow — could not get NSWindow handle")
132+
return
133+
}
134+
log.Printf("macOS: moveWindow x=%d y=%d", x, y)
135+
C.moveNSWindowTo(handle, C.int(x), C.int(y))
136+
go func(h C.uintptr_t, px, py C.int) {
137+
for _, delay := range []int{150, 400, 900} {
138+
time.Sleep(time.Duration(delay) * time.Millisecond)
139+
notifyDarwinMoveByUs()
140+
C.moveNSWindowTo(h, px, py)
141+
}
142+
}(handle, C.int(x), C.int(y))
143+
}
144+
145+
// getWindowPosition returns the current top-left position of the widget window.
146+
func getWindowPosition() (int, int) {
147+
handle := getNSWindowHandle()
148+
if handle == 0 {
149+
return 0, 0
150+
}
151+
var x, y C.int
152+
C.getNSWindowPos(handle, &x, &y)
153+
return int(x), int(y)
154+
}
155+
156+
// setWindowOpacity is a no-op on macOS for now.
157+
func setWindowOpacity(_ int) {}
158+
159+
// getMonitorCount returns the number of display monitors on macOS.
160+
func getMonitorCount() int {
161+
return int(C.getScreenCount())
162+
}
163+
164+
// getMonitorBounds returns the visible work-area rectangle for the monitor
165+
// at the given index. Coordinates use top-left origin.
166+
func getMonitorBounds(index int) (int, int, int, int) {
167+
var x, y, w, h C.int
168+
C.getScreenBounds(C.int(index), &x, &y, &w, &h)
169+
return int(x), int(y), int(w), int(h)
170+
}

0 commit comments

Comments
 (0)