package ui import ( "sync/atomic" "time" "git.wxccs.org/iceking2nd/winauth-go/internal/global" "git.wxccs.org/iceking2nd/winauth-go/internal/i18n" "git.wxccs.org/iceking2nd/winauth-go/internal/win32" ) // Tray menu IDs. Reserved range: 1..255 for system items, 256+ for // per-entry actions (none yet). 0 is the icon-click sentinel and MUST // NOT be used here. const ( trayCmdShow uint32 = 1 trayCmdHide uint32 = 2 trayCmdExit uint32 = 3 ) // trayRuntime owns the tray icon and the window subclass that intercepts // WM_CLOSE. Both are best-effort: failure to install either is logged and // the program continues without tray support. type trayRuntime struct { mgr *win32.TrayManager subclass *win32.WindowSubclass hwnd uintptr // hidden tracks the most recently *requested* visibility. The // Show/Hide menu labels are static today; flipping them would require // rebuilding the popup which is more work than it's worth. hidden atomic.Bool // exiting flips when the user picks the tray "Exit" entry or when the // process is shutting down. While set, the close hook stops swallowing // WM_CLOSE so the window can actually go down. exiting atomic.Bool } // installTrayRuntime resolves the Gio window's HWND by title (the only // public hook available in v0.7) then sets up the icon + close hook. // minimizeToTray controls the WM_CLOSE behaviour: when false we install // no hook and let the window close normally; the tray icon is still // useful for "Show/Hide" so we install it either way. onShow is invoked // after the window is restored so the caller can Invalidate. func installTrayRuntime(title string, minimizeToTray bool, onShow func()) *trayRuntime { const fn = "internal.ui.installTrayRuntime" // Gio creates the OS window from a goroutine inside app.Main(); on // the very first frame the HWND may not be discoverable yet. Poll // briefly so installation is reliable without making the UI wait. var hwnd uintptr for i := 0; i < 20; i++ { hwnd = win32.FindWindowByTitle(title) if hwnd != 0 { break } time.Sleep(50 * time.Millisecond) } if hwnd == 0 { global.Log.WithField("func", fn).Warn("could not find window HWND; tray disabled") return nil } tr := &trayRuntime{hwnd: hwnd} items := []win32.TrayItem{ {ID: trayCmdShow, Label: i18n.T("tray_show")}, {ID: trayCmdHide, Label: i18n.T("tray_hide")}, {ID: trayCmdExit, Label: i18n.T("tray_exit")}, } mgr, err := win32.NewTrayManager(i18n.T("app_title"), items) if err != nil { global.Log.WithField("func", fn).WithError(err).Warn("tray icon install failed") return nil } tr.mgr = mgr if minimizeToTray { tr.installCloseHook() } go func() { for ev := range mgr.Events() { switch ev.ItemID { case 0, trayCmdShow: win32.ShowWindow(hwnd, win32.SW_RESTORE) _ = win32.FocusWindow(hwnd) tr.hidden.Store(false) if onShow != nil { onShow() } case trayCmdHide: win32.ShowWindow(hwnd, win32.SW_HIDE) tr.hidden.Store(true) case trayCmdExit: tr.exiting.Store(true) // Route through WM_CLOSE so Gio sees a normal close and // shuts the event loop down — the hook (if installed) // reads tr.exiting and lets the message through. win32.PostCloseMessage(hwnd) } } }() return tr } func (tr *trayRuntime) installCloseHook() { const fn = "internal.ui.trayRuntime.installCloseHook" hwnd := tr.hwnd sc, err := win32.InstallCloseHook(hwnd, func() bool { if tr.exiting.Load() { return true } win32.ShowWindow(hwnd, win32.SW_HIDE) tr.hidden.Store(true) return false }) if err != nil { global.Log.WithField("func", fn).WithError(err).Warn("close hook install failed") return } tr.subclass = sc } // Stop tears down the tray + subclass. Idempotent. func (tr *trayRuntime) Stop() { if tr == nil { return } if tr.subclass != nil { tr.subclass.Remove() } if tr.mgr != nil { tr.mgr.Stop() } } // setMinimizeToTray hot-reloads the WM_CLOSE behaviour after the user // toggles the preference: install the hook if it wasn't there, or // remove it so the X button reverts to "actually quit." Safe to call // repeatedly; no-op when the requested state already matches. func (tr *trayRuntime) setMinimizeToTray(enable bool) { if tr == nil { return } if enable && tr.subclass == nil { tr.installCloseHook() return } if !enable && tr.subclass != nil { tr.subclass.Remove() tr.subclass = nil } }