diff --git a/crates/base/src/dock/detached.rs b/crates/base/src/dock/detached.rs index e7a5f9be9f..7ad7a7957e 100644 --- a/crates/base/src/dock/detached.rs +++ b/crates/base/src/dock/detached.rs @@ -170,9 +170,10 @@ impl DetachedDock { /// name is the stable identity the summon means. pub fn hosts_panel_named(&self, name: &str, cx: &App) -> bool { let area = self.area.read(cx); - area.placed_panels() - .iter() - .any(|(id, _)| area.panel(*id).is_some_and(|view| view.panel_name(cx) == name)) + area.placed_panels().iter().any(|(id, _)| { + area.panel(*id) + .is_some_and(|view| view.panel_name(cx) == name) + }) } /// The title the header row leads with: the host's window title when one @@ -225,9 +226,10 @@ impl DetachedDock { all_home = false; continue; } - let Some(detached) = self.area.update(cx, |area, cx| { - area.detach_panel(panel, window, cx) - }) else { + let Some(detached) = self + .area + .update(cx, |area, cx| area.detach_panel(panel, window, cx)) + else { continue; }; // The recorded origin beats `from` here — inside this window the @@ -344,11 +346,7 @@ impl DetachedWindows { .filter(|root| { !root .update_in(cx, |detached, pop_window, cx| { - let home = detached.redock_home_inner( - pop_window, - Some(&mut *window), - cx, - ); + let home = detached.redock_home_inner(pop_window, Some(&mut *window), cx); if home { pop_window.remove_window(); } @@ -450,11 +448,7 @@ impl Render for DetachedDock { // lifts to the accent wash like a tab affordance. .text_xs() .text_color(colors.muted_foreground) - .hover(|style| { - style - .bg(colors.accent) - .text_color(colors.foreground) - }) + .hover(|style| style.bg(colors.accent).text_color(colors.foreground)) .child( svg() .path("icons/panel-right.svg") @@ -578,9 +572,7 @@ mod tests { } #[gpui::test] - fn an_adopted_panel_reenters_its_own_area_without_a_removed_edge( - cx: &mut TestAppContext, - ) { + fn an_adopted_panel_reenters_its_own_area_without_a_removed_edge(cx: &mut TestAppContext) { let log = log_of(); let (area, alpha, _beta, cx) = two_groups(&log, cx); cx.run_until_parked(); @@ -732,9 +724,7 @@ mod tests { } #[gpui::test] - fn closing_the_pop_out_re_docks_to_the_region_the_panel_left( - cx: &mut TestAppContext, - ) { + fn closing_the_pop_out_re_docks_to_the_region_the_panel_left(cx: &mut TestAppContext) { let log = log_of(); let (area, _alpha, _beta, cx) = two_groups(&log, cx); let gamma = cx.update(|window, cx| { @@ -829,13 +819,12 @@ mod tests { cx.run_until_parked(); assert!( - cx.update(|_window, cx| DetachedWindows::live(cx)).is_empty(), + cx.update(|_window, cx| DetachedWindows::live(cx)) + .is_empty(), "a closed window drops out of the inventory without an unregister" ); assert!( - pop_window - .update(&mut cx.cx, |_, _, _| ()) - .is_err(), + pop_window.update(&mut cx.cx, |_, _, _| ()).is_err(), "and the window is really gone" ); } @@ -885,7 +874,8 @@ mod tests { assert!(panel_is_in(&area, alpha_id, Placement::Center, cx)); assert!(panel_is_in(&area, beta_id, Placement::Center, cx)); assert!( - cx.update(|_window, cx| DetachedWindows::live(cx)).is_empty(), + cx.update(|_window, cx| DetachedWindows::live(cx)) + .is_empty(), "the inventory is empty once the windows are gone" ); assert!( diff --git a/crates/component/src/dock/mod.rs b/crates/component/src/dock/mod.rs index 6e3c5f1072..c5d0724405 100644 --- a/crates/component/src/dock/mod.rs +++ b/crates/component/src/dock/mod.rs @@ -46,12 +46,12 @@ pub use gpui_base::dock::PanelView as BasePanelView; /// crate and handing it back with a different meaning is worse than dropping /// it. A skin reads a dock through [`DockContext`]. pub use gpui_base::dock::{ - AnyDrag, DockArea, DockAreaRenderer, DockAreaState, DockContext, DockEvent, DockLayout, - DockPlacement, DockSizing, DockState, DragPanel, DropIndicator, DropPlaceholderBounds, - DropTarget, EditResult, InsertTarget, NodeId, PaneNode, PaneRef, PaneTree, PanelBuildContext, - PanelBuilder, PanelEvent, PanelId, PanelInfo, PanelRegistry, PanelSource, PanelState, RootKind, - TabGroup, TabGroupConstraints, TabGroupContext, TabGroupEvent, TabGroupRenderer, - register_panel, + AnyDrag, CloseDetached, DetachedDock, DetachedPanel, DetachedWindows, DockArea, + DockAreaRenderer, DockAreaState, DockContext, DockEvent, DockLayout, DockPlacement, DockSizing, + DockState, DragPanel, DropIndicator, DropPlaceholderBounds, DropTarget, EditResult, + InsertTarget, NodeId, PaneNode, PaneRef, PaneTree, PanelBuildContext, PanelBuilder, PanelEvent, + PanelId, PanelInfo, PanelRegistry, PanelSource, PanelState, RootKind, TabGroup, + TabGroupConstraints, TabGroupContext, TabGroupEvent, TabGroupRenderer, register_panel, }; pub use panel::*; pub use tab_panel::DragPanelPreview; diff --git a/crates/component/src/input/input.rs b/crates/component/src/input/input.rs index 1b62fa59c3..b66089de63 100644 --- a/crates/component/src/input/input.rs +++ b/crates/component/src/input/input.rs @@ -1154,9 +1154,8 @@ mod tests { let mut textarea = None; let window = cx.update(|cx| { cx.open_window(Default::default(), |window, cx| { - let state = cx.new(|cx| { - crate::input::TextareaState::new(window, cx).submit_on_enter(true) - }); + let state = + cx.new(|cx| crate::input::TextareaState::new(window, cx).submit_on_enter(true)); textarea = Some(state.clone()); let probe = cx.new(|_| Probe { textarea: state }); cx.new(|cx| Root::new(probe, window, cx)) @@ -1170,11 +1169,17 @@ mod tests { let submitted = Arc::new(Mutex::new(Vec::new())); cx.update(|_, cx| { let submitted = submitted.clone(); - cx.subscribe(&textarea, move |state, event: &gpui_base::input::InputEvent, cx| { - if let gpui_base::input::InputEvent::PressEnter { shift: false, .. } = event { - submitted.lock().unwrap().push(state.read(cx).value().to_string()); - } - }) + cx.subscribe( + &textarea, + move |state, event: &gpui_base::input::InputEvent, cx| { + if let gpui_base::input::InputEvent::PressEnter { shift: false, .. } = event { + submitted + .lock() + .unwrap() + .push(state.read(cx).value().to_string()); + } + }, + ) .detach(); }); @@ -1191,7 +1196,9 @@ mod tests { // A value written through SetValue, then Return, submits it. let base: TextInputState = textarea.clone().into(); let value = gpui::accesskit::ActionData::Value("hello".into()); - cx.update(|window, cx| Input::handle_accessibility_set_value(&base, Some(&value), window, cx)); + cx.update(|window, cx| { + Input::handle_accessibility_set_value(&base, Some(&value), window, cx) + }); cx.simulate_keystrokes("enter"); cx.run_until_parked(); // The test platform also delivers Return's "\n" as text input, so @@ -1202,7 +1209,11 @@ mod tests { .iter() .map(|value| value.trim().to_string()) .collect(); - assert_eq!(submitted, vec!["hello".to_string()], "Return submits the draft"); + assert_eq!( + submitted, + vec!["hello".to_string()], + "Return submits the draft" + ); } // A disabled Input keeps its editor unfocused when its frame is focused. @@ -1290,14 +1301,20 @@ mod tests { draw(&mut cx); cx.update(|window, cx| window.focus_prev(cx)); draw(&mut cx); - assert!(cx.update(|window, _| before.is_focused(window)), "Shift+Tab leaves the input"); + assert!( + cx.update(|window, _| before.is_focused(window)), + "Shift+Tab leaves the input" + ); cx.update(|window, cx| input.focus_handle(cx).focus(window, cx)); draw(&mut cx); draw(&mut cx); cx.update(|window, cx| window.focus_next(cx)); draw(&mut cx); - assert!(cx.update(|window, _| after.is_focused(window)), "Tab leaves the input"); + assert!( + cx.update(|window, _| after.is_focused(window)), + "Tab leaves the input" + ); } #[test] diff --git a/crates/component/src/input/state.rs b/crates/component/src/input/state.rs index 641ae253f0..a5ae6d5902 100644 --- a/crates/component/src/input/state.rs +++ b/crates/component/src/input/state.rs @@ -326,10 +326,99 @@ pub(super) fn sync_focused_input_registry( let focused = state.focus_handle(cx).is_focused(window); Root::try_update(window, cx, |root, _, cx| { if focused { - root.focused_input = Some(state.clone()); + if root.focused_input.as_ref() != Some(&state) { + root.focused_input = Some(state.clone()); + cx.notify(); + } } else if root.focused_input.as_ref() == Some(&state) { root.focused_input = None; + cx.notify(); } - cx.notify(); }); } + +#[cfg(test)] +mod registry_tests { + use super::{AnyInputState, InputState, sync_focused_input_registry}; + use crate::Root; + use gpui::{ + AppContext as _, Context, Focusable as _, IntoElement, Render, TestAppContext, + VisualTestContext, Window, div, + }; + use std::{cell::Cell, rc::Rc}; + + struct EmptyView; + impl Render for EmptyView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div() + } + } + + #[gpui::test] + fn focused_input_registry_only_notifies_on_registration_changes(cx: &mut TestAppContext) { + cx.update(crate::init); + let mut inputs = None; + let root = cx.update(|cx| { + cx.open_window(Default::default(), |window, cx| { + inputs = Some(( + cx.new(|cx| InputState::new(window, cx)), + cx.new(|cx| InputState::new(window, cx)), + )); + let view = cx.new(|_| EmptyView); + cx.new(|cx| Root::new(view, window, cx)) + }) + .unwrap() + }); + let (first, second) = inputs.unwrap(); + let notifications = Rc::new(Cell::new(0)); + let observation = cx.update(|cx| { + let count = notifications.clone(); + let entity = root.update(cx, |_, _, cx| cx.entity()).unwrap(); + cx.observe(&entity, move |_, _| count.set(count.get() + 1)) + }); + let cx = &mut VisualTestContext::from_window(root.into(), cx); + cx.update(|window, cx| { + window.activate_window(); + first.focus_handle(cx).focus(window, cx); + sync_focused_input_registry(first.clone(), window, cx); + }); + assert_eq!(notifications.get(), 1); + cx.update(|window, cx| { + for _ in 0..4 { + sync_focused_input_registry(first.clone(), window, cx); + sync_focused_input_registry(second.clone(), window, cx); + } + }); + assert_eq!( + notifications.get(), + 1, + "unchanged render registration is silent" + ); + cx.update(|window, cx| { + second.focus_handle(cx).focus(window, cx); + sync_focused_input_registry(second.clone(), window, cx); + sync_focused_input_registry(first.clone(), window, cx); + assert_eq!( + Root::read(window, cx).focused_input, + Some(AnyInputState::from(second.clone())) + ); + }); + assert_eq!( + notifications.get(), + 2, + "an old input cannot clear the new focused input" + ); + cx.update(|window, cx| { + window.blur(cx); + sync_focused_input_registry(second.clone(), window, cx); + sync_focused_input_registry(second.clone(), window, cx); + assert_eq!(Root::read(window, cx).focused_input, None); + }); + assert_eq!( + notifications.get(), + 3, + "unregister exactly once when focus leaves" + ); + drop(observation); + } +} diff --git a/examples/ai_recipes/Cargo.lock b/examples/ai_recipes/Cargo.lock index 214a746615..a4d3e6fd15 100644 --- a/examples/ai_recipes/Cargo.lock +++ b/examples/ai_recipes/Cargo.lock @@ -2243,7 +2243,7 @@ dependencies = [ [[package]] name = "gpui-base" -version = "0.6.1" +version = "0.6.4" dependencies = [ "aho-corasick", "anyhow", @@ -2279,7 +2279,7 @@ dependencies = [ [[package]] name = "gpui-component" -version = "0.6.1" +version = "0.6.4" dependencies = [ "anyhow", "chrono", @@ -2321,7 +2321,7 @@ dependencies = [ [[package]] name = "gpui-component-macros" -version = "0.6.1" +version = "0.6.4" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2331,7 +2331,7 @@ dependencies = [ [[package]] name = "gpui-kit" -version = "0.6.1" +version = "0.6.4" dependencies = [ "gpui-base", "gpui-component", @@ -2343,7 +2343,7 @@ dependencies = [ [[package]] name = "gpui-kit-assets" -version = "0.6.1" +version = "0.6.4" dependencies = [ "anyhow", "gpui-pre",