Skip to content

fix(v3/macos): apply setMenuItemChecked state synchronously#5165

Open
RALIST wants to merge 3 commits intowailsapp:masterfrom
RALIST:fix/v3-menuitem-checked-race
Open

fix(v3/macos): apply setMenuItemChecked state synchronously#5165
RALIST wants to merge 3 commits intowailsapp:masterfrom
RALIST:fix/v3-menuitem-checked-race

Conversation

@RALIST
Copy link
Copy Markdown
Contributor

@RALIST RALIST commented Apr 17, 2026

Description

setMenuItemChecked() (in v3/pkg/application/menuitem_darwin.go) wraps the
NSMenuItem.state assignment inside dispatch_async(dispatch_get_main_queue(), ...).
When the function is invoked from the main thread — which is the common path,
since MenuItem.handleClick runs on the menu's action thread — the assignment
is enqueued for the next runloop turn. If the user reopens the menu before
that turn fires, the menu renders with the previous checked state.

This makes context menus with checkbox-style items (e.g. Bold/Italic toggles)
visibly out of sync until the next idle tick. Reproduction steps and full
analysis are in the issue.

This PR applies the maintainer's suggested fix from the issue thread:

  • Short-circuit on the main thread and assign NSMenuItem.state directly
    (avoids dispatch_sync-from-main deadlock).
  • Fall back to dispatch_sync from background threads, so the state is
    applied before we return.

The same dispatch_async pattern still exists in the sibling setters
(setMenuItemLabel, setMenuItemDisabled, setMenuItemHidden,
setMenuItemTooltip). They have the same theoretical race, but the issue and
the maintainer comment scope to setMenuItemChecked, so this PR is kept
surgical. Happy to extend the fix in a follow-up if desired.

Fixes #5002

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

  • Built ./pkg/application/... on macOS.

  • Ran the menuitem unit tests: go test -run "TestMenuItem|MenuItem" ./pkg/application/ — all pass.

  • Manually verified in a downstream app (Writer Studio) that uses checkbox-style
    context menu items: after toggling, reopening the menu now shows the new
    state immediately, without the previous Go-side workaround.

  • Windows

  • macOS

  • Linux

Test Configuration

macOS 26.3.1 (darwin/arm64), Go toolchain matching repo go.mod.

Checklist:

  • I have updated v3/UNRELEASED_CHANGELOG.md with details of this PR
  • My code follows the general coding style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

A targeted unit test for this race would require driving the AppKit menu
runloop from a Go test, which the package does not currently set up. Happy
to add a manual test harness under v3/test/manual/ if that would help.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed a macOS issue where menu items could show stale checked/visual state when reopened quickly. Related menu visuals (labels, enabled/disabled state, visibility, and tooltips) now update immediately so menus reflect changes reliably when reopened.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 14654c7c-632f-4ffc-8f9d-3825231cdb4d

📥 Commits

Reviewing files that changed from the base of the PR and between e1e73ca and 784eeac.

📒 Files selected for processing (2)
  • v3/UNRELEASED_CHANGELOG.md
  • v3/pkg/application/menuitem_darwin.go
✅ Files skipped from review due to trivial changes (1)
  • v3/UNRELEASED_CHANGELOG.md

Walkthrough

Updates macOS menu item mutators to apply changes synchronously on the Cocoa main thread (short‑circuiting when already on main thread and using dispatch_sync otherwise) to eliminate a race where menu visuals could show stale state.

Changes

Cohort / File(s) Summary
Changelog Entry
v3/UNRELEASED_CHANGELOG.md
Adds a "Fixed" entry describing a macOS menus bug where menu items could show stale checked/visual state when reopened quickly; documents that mutators are now applied synchronously on the main thread.
Menu Item State Synchronization
v3/pkg/application/menuitem_darwin.go
Replaces unconditional dispatch_async with a main-thread short‑circuit + dispatch_sync pattern for setMenuItemChecked, setMenuItemLabel, setMenuItemDisabled, setMenuItemHidden, and setMenuItemTooltip so updates are applied immediately and avoid the async race.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

MacOS, Bug, v3-alpha, Documentation

Suggested reviewers

  • leaanthony

Poem

🐰 I hopped through code with nimble paws,
I chased a race that broke the laws.
Now menus show the state that's true,
No stale ticks hiding from view.
Quick clicks rejoice — the rabbit chews the clue! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the primary change: applying setMenuItemChecked state synchronously instead of asynchronously on macOS.
Description check ✅ Passed The description follows the template structure with clear sections (Description, Type of change, Testing, Checklist) and addresses the issue comprehensively with reproduction steps and fix rationale.
Linked Issues check ✅ Passed The PR directly addresses issue #5002 by fixing the dispatch_async race condition in setMenuItemChecked by short-circuiting on main thread and using dispatch_sync for background threads, ensuring synchronous state updates.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing setMenuItemChecked and closely related menu item setters (label, disabled, hidden, tooltip) with the same dispatch pattern, which aligns with the linked issue's scope of checkbox state synchronization.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.11.4)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
v3/pkg/application/menuitem_darwin.go (1)

47-93: Optional: sibling setters retain the same dispatch_async pattern.

