Skip to content

fix: make CheckableItem reactive and serialize tray updates - #433

Merged
kdroidFilter merged 2 commits into
masterfrom
fix/checkable-item-reactivity
Aug 31, 2026
Merged

fix: make CheckableItem reactive and serialize tray updates#433
kdroidFilter merged 2 commits into
masterfrom
fix/checkable-item-reactivity

Conversation

@kdroidFilter

@kdroidFilter kdroidFilter commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes #432

Problem

  1. CheckableItem clicks captured checked at menu-build time. On Windows/macOS the click handler sent !capturedChecked forever; Linux's "live lookup" read the builder's own list, which is never updated (only the manager's copy is), so it behaved the same. After one toggle without a rebuild, further clicks were no-ops.
  2. Menu updates raced. NativeTray.updateComposable fire-and-forgot each update on trayScope, so a stale render/menu (e.g. still holding checked = true) could land after a fresher one whenever the parent recomposes frequently.

Fix

Live checked state (all platforms)

  • Each CheckableItem keeps a per-item AtomicBoolean toggled on click; onCheckedChange receives the toggle of the current value, not the captured one.
  • The native checkmark is patched immediately so the menu stays consistent even when no rebuild follows:
    • Windows: new WindowsTrayManager.updateMenuItemCheckedState(text, checked) — queued and applied on the tray thread (patches the stored item list, rebuilds the menu structs, nativeUpdateTray). The builder now receives the manager, so WindowsTrayInitializer creates/looks up the manager before building.
    • macOS: builder now actually calls the existing updateMenuItemCheckedState, which is now submenu-aware.
    • Linux: replaced the broken stale-list lookup with the holder; still uses nativeItemCheck/Uncheck.
  • Subsequent rebuilds from app state remain authoritative — controlled usage (Switch in a window toggling the same flag) reasserts whatever the app decides.

Serialized updates

  • updateComposable now enqueues into a Channel(CONFLATED) drained by a single worker: updates apply in submission order, one at a time, and a newer update replaces any queued-but-unapplied one. A stale update can no longer overwrite a fresh one.
  • dispose() cancels trayScope so an in-flight update can't re-create a disposed tray.

Testing

  • compileKotlinJvm (main + tray-app), ktlintCheck, detekt, jvmTest all pass.

CheckableItem clicks captured the checked value at menu-build time, so
repeated clicks without a rebuild kept sending the same toggle (Linux's
live lookup read the builder's own never-updated list, so it had the
same bug). Each checkable item now keeps a live AtomicBoolean toggled on
click, reports the new value to onCheckedChange, and patches the native
checkmark immediately via updateMenuItemCheckedState (added to
WindowsTrayManager, made submenu-aware on macOS).

NativeTray.updateComposable was fire-and-forget on trayScope, letting a
stale icon/menu update land after a fresher one when the parent
recomposes frequently. Updates now go through a conflated channel with
a single worker: applied in order, one at a time, superseded updates
dropped. dispose() cancels the scope so an in-flight update cannot
re-create a disposed tray.

Fixes #432
@kdroidFilter
kdroidFilter merged commit 54b3b0d into master Aug 31, 2026
4 checks passed
@kdroidFilter
kdroidFilter deleted the fix/checkable-item-reactivity branch August 31, 2026 23:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CheckableItem is not reactive: captured checked + concurrent menu updates

1 participant