Skip to content

Variant C: Add on_blur_by_user and migrate blur-cancelling controls onto it - #3

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

Variant C: Add on_blur_by_user and migrate blur-cancelling controls onto it#3
butvinm wants to merge 1 commit into
fix-layout-switch-cancels-renamefrom
focus-variant-c-helper

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 A and variant B PRs.

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

Design: leave GPUI alone, make the correct pattern the easy one

Two subscription constructors on Context<T>, placed immediately beside the two methods that carry the trap (crates/gpui/src/app/context.rs):

pub fn on_blur_by_user(&mut self, handle: &FocusHandle, window: &mut Window,
    listener: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static) -> Subscription

pub fn on_focus_out_by_user(&mut self, handle: &FocusHandle, window: &mut Window,
    listener: impl FnMut(&mut T, FocusOutEvent, &mut Window, &mut Context<T>) + 'static) -> Subscription

Each is a three-line wrapper gating the existing constructor on window.is_window_active(). GPUI's event semantics are entirely untouched, so this is the lowest-risk option to land.

Migrated: project panel, collab panel, Go to Line, picker (both head kinds), terminal tab rename, and modal_layer. ErasedEditorEvent::Blurred was deleted once it had no consumer left - the one place the convention became an actual constraint.

Diffstat

13 files changed, 368 insertions, 65 deletions.

Verification

Findings worth more than the code

  • modal_layer is not the good prior art it appears to be. It has no activation awareness and only looks correct because dismiss_on_focus_lost is usually false. file_finder's on_before_dismiss does set it, so that was a real latent bug, fixed here.
  • The base branch's regression test was vacuous - this is what surfaced it. Fixed properly in the separate fix-vacuous-deactivation-test PR.

Honest weaknesses

  • This is a convention, not a constraint. cx.on_blur, cx.on_focus_out, and EditorEvent::Blurred all still exist and still misbehave; 12 other Blurred subscribers are untouched. Nothing stops the next contributor from hand-rolling the buggy version. It does not close the bug class for future UI and cannot without removing or renaming the dangerous API outright.
  • Callback ordering changed in the five editor migrations: they moved from EditorEvent::Blurred subscribers (which run in the effect flush) to focus listeners (synchronous inside Window::draw). For the picker, cancel/DismissEvent now runs before ModalLayer's focus-out listener rather than after. Wants hands-on exercise of the command palette, file finder, and file-finder submenu.
  • Two new GPUI tests were added whose scaffolding (two track_focus divs producing the expected focus paths) is the kind of thing that is easy to get wrong on paper.

Full API rationale, migration-by-migration notes on how cleanly each call site fit, and all judgment calls are in VARIANT-NOTES.md at the repo root of this branch.

Release Notes:

  • N/A

Window deactivation blanks the window's focus path, so every blur
listener observes something indistinguishable from the user moving
focus away. Controls that cancel, dismiss or commit in-progress input
therefore destroy user input when the window loses OS focus, which on
Wayland happens on a mere keyboard layout switch. This has been patched
one control at a time with opt-in window.is_window_active() guards
(zed-industries#41320, zed-industries#46866, zed-industries#47044, zed-industries#39286).

Add Context::on_blur_by_user and Context::on_focus_out_by_user, which
fire only while the window is still active, and migrate the project
panel rename editor, the collab panel channel rename editor, Go to Line,
the picker head, the terminal tab rename editor and the modal layer onto
them. GPUI's event semantics are unchanged; on_blur, on_focus_out and
WindowFocusEvent behave exactly as before, and the new constructors are
thin wrappers that point at each other from their doc comments.

Drop ErasedEditorEvent::Blurred, which had no consumer left once the
picker subscribed to the erased editor's focus handle instead.

test_rename_survives_window_deactivation could not fail before: test
windows start inactive, so deactivate_window() was a no-op and no blur
was ever emitted. Activate the window and assert the preconditions.
@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-c-helper 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