setMenuItemLabel, setMenuItemDisabled, setMenuItemHidden, and setMenuItemTooltip still enqueue to the main queue via dispatch_async, so in principle they can exhibit the same "update applied after menu render on rapid reopen" behavior as the bug fixed here. The PR description explicitly scopes this change to setMenuItemChecked because only the checked-state race was user-reported, which is fine. Consider a follow-up to apply the same isMainThread + dispatch_sync pattern to these setters (or factor out a small helper) if similar staleness reports emerge for label/disabled/hidden/tooltip updates on context menus.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/pkg/application/menuitem_darwin.go` around lines 47 - 93, The four setters
(setMenuItemLabel, setMenuItemDisabled, setMenuItemHidden, setMenuItemTooltip)
currently use dispatch_async to update UI and can suffer the same stale-update
race as setMenuItemChecked; change them to perform the update immediately if
already on the main thread, otherwise use dispatch_sync to dispatch to the main
queue so updates apply before the menu is rendered. Implement this by adding the
isMainThread check (or a small helper like performOnMainThreadSync) and applying
the existing body (casting to MenuItem, setting title/enabled/hidden/toolTip and
freeing label/tooltip) directly when on main, or via dispatch_sync when not.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@v3/pkg/application/menuitem_darwin.go`:
- Around line 47-93: The four setters (setMenuItemLabel, setMenuItemDisabled,
setMenuItemHidden, setMenuItemTooltip) currently use dispatch_async to update UI
and can suffer the same stale-update race as setMenuItemChecked; change them to
perform the update immediately if already on the main thread, otherwise use
dispatch_sync to dispatch to the main queue so updates apply before the menu is
rendered. Implement this by adding the isMainThread check (or a small helper
like performOnMainThreadSync) and applying the existing body (casting to
MenuItem, setting title/enabled/hidden/toolTip and freeing label/tooltip)
directly when on main, or via dispatch_sync when not.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a504dd10-ff2c-4cc3-aaa9-3957f9b76bf5

📥 Commits

Reviewing files that changed from the base of the PR and between 5d3993c and e1e73ca.

📒 Files selected for processing (2)
  • v3/UNRELEASED_CHANGELOG.md
  • v3/pkg/application/menuitem_darwin.go

Copy link
Copy Markdown
Member

@leaanthony leaanthony left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I'm ok with extending this to the other menu item types too!

ralist added 2 commits April 19, 2026 09:57
setMenuItemChecked() wrapped the NSMenuItem.state assignment in
dispatch_async(dispatch_get_main_queue(), ...). When invoked from the
main thread (e.g. handleClick reacting to a user toggle), the block
runs on the next runloop turn, which can land *after* the menu has
already been rendered if the user reopens the menu quickly. The result
is a stale checkmark on the next open.

Apply the state directly when called on the main thread, otherwise fall
back to dispatch_sync so the assignment lands before we return. This
matches the maintainer's suggested fix in the issue and avoids the
deadlock of dispatch_sync-from-main.

Fixes wailsapp#5002
…tem setters

Extend the isMainThread/dispatch_sync pattern from setMenuItemChecked to the
other NSMenuItem mutators that shared the same dispatch_async race:

  - setMenuItemLabel
  - setMenuItemDisabled
  - setMenuItemHidden
  - setMenuItemTooltip

Each had the same failure mode as setMenuItemChecked: a rapid menu reopen
could render the previous title/enabled/hidden/tooltip state because the
dispatch_async block had not yet run. Apply the change directly on the main
thread and fall back to dispatch_sync off-thread so the mutation lands
before the caller returns.

Per maintainer approval on wailsapp#5165.
@RALIST RALIST force-pushed the fix/v3-menuitem-checked-race branch from e1e73ca to 784eeac Compare April 19, 2026 06:01
@RALIST RALIST requested a review from leaanthony April 19, 2026 06:06
@leaanthony leaanthony changed the base branch from v3-alpha to master April 29, 2026 13:08
@leaanthony
Copy link
Copy Markdown
Member

Triaged by Wails PR Reviewer

This PR has been reviewed and accepted. A test sub-issue has been created for macOS.

Head Ref OID: 784eeacbdcd4b8ff1d791230cee5be2d4dd0666e


This comment serves as a signature that this PR has been triaged. Future runs will skip this PR based on the headRefOid.

@leaanthony
Copy link
Copy Markdown
Member

Tested on macOS — verdict: ✓ pass

  • Commit: 784eeacbdcd4b8ff1d791230cee5be2d4dd0666e
  • Platform: macOS 26.4.1 (arm64)
  • Build: 7.8s — successful (go install ./v3/cmd/wails3)
  • Unit tests: 17 packages pass, 4 fail (all pre-existing failures in internal/commands and internal/generator, unrelated to this PR)
  • Smoke: v3/examples/menu and v3/examples/contextmenus compile and link cleanly

Notes:

The PR extends the dispatch_asyncdispatch_sync fix beyond setMenuItemChecked (as named in the title) to the 4 sibling functions setMenuItemLabel, setMenuItemDisabled, setMenuItemHidden, and setMenuItemTooltip. The [NSThread isMainThread] guard pattern is correct throughout — direct assignment when already on the main thread, dispatch_sync from background threads — no deadlock risk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v3] setMenuItemChecked() applies state after menu is shown on macOS (dispatch_async race)

2 participants