@@ -2723,6 +2723,7 @@ impl Window {
27232723 self . next_frame . clear ( ) ;
27242724 let current_focus_path = self . rendered_frame . focus_path ( ) ;
27252725 let current_window_active = self . rendered_frame . window_active ;
2726+ let mut focus_before_listeners = self . focus ;
27262727
27272728 if previous_focus_path != current_focus_path
27282729 || previous_window_active != current_window_active
@@ -2731,6 +2732,11 @@ impl Window {
27312732 self . focus_lost_listeners
27322733 . clone ( )
27332734 . retain ( & ( ) , |listener| listener ( self , cx) ) ;
2735+ // The focus-lost fallback (e.g. a workspace refocusing itself) may target
2736+ // an element that isn't part of the element tree, in which case scheduling
2737+ // a redraw below would dispatch focus-lost again, looping forever. Only
2738+ // track focus movement caused by the focus listeners.
2739+ focus_before_listeners = self . focus ;
27342740 }
27352741
27362742 let event = WindowFocusEvent {
@@ -2755,6 +2761,13 @@ impl Window {
27552761 self . reset_cursor_style ( cx) ;
27562762 self . refreshing = false ;
27572763 self . invalidator . set_phase ( DrawPhase :: None ) ;
2764+ // Focus listeners may move focus (e.g. a dock forwarding focus to its active
2765+ // panel). `Window::focus` suppresses `refresh` while a draw is in progress, so
2766+ // schedule another frame here to render the new focus state and dispatch the
2767+ // resulting focus events.
2768+ if self . focus != focus_before_listeners {
2769+ self . refresh ( ) ;
2770+ }
27582771 self . needs_present . set ( true ) ;
27592772
27602773 if let Some ( draw_start) = draw_started_at {
@@ -6380,8 +6393,9 @@ pub fn outline(
63806393#[ cfg( test) ]
63816394mod tests {
63826395 use crate :: {
6383- AppContext as _, Bounds , Context , IntoElement , ParentElement as _, Pixels , Render ,
6384- Styled as _, TestAppContext , Window , canvas, div, px, size,
6396+ AppContext as _, Bounds , Context , FocusHandle , InteractiveElement as _, IntoElement ,
6397+ ParentElement as _, Pixels , Render , Styled as _, TestAppContext , Window , canvas, div, px,
6398+ size,
63856399 } ;
63866400 use std:: { cell:: Cell , rc:: Rc } ;
63876401
@@ -6449,4 +6463,63 @@ mod tests {
64496463
64506464 assert_eq ! ( child_bounds. get( ) . size, size( px( 300. ) , px( 200. ) ) ) ;
64516465 }
6466+
6467+ struct FocusForwarder {
6468+ a : FocusHandle ,
6469+ b : FocusHandle ,
6470+ }
6471+
6472+ impl Render for FocusForwarder {
6473+ fn render ( & mut self , _: & mut Window , _: & mut Context < Self > ) -> impl IntoElement {
6474+ div ( )
6475+ . size_full ( )
6476+ . child ( div ( ) . w ( px ( 50. ) ) . h ( px ( 50. ) ) . track_focus ( & self . a ) )
6477+ . child ( div ( ) . w ( px ( 50. ) ) . h ( px ( 50. ) ) . track_focus ( & self . b ) )
6478+ }
6479+ }
6480+
6481+ /// When a focus listener moves focus again (e.g. a dock forwarding focus to its
6482+ /// active panel), the resulting focus events must be dispatched without waiting
6483+ /// for an unrelated redraw of the window.
6484+ #[ gpui:: test]
6485+ fn test_focus_moved_by_focus_listener_is_dispatched ( cx : & mut TestAppContext ) {
6486+ let b_focus_count = Rc :: new ( Cell :: new ( 0 ) ) ;
6487+ let window = cx. add_window ( {
6488+ let b_focus_count = b_focus_count. clone ( ) ;
6489+ move |window, cx| {
6490+ let a = cx. focus_handle ( ) ;
6491+ let b = cx. focus_handle ( ) ;
6492+ cx. on_focus ( & a, window, |this : & mut FocusForwarder , window, cx| {
6493+ let b = this. b . clone ( ) ;
6494+ window. focus ( & b, cx) ;
6495+ } )
6496+ . detach ( ) ;
6497+ cx. on_focus ( & b, window, move |_, _, _| {
6498+ b_focus_count. set ( b_focus_count. get ( ) + 1 ) ;
6499+ } )
6500+ . detach ( ) ;
6501+ FocusForwarder { a, b }
6502+ }
6503+ } ) ;
6504+
6505+ window
6506+ . update ( cx, |_, window, _| window. activate_window ( ) )
6507+ . unwrap ( ) ;
6508+ cx. executor ( ) . run_until_parked ( ) ;
6509+
6510+ window
6511+ . update ( cx, |this, window, cx| {
6512+ let a = this. a . clone ( ) ;
6513+ window. focus ( & a, cx) ;
6514+ } )
6515+ . unwrap ( ) ;
6516+ cx. executor ( ) . run_until_parked ( ) ;
6517+
6518+ window
6519+ . update ( cx, |this, window, _| {
6520+ assert ! ( this. b. is_focused( window) ) ;
6521+ } )
6522+ . unwrap ( ) ;
6523+ assert_eq ! ( b_focus_count. get( ) , 1 ) ;
6524+ }
64526525}
0 commit comments