Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions VARIANT-NOTES.md

Large diffs are not rendered by default.

25 changes: 10 additions & 15 deletions crates/collab_ui/src/collab_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,23 +350,18 @@ impl CollabPanel {

let channel_name_editor = cx.new(|cx| Editor::single_line(window, cx));

cx.subscribe_in(
&channel_name_editor,
cx.on_blur_by_user(
&channel_name_editor.focus_handle(cx),
window,
|this: &mut Self, _, event, window, cx| {
if let editor::EditorEvent::Blurred = event {
if !window.is_window_active() {
return;
}
if let Some(state) = &this.channel_editing_state
&& state.pending_name().is_some()
{
return;
}
this.take_editing_state(window, cx);
this.update_entries(false, cx);
cx.notify();
|this: &mut Self, window, cx| {
if let Some(state) = &this.channel_editing_state
&& state.pending_name().is_some()
{
return;
}
this.take_editing_state(window, cx);
this.update_entries(false, cx);
cx.notify();
},
)
.detach();
Expand Down
1 change: 0 additions & 1 deletion crates/editor/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28743,7 +28743,6 @@ impl ui_input::ErasedEditor for ErasedEditorImpl {
window.subscribe(&self.0, cx, move |_, event: &EditorEvent, window, cx| {
let event = match event {
EditorEvent::BufferEdited => ui_input::ErasedEditorEvent::BufferEdited,
EditorEvent::Blurred => ui_input::ErasedEditorEvent::Blurred,
_ => return,
};
(callback)(event, window, cx);
Expand Down
26 changes: 15 additions & 11 deletions crates/go_to_line/src/go_to_line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ impl GoToLine {
editor
});
let line_editor_change = cx.subscribe_in(&line_editor, window, Self::on_line_editor_event);
let line_editor_blur =
cx.on_blur_by_user(&line_editor.focus_handle(cx), window, Self::on_line_editor_blur);

let current_text = format!(
"Current Line: {} of {} (column {})",
Expand All @@ -148,7 +150,11 @@ impl GoToLine {
current_text: current_text.into(),
prev_scroll_position: Some(scroll_position),
current_line: line,
_subscriptions: vec![line_editor_change, cx.on_release_in(window, Self::release)],
_subscriptions: vec![
line_editor_change,
line_editor_blur,
cx.on_release_in(window, Self::release),
],
}
}

Expand All @@ -167,21 +173,19 @@ impl GoToLine {
&mut self,
_: &Entity<Editor>,
event: &editor::EditorEvent,
window: &mut Window,
_window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
editor::EditorEvent::Blurred => {
if window.is_window_active() {
self.prev_scroll_position.take();
cx.emit(DismissEvent)
}
}
editor::EditorEvent::BufferEdited => self.highlight_current_line(cx),
_ => {}
if let editor::EditorEvent::BufferEdited = event {
self.highlight_current_line(cx)
}
}

fn on_line_editor_blur(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.prev_scroll_position.take();
cx.emit(DismissEvent)
}

fn highlight_current_line(&mut self, cx: &mut Context<Self>) {
self.active_editor.update(cx, |editor, cx| {
editor.clear_row_highlights::<GoToLineRowHighlights>();
Expand Down
138 changes: 138 additions & 0 deletions crates/gpui/src/app/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,11 @@ impl<'a, T: 'static> Context<'a, T> {
}

/// Register a listener to be called when the given focus handle loses focus.
///
/// This also fires when the window is deactivated by the operating system, because window
/// deactivation blanks the window's focus path. Controls that cancel, dismiss or commit
/// in-progress user input on blur want [`Context::on_blur_by_user`] instead.
///
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_blur(
&mut self,
Expand All @@ -619,6 +624,33 @@ impl<'a, T: 'static> Context<'a, T> {
subscription
}

/// Register a listener to be called when the given focus handle loses focus while the
/// window is still active, i.e. because focus moved somewhere else inside the window.
///
/// Prefer this over [`Context::on_blur`] for any control that cancels, dismisses or commits
/// in-progress user input, such as a rename field or a modal. The operating system
/// deactivating the window (switching apps, or on Wayland merely switching keyboard layout)
/// blanks the window's focus path and is otherwise indistinguishable from the user moving
/// focus away, which would destroy what the user typed.
///
/// A control that additionally wants to react to the window going away should say so
/// explicitly with [`Context::observe_window_activation`], so that the two facts stay
/// separate.
///
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_blur_by_user(
&mut self,
handle: &FocusHandle,
window: &mut Window,
mut listener: impl FnMut(&mut T, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
self.on_blur(handle, window, move |view, window, cx| {
if window.is_window_active() {
listener(view, window, cx)
}
})
}

/// Register a listener to be called when nothing in the window has focus.
/// This typically happens when the node that was focused is removed from the tree,
/// and this callback lets you chose a default place to restore the users focus.
Expand All @@ -641,6 +673,10 @@ impl<'a, T: 'static> Context<'a, T> {
}

/// Register a listener to be called when the given focus handle or one of its descendants loses focus.
///
/// Like [`Context::on_blur`], this fires on window deactivation as well; see
/// [`Context::on_blur_by_user`].
///
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus_out(
&mut self,
Expand Down Expand Up @@ -671,6 +707,26 @@ impl<'a, T: 'static> Context<'a, T> {
subscription
}

/// Register a listener to be called when the given focus handle or one of its descendants
/// loses focus while the window is still active.
///
/// This is the subtree-wide counterpart of [`Context::on_blur_by_user`]; see that method for
/// why blur alone is not enough to decide whether the user is done with a control.
///
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus_out_by_user(
&mut self,
handle: &FocusHandle,
window: &mut Window,
mut listener: impl FnMut(&mut T, FocusOutEvent, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
self.on_focus_out(handle, window, move |view, event, window, cx| {
if window.is_window_active() {
listener(view, event, window, cx)
}
})
}

/// Schedule a future to be run asynchronously.
/// The given callback is invoked with a [`WeakEntity<V>`] to avoid leaking the entity for a long-running process.
/// It's also given an [`AsyncWindowContext`], which can be used to access the state of the entity across await points.
Expand Down Expand Up @@ -875,3 +931,85 @@ impl<T> BorrowMut<App> for Context<'_, T> {
self.app
}
}

#[cfg(test)]
mod tests {
use crate::{Context, FocusHandle, Subscription, TestAppContext, Window, div, prelude::*};
use std::{cell::Cell, rc::Rc};

struct BlurCounters {
first: FocusHandle,
second: FocusHandle,
_subscriptions: Vec<Subscription>,
}

impl Render for BlurCounters {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.child(div().track_focus(&self.first))
.child(div().track_focus(&self.second))
}
}

fn build_blur_counters(
blurs: Rc<Cell<usize>>,
blurs_by_user: Rc<Cell<usize>>,
window: &mut Window,
cx: &mut Context<BlurCounters>,
) -> BlurCounters {
let first = cx.focus_handle();
let second = cx.focus_handle();
window.focus(&first, cx);
let subscriptions = vec![
cx.on_blur(&first, window, move |_, _, _| blurs.set(blurs.get() + 1)),
cx.on_blur_by_user(&first, window, move |_, _, _| {
blurs_by_user.set(blurs_by_user.get() + 1)
}),
];
BlurCounters {
first,
second,
_subscriptions: subscriptions,
}
}

#[gpui::test]
fn test_on_blur_by_user_fires_when_focus_moves_within_window(cx: &mut TestAppContext) {
let blurs = Rc::new(Cell::new(0));
let blurs_by_user = Rc::new(Cell::new(0));
let (view, cx) = cx.add_window_view({
let blurs = blurs.clone();
let blurs_by_user = blurs_by_user.clone();
move |window, cx| build_blur_counters(blurs, blurs_by_user, window, cx)
});
cx.update(|window, _| window.activate_window());
cx.run_until_parked();

view.update_in(cx, |view, window, cx| {
let second = view.second.clone();
window.focus(&second, cx);
});
cx.run_until_parked();

assert_eq!(blurs.get(), 1);
assert_eq!(blurs_by_user.get(), 1);
}

#[gpui::test]
fn test_on_blur_by_user_ignores_window_deactivation(cx: &mut TestAppContext) {
let blurs = Rc::new(Cell::new(0));
let blurs_by_user = Rc::new(Cell::new(0));
let (_view, cx) = cx.add_window_view({
let blurs = blurs.clone();
let blurs_by_user = blurs_by_user.clone();
move |window, cx| build_blur_counters(blurs, blurs_by_user, window, cx)
});
cx.update(|window, _| window.activate_window());
cx.run_until_parked();

cx.deactivate_window();

assert_eq!(blurs.get(), 1);
assert_eq!(blurs_by_user.get(), 0);
}
}
5 changes: 5 additions & 0 deletions crates/gpui/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4010,6 +4010,11 @@ impl Window {
}

/// Register a listener to be called when the given focus handle or one of its descendants loses focus.
///
/// This also fires when the window is deactivated by the operating system; controls that
/// cancel, dismiss or commit in-progress user input want [`crate::Context::on_blur_by_user`]
/// or [`crate::Context::on_focus_out_by_user`] instead.
///
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus_out(
&mut self,
Expand Down
5 changes: 4 additions & 1 deletion crates/picker/src/head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ impl Head {
pub fn editor<V: 'static>(
placeholder_text: Arc<str>,
mut edit_handler: impl FnMut(&mut V, &ErasedEditorEvent, &mut Window, &mut Context<V>) + 'static,
blur_handler: impl FnMut(&mut V, &mut Window, &mut Context<V>) + 'static,
window: &mut Window,
cx: &mut Context<V>,
) -> Self {
let editor = (ui_input::ERASED_EDITOR_FACTORY.get().unwrap())(window, cx);

editor.set_placeholder_text(placeholder_text.as_ref(), window, cx);
cx.on_blur_by_user(&editor.focus_handle(cx), window, blur_handler)
.detach();
let this = cx.weak_entity();
editor
.subscribe(
Expand All @@ -47,7 +50,7 @@ impl Head {
cx: &mut Context<V>,
) -> Self {
let head = cx.new(EmptyHead::new);
cx.on_blur(&head.focus_handle(cx), window, blur_handler)
cx.on_blur_by_user(&head.focus_handle(cx), window, blur_handler)
.detach();
Self::Empty(head)
}
Expand Down
17 changes: 9 additions & 8 deletions crates/picker/src/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ impl<D: PickerDelegate> Picker<D> {
let head = Head::editor(
delegate.placeholder_text(window, cx),
Self::on_input_editor_event,
Self::on_input_editor_blur,
window,
cx,
);
Expand Down Expand Up @@ -314,6 +315,7 @@ impl<D: PickerDelegate> Picker<D> {
let head = Head::editor(
delegate.placeholder_text(window, cx),
Self::on_input_editor_event,
Self::on_input_editor_blur,
window,
cx,
);
Expand Down Expand Up @@ -649,21 +651,20 @@ impl<D: PickerDelegate> Picker<D> {
let query = editor.text(cx);
self.update_matches(query, window, cx);
}
ErasedEditorEvent::Blurred => {
if self.is_modal && window.is_window_active() {
self.cancel(&menu::Cancel, window, cx);
}
}
}
}

fn on_input_editor_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.is_modal {
self.cancel(&menu::Cancel, window, cx);
}
}

fn on_empty_head_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Head::Empty(_) = &self.head else {
panic!("unexpected call");
};
if window.is_window_active() {
self.cancel(&menu::Cancel, window, cx);
}
self.cancel(&menu::Cancel, window, cx);
}

pub fn refresh_placeholder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
Expand Down
Loading
Loading