Skip to content

Variant A: Stop synthesizing focus events from window activation - #1

Closed
butvinm wants to merge 1 commit into
fix-layout-switch-cancels-renamefrom
focus-variant-a-subtractive
Closed

Variant A: Stop synthesizing focus events from window activation#1
butvinm wants to merge 1 commit into
fix-layout-switch-cancels-renamefrom
focus-variant-a-subtractive

Conversation

@butvinm

@butvinm butvinm commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Review-only PR in the fork. Do not merge upstream from here. One of three competing designs for the same problem, built so they can be compared side by side. See also the variant B and variant C PRs.

Base is fix-layout-switch-cancels-rename (merged upstream as zed-industries#61852), so the diff shows only this design.

Design: make window activation stop producing focus events

Window activation and element focus become two genuinely independent signals. The core change is about 8 net-removed lines in crates/gpui/src/window.rs: the focus paths are no longer blanked when the window's active state changes, and the previous_window_active != current_window_active disjunct is deleted entirely. With the paths unblanked, an activation-only event is provably a no-op for all six focus-listener registrations, so Frame::window_active loses its last reader and is removed.

Consequence: every dismiss/cancel handler becomes correct with no code at all, including handlers written in the future. Anything that genuinely wants the deactivation signal subscribes via cx.observe_window_activation.

Three consumers wanted the old behavior and were migrated: the editor's transient UI (hover popover, completion menu, edit prediction), the context menu (preserving zed-industries#46866's deliberate dismiss-on-window-blur), and the terminal's xterm focus reporting plus hollow cursor. Four now-redundant is_window_active() guards were deleted, including the ones added by zed-industries#61852.

Diffstat

11 files changed, 380 insertions, 111 deletions.

Verification

Nothing was compiled locally (this machine cannot build Zed); all verification is fork CI.

Honest weaknesses

  • Silent blast radius. Roughly 60 focus-subscription sites change behavior at once, and a handler that wanted the old behavior produces no compiler error - it just stops running. This is structural to the subtractive approach.
  • Reactivation stops firing focus events too. Editor::handle_focus, Pane::focus_in, and Dock zoom bookkeeping no longer run on window reactivation. One concrete loss: buffer.finalize_last_transaction no longer creates an undo boundary at an app switch.
  • Deliberate regressions: vim's block cursor no longer hollows on window blur, the which-key overlay no longer dismisses, and selections keep broadcasting to collaborators while you are in another app.
  • Unresolved macOS risk. Re-adding context-menu dismissal on deactivation re-lands the shape of ui: Dismiss context menus when window loses focus zed-industries/zed#46866, which was reverted in ade8749537 while fixing macOS emoji-palette typing. The claim that the emoji bug came from Preserve and restore focus across window activation cycles zed-industries/zed#47044's focus save/restore rather than ui: Dismiss context menus when window loses focus zed-industries/zed#46866 could not be verified without a Mac.
  • The new GPUI test test_window_activation_does_not_produce_focus_events (crates/gpui/src/key_dispatch.rs) compiled but did not run - the fork workflow only executes project_panel and go_to_line tests.

Full per-file rationale, all judgment calls, and the platform-layer comparison are in VARIANT-NOTES.md at the repo root of this branch.

Release Notes:

  • N/A

Window activation and element focus were conflated: on deactivation draw()
blanked the focus paths of WindowFocusEvent, so every focused element observed
something indistinguishable from "the user moved focus away". Controls that
cancel, dismiss, or commit on blur therefore destroyed user input whenever the
window lost OS focus, which on Wayland happens merely by switching keyboard
layout. Each occurrence has been patched one control at a time with an opt-in
window.is_window_active() guard (zed-industries#41320, zed-industries#46866, zed-industries#47044, zed-industries#39286).

Make the two signals independent instead. Focus events now fire only when the
focused element inside the window changes; deactivation and reactivation emit
nothing, which matches window.focus never being cleared on deactivation.
Consumers that genuinely wanted the deactivation blur move to the existing
observe_window_activation API:

- Editor: hover popover, completion menu and active edit prediction are hidden
  from the subscription that already handles cursor blink.
- ContextMenu: dismissal on window blur, previously a side effect of the
  synthesized blur, is now requested explicitly.
- TerminalView: xterm focus reporting and the hollow cursor keep tracking
  window activation.

The is_window_active() guards in the picker, project panel, collab panel and
Go to Line are now redundant and are removed.

Frame::window_active has no remaining readers and is deleted.
@butvinm

butvinm commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #5 (variant D). Closing to keep the field clear; the diff stays viewable here.

@butvinm butvinm closed this Aug 3, 2026
@butvinm
butvinm deleted the focus-variant-a-subtractive branch August 3, 2026 01:59
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.

1 participant