Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
16 changes: 16 additions & 0 deletions internal/cocoa/cocoa_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()
Expand Down
64 changes: 64 additions & 0 deletions internal/cocoa/live_darwin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}