diff --git a/go.mod b/go.mod index 2d58964..8750a4b 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/go-widgets/window go 1.26.4 require ( - github.com/go-macos/objc v0.9.1 + github.com/go-macos/objc v0.10.1 github.com/go-mswin/win32 v0.4.0 github.com/go-opentype/opentype v0.12.0 github.com/go-widgets/android v0.13.1 diff --git a/go.sum b/go.sum index 54ddaf5..bab9415 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-macos/appkit v0.5.0 h1:4LeIstgHaKcoTk4Eb0qFoHmUVa7arW1NNZAO8VLJtfQ= github.com/go-macos/appkit v0.5.0/go.mod h1:KQ+vIsZ2hBFniqFJ5Q4uQ4ylQX+MXPvJcMOzvBpJRdE= -github.com/go-macos/objc v0.9.1 h1:GGBoj8aMijBCYNPjGAYS4neeA3lWa8/JEIF1K5GswDo= -github.com/go-macos/objc v0.9.1/go.mod h1:00GJyieLzPcb65rPLVWNPPUCVm79nmmiEXUDsslRpOs= +github.com/go-macos/objc v0.10.1 h1:uXOc5w3Hp6FLB9RwmxbSY7ZyUKg37j/VOT2DjxPfmDU= +github.com/go-macos/objc v0.10.1/go.mod h1:68hzSUlhg5bFbg1kO0z4DM6xOk4B/IofuxdlyhHHtt8= github.com/go-macos/virtualdisplay v0.3.0 h1:OWo8bGaXWEGutoGHO/RV9qO5Lzfe1hFx2aXNocZ4bHI= github.com/go-macos/virtualdisplay v0.3.0/go.mod h1:68/38e3PlQ3cwK7du2wflPaNan+DQqKz7Hhi89uo6gg= github.com/go-mswin/win32 v0.4.0 h1:0JAvm3XNMxOyfYX1anbtAqxNvxvUA0DZ1SyAUinhMa0= diff --git a/internal/cocoa/cocoa_darwin.go b/internal/cocoa/cocoa_darwin.go index b2a37ef..77787f2 100644 --- a/internal/cocoa/cocoa_darwin.go +++ b/internal/cocoa/cocoa_darwin.go @@ -92,6 +92,8 @@ var ( selRelease = objc.RegisterName("release") selSetActivationPolicy = objc.RegisterName("setActivationPolicy:") selActivateIgnoring = objc.RegisterName("activateIgnoringOtherApps:") + selApplicationIconImage = objc.RegisterName("applicationIconImage") + selSetApplicationIcon = objc.RegisterName("setApplicationIconImage:") selNextEvent = objc.RegisterName("nextEventMatchingMask:untilDate:inMode:dequeue:") selSendEvent = objc.RegisterName("sendEvent:") selRun = objc.RegisterName("run") @@ -722,6 +724,20 @@ func NewWithOptions(o Options) (*Window, error) { policy = activationPolicyAccessory } app.Send(selSetActivationPolicy, policy) + // A caller that set applicationIconImage before ever becoming Regular + // (an accessory app's own startup, before its first window) has + // nothing live to land on: there is no Dock tile yet to update. Apple's + // own DTS guidance on exactly this Accessory<->Regular pattern is that + // the Dock's reaction to a runtime icon change is not instant and can + // revert unexpectedly across a policy transition — re-asserting the + // SAME image here, right as a real Dock tile is about to exist, is + // what actually lands it. A caller that never set one gets nil back + // and this is a harmless no-op re-assignment of nil to nil. + if policy == activationPolicyReg { + if icon := app.Send(selApplicationIconImage); icon != 0 { + app.Send(selSetApplicationIcon, icon) + } + } syncAppKitScreens(appKitScreenSyncTimeout) screen, err := o.resolveScreen() diff --git a/internal/cocoa/live_darwin_test.go b/internal/cocoa/live_darwin_test.go index 4d0aff3..55fc141 100644 --- a/internal/cocoa/live_darwin_test.go +++ b/internal/cocoa/live_darwin_test.go @@ -574,3 +574,67 @@ func TestLiveWindowNumberIsTheCGWindowID(t *testing.T) { } callOnMain(func() { win.Close() }) } + +// TestApplicationIconSurvivesAccessoryToRegular covers the regression this +// package's Regular-policy re-assert exists for: an accessory-policy +// process (a menu-bar app) that already set applicationIconImage at its +// own startup — before ever having a Dock tile to update — must still +// show that icon once a real window (this package's own NewWithOptions) +// switches it to Regular. Apple's own DTS guidance on this exact +// Accessory<->Regular pattern is that the Dock's reaction to a runtime +// icon change is not instant and can revert unexpectedly across a policy +// transition; live-verified on-device (a real Dock tile, screenshotted) +// that WITHOUT the re-assert this package now does, the tile showed a +// generic icon despite applicationIconImage correctly reporting the +// custom one throughout. +func TestApplicationIconSurvivesAccessoryToRegular(t *testing.T) { + if os.Getenv("WINDOW_COCOA_INTEGRATION") == "" { + t.Skip("set WINDOW_COCOA_INTEGRATION=1 to run the live macOS window proof") + } + var buf bytes.Buffer + if err := png.Encode(&buf, image.NewNRGBA(image.Rect(0, 0, 8, 8))); err != nil { + t.Fatalf("encoding a test PNG: %v", err) + } + + // applicationIconImage lives on the one shared NSApplication every test + // in this binary runs against — left set, it would silently change + // window-creation timing for every test declared after this one + // (alphabetically, that includes liverepaint_darwin_test.go), not just + // this test's own assertions. + t.Cleanup(func() { + callOnMain(func() { objc.App().Send(objc.RegisterName("setApplicationIconImage:"), objc.ID(0)) }) + }) + + var win *Window + var err error + var sizeBeforeOpen, sizeAfterOpen toolkit.Rect + callOnMain(func() { + app := objc.App() + app.Send(objc.RegisterName("setActivationPolicy:"), 1) // Accessory + app.Send(objc.RegisterName("finishLaunching")) + objc.SetApplicationIconImage(buf.Bytes()) + + icon := app.Send(objc.RegisterName("applicationIconImage")) + sz := objc.Send[nsSize](icon, objc.RegisterName("size")) + sizeBeforeOpen = toolkit.Rect{W: int(sz.W), H: int(sz.H)} + + win, err = New("dock icon proof", 200, 120, toolkit.DefaultDark()) + if err != nil { + return + } + icon = app.Send(objc.RegisterName("applicationIconImage")) + sz = objc.Send[nsSize](icon, objc.RegisterName("size")) + sizeAfterOpen = toolkit.Rect{W: int(sz.W), H: int(sz.H)} + }) + if err != nil { + t.Fatalf("New: %v", err) + } + defer callOnMain(func() { win.Close() }) + + if sizeBeforeOpen.W == 0 || sizeBeforeOpen.H == 0 { + t.Fatal("setup: applicationIconImage was not set before opening the window") + } + if sizeAfterOpen != sizeBeforeOpen { + t.Errorf("applicationIconImage changed across the Accessory->Regular transition: before=%+v after=%+v", sizeBeforeOpen, sizeAfterOpen) + } +}