package gio import ( "errors" "fmt" "softbox.local/core/application" ) var ErrIconRequestStale = errors.New("icon request no longer matches catalog") type iconFailureState struct { Identity application.IconEventIdentity Code application.IconFailureCode } // ExpectIcon records the newest request identity on the UI goroutine. func (shell *AppShell) ExpectIcon(identity application.IconEventIdentity) error { validated, err := application.NewIconEventIdentity( identity.RequestID, identity.AppID, identity.Reference, identity.DPI, ) if err != nil { return err } currentReference, exists := shell.iconReferences[validated.AppID] if !exists || currentReference != validated.Reference { return fmt.Errorf( "%w: app=%q reference=%q", ErrIconRequestStale, validated.AppID, validated.Reference, ) } if applied, exists := shell.iconApplied[validated.AppID]; !exists || !sameIconResource(applied, validated) { delete(shell.icons, validated.AppID) delete(shell.iconApplied, validated.AppID) } shell.iconRequests[validated.AppID] = validated delete(shell.iconFailures, validated.AppID) return nil } // CancelIconRequest invalidates the matching pending request on the UI goroutine. func (shell *AppShell) CancelIconRequest(appID, requestID string) bool { pending, exists := shell.iconRequests[appID] if !exists || pending.RequestID != requestID { return false } delete(shell.iconRequests, appID) delete(shell.iconFailures, appID) return true } // ApplyEvent validates and applies an application event on the UI goroutine. func (shell *AppShell) ApplyEvent(event application.Event) error { iconEvent, handled, err := application.ParseIconEvent(event) if err != nil || !handled { return err } identity := iconEvent.Identity pending, exists := shell.iconRequests[identity.AppID] if !exists || pending != identity { return nil } if shell.iconReferences[identity.AppID] != identity.Reference { return nil } delete(shell.iconRequests, identity.AppID) switch iconEvent.Type { case application.EventIconReady: shell.ApplyIcon(identity.AppID, iconEvent.Image) shell.iconApplied[identity.AppID] = identity case application.EventIconFailed: applied, hasApplied := shell.iconApplied[identity.AppID] if !hasApplied || !sameIconResource(applied, identity) { shell.ApplyIcon(identity.AppID, nil) } shell.iconFailures[identity.AppID] = iconFailureState{ Identity: identity, Code: iconEvent.ErrorCode, } } return nil } // IconFailure exposes the last failure for diagnostics without raw network data. func (shell *AppShell) IconFailure(appID string) (application.IconFailureCode, bool) { failure, exists := shell.iconFailures[appID] return failure.Code, exists } func canonicalIconReference(reference string) string { canonical, err := application.NormalizeIconReference(reference) if err != nil { return reference } return canonical } func sameIconResource( left application.IconEventIdentity, right application.IconEventIdentity, ) bool { return left.AppID == right.AppID && left.Reference == right.Reference && left.DPI == right.DPI }