chore: Fix subscriptions leak. (#597)

This commit is contained in:
Jason Lee 2025-02-04 15:43:26 +08:00 committed by GitHub
parent 3ffbbb0688
commit dffb419145
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 107 additions and 81 deletions

View file

@ -3,7 +3,7 @@ use std::rc::Rc;
use gpui::{ use gpui::{
div, px, AnyElement, App, AppContext, ClickEvent, Context, Corner, Entity, FocusHandle, Hsla, div, px, AnyElement, App, AppContext, ClickEvent, Context, Corner, Entity, FocusHandle, Hsla,
InteractiveElement as _, IntoElement, MouseButton, ParentElement as _, Render, SharedString, InteractiveElement as _, IntoElement, MouseButton, ParentElement as _, Render, SharedString,
Styled as _, Window, Styled as _, Subscription, Window,
}; };
use ui::{ use ui::{
badge::Badge, badge::Badge,
@ -24,6 +24,7 @@ pub struct AppTitleBar {
font_size_selector: Entity<FontSizeSelector>, font_size_selector: Entity<FontSizeSelector>,
theme_color_picker: Entity<ColorPicker>, theme_color_picker: Entity<ColorPicker>,
child: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>, child: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>,
_subscriptions: Vec<Subscription>,
} }
impl AppTitleBar { impl AppTitleBar {
@ -43,7 +44,8 @@ impl AppTitleBar {
picker.set_value(cx.theme().primary, window, cx); picker.set_value(cx.theme().primary, window, cx);
picker picker
}); });
cx.subscribe_in(
let _subscriptions = vec![cx.subscribe_in(
&theme_color_picker, &theme_color_picker,
window, window,
|this, _, ev: &ColorPickerEvent, window, cx| match ev { |this, _, ev: &ColorPickerEvent, window, cx| match ev {
@ -51,8 +53,7 @@ impl AppTitleBar {
this.set_theme_color(*color, window, cx); this.set_theme_color(*color, window, cx);
} }
}, },
) )];
.detach();
Self { Self {
title: title.into(), title: title.into(),
@ -61,6 +62,7 @@ impl AppTitleBar {
font_size_selector, font_size_selector,
theme_color_picker, theme_color_picker,
child: Rc::new(|_, _| div().into_any_element()), child: Rc::new(|_, _| div().into_any_element()),
_subscriptions,
} }
} }

View file

@ -2,7 +2,7 @@ use gpui::{
anchored, canvas, deferred, div, prelude::FluentBuilder as _, px, relative, App, AppContext, anchored, canvas, deferred, div, prelude::FluentBuilder as _, px, relative, App, AppContext,
Bounds, Context, Corner, ElementId, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Bounds, Context, Corner, ElementId, Entity, EventEmitter, FocusHandle, Focusable, Hsla,
InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
Render, SharedString, StatefulInteractiveElement as _, Styled, Window, Render, SharedString, StatefulInteractiveElement as _, Styled, Subscription, Window,
}; };
use crate::{ use crate::{
@ -66,13 +66,14 @@ pub struct ColorPicker {
open: bool, open: bool,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
_subscriptions: Vec<Subscription>,
} }
impl ColorPicker { impl ColorPicker {
pub fn new(id: impl Into<ElementId>, window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(id: impl Into<ElementId>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let color_input = cx.new(|cx| TextInput::new(window, cx).xsmall()); let color_input = cx.new(|cx| TextInput::new(window, cx).xsmall());
cx.subscribe_in( let _subscriptions = vec![cx.subscribe_in(
&color_input, &color_input,
window, window,
|this, _, ev: &InputEvent, window, cx| match ev { |this, _, ev: &InputEvent, window, cx| match ev {
@ -91,8 +92,7 @@ impl ColorPicker {
} }
_ => {} _ => {}
}, },
) )];
.detach();
Self { Self {
id: id.into(), id: id.into(),
@ -118,6 +118,7 @@ impl ColorPicker {
color_input, color_input,
open: false, open: false,
bounds: Bounds::default(), bounds: Bounds::default(),
_subscriptions,
} }
} }

View file

@ -2,7 +2,8 @@ use gpui::{
actions, anchored, canvas, deferred, div, prelude::FluentBuilder, px, rems, AnyElement, App, actions, anchored, canvas, deferred, div, prelude::FluentBuilder, px, rems, AnyElement, App,
AppContext, Bounds, ClickEvent, Context, DismissEvent, ElementId, Entity, EventEmitter, AppContext, Bounds, ClickEvent, Context, DismissEvent, ElementId, Entity, EventEmitter,
FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, Length, ParentElement, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, Length, ParentElement,
Pixels, Render, SharedString, StatefulInteractiveElement, Styled, Task, WeakEntity, Window, Pixels, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task,
WeakEntity, Window,
}; };
use rust_i18n::t; use rust_i18n::t;
@ -252,6 +253,7 @@ pub struct Dropdown<D: DropdownDelegate + 'static> {
/// Store the bounds of the input /// Store the bounds of the input
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
disabled: bool, disabled: bool,
_subscriptions: Vec<Subscription>,
} }
pub struct SearchableVec<T> { pub struct SearchableVec<T> {
@ -352,9 +354,10 @@ where
list list
}); });
cx.on_blur(&list.focus_handle(cx), window, Self::on_blur) let _subscriptions = vec![
.detach(); cx.on_blur(&list.focus_handle(cx), window, Self::on_blur),
cx.on_blur(&focus_handle, window, Self::on_blur).detach(); cx.on_blur(&focus_handle, window, Self::on_blur),
];
let mut this = Self { let mut this = Self {
id: id.into(), id: id.into(),
@ -372,6 +375,7 @@ where
menu_width: Length::Auto, menu_width: Length::Auto,
bounds: Bounds::default(), bounds: Bounds::default(),
disabled: false, disabled: false,
_subscriptions,
}; };
this.set_selected_index(selected_index, window, cx); this.set_selected_index(selected_index, window, cx);
this this

View file

@ -15,7 +15,7 @@ use gpui::{
Context, Entity, EntityInputHandler, EventEmitter, FocusHandle, Focusable, Context, Entity, EntityInputHandler, EventEmitter, FocusHandle, Focusable,
InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Rems, Render, ScrollHandle, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Rems, Render, ScrollHandle,
ScrollWheelEvent, SharedString, Styled as _, UTF16Selection, Window, WrappedLine, ScrollWheelEvent, SharedString, Styled as _, Subscription, UTF16Selection, Window, WrappedLine,
}; };
// TODO: // TODO:
@ -230,6 +230,7 @@ pub struct TextInput {
pub(crate) scroll_size: gpui::Size<Pixels>, pub(crate) scroll_size: gpui::Size<Pixels>,
/// To remember the horizontal column (x-coordinate) of the cursor position. /// To remember the horizontal column (x-coordinate) of the cursor position.
preferred_x_offset: Option<Pixels>, preferred_x_offset: Option<Pixels>,
_subscriptions: Vec<Subscription>,
} }
impl EventEmitter<InputEvent> for TextInput {} impl EventEmitter<InputEvent> for TextInput {}
@ -239,7 +240,26 @@ impl TextInput {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
let blink_cursor = cx.new(|_| BlinkCursor::new()); let blink_cursor = cx.new(|_| BlinkCursor::new());
let history = History::new().group_interval(std::time::Duration::from_secs(1)); let history = History::new().group_interval(std::time::Duration::from_secs(1));
let input = Self {
let _subscriptions = vec![
// Observe the blink cursor to repaint the view when it changes.
cx.observe(&blink_cursor, |_, _, cx| cx.notify()),
// Blink the cursor when the window is active, pause when it's not.
cx.observe_window_activation(window, |input, window, cx| {
if window.is_window_active() {
let focus_handle = input.focus_handle.clone();
if focus_handle.is_focused(window) {
input.blink_cursor.update(cx, |blink_cursor, cx| {
blink_cursor.start(cx);
});
}
}
}),
cx.on_focus(&focus_handle, window, Self::on_focus),
cx.on_blur(&focus_handle, window, Self::on_blur),
];
Self {
focus_handle: focus_handle.clone(), focus_handle: focus_handle.clone(),
text: "".into(), text: "".into(),
multi_line: false, multi_line: false,
@ -272,28 +292,8 @@ impl TextInput {
scrollbar_state: Rc::new(Cell::new(ScrollbarState::default())), scrollbar_state: Rc::new(Cell::new(ScrollbarState::default())),
scroll_size: gpui::size(px(0.), px(0.)), scroll_size: gpui::size(px(0.), px(0.)),
preferred_x_offset: None, preferred_x_offset: None,
}; _subscriptions,
}
// Observe the blink cursor to repaint the view when it changes.
cx.observe(&input.blink_cursor, |_, _, cx| cx.notify())
.detach();
// Blink the cursor when the window is active, pause when it's not.
cx.observe_window_activation(window, |input, window, cx| {
if window.is_window_active() {
let focus_handle = input.focus_handle.clone();
if focus_handle.is_focused(window) {
input.blink_cursor.update(cx, |blink_cursor, cx| {
blink_cursor.start(cx);
});
}
}
})
.detach();
cx.on_focus(&focus_handle, window, Self::on_focus).detach();
cx.on_blur(&focus_handle, window, Self::on_blur).detach();
input
} }
/// Use the text input field as a multi-line Textarea. /// Use the text input field as a multi-line Textarea.

View file

@ -1,7 +1,7 @@
use gpui::{ use gpui::{
div, prelude::FluentBuilder, px, AnyElement, AppContext as _, Context, Entity, EventEmitter, div, prelude::FluentBuilder, px, AnyElement, AppContext as _, Context, Entity, EventEmitter,
FocusHandle, Focusable, InteractiveElement, IntoElement, KeyDownEvent, MouseButton, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyDownEvent, MouseButton,
MouseDownEvent, ParentElement as _, Render, SharedString, Styled as _, Window, MouseDownEvent, ParentElement as _, Render, SharedString, Styled as _, Subscription, Window,
}; };
use crate::{h_flex, v_flex, ActiveTheme, Icon, IconName, Sizable, Size}; use crate::{h_flex, v_flex, ActiveTheme, Icon, IconName, Sizable, Size};
@ -29,13 +29,33 @@ pub struct OtpInput {
value: SharedString, value: SharedString,
blink_cursor: Entity<BlinkCursor>, blink_cursor: Entity<BlinkCursor>,
size: Size, size: Size,
_subscriptions: Vec<Subscription>,
} }
impl OtpInput { impl OtpInput {
pub fn new(length: usize, window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(length: usize, window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
let blink_cursor = cx.new(|_| BlinkCursor::new()); let blink_cursor = cx.new(|_| BlinkCursor::new());
let input = Self {
let _subscriptions = vec![
// Observe the blink cursor to repaint the view when it changes.
cx.observe(&blink_cursor, |_, _, cx| cx.notify()),
// Blink the cursor when the window is active, pause when it's not.
cx.observe_window_activation(window, |this, window, cx| {
if window.is_window_active() {
let focus_handle = this.focus_handle.clone();
if focus_handle.is_focused(window) {
this.blink_cursor.update(cx, |blink_cursor, cx| {
blink_cursor.start(cx);
});
}
}
}),
cx.on_focus(&focus_handle, window, Self::on_focus),
cx.on_blur(&focus_handle, window, Self::on_blur),
];
Self {
focus_handle: focus_handle.clone(), focus_handle: focus_handle.clone(),
length, length,
number_of_groups: 2, number_of_groups: 2,
@ -43,27 +63,8 @@ impl OtpInput {
masked: false, masked: false,
blink_cursor: blink_cursor.clone(), blink_cursor: blink_cursor.clone(),
size: Size::Medium, size: Size::Medium,
}; _subscriptions,
}
// Observe the blink cursor to repaint the view when it changes.
cx.observe(&blink_cursor, |_, _, cx| cx.notify()).detach();
// Blink the cursor when the window is active, pause when it's not.
cx.observe_window_activation(window, |this, window, cx| {
if window.is_window_active() {
let focus_handle = this.focus_handle.clone();
if focus_handle.is_focused(window) {
this.blink_cursor.update(cx, |blink_cursor, cx| {
blink_cursor.start(cx);
});
}
}
})
.detach();
cx.on_focus(&focus_handle, window, Self::on_focus).detach();
cx.on_blur(&focus_handle, window, Self::on_blur).detach();
input
} }
/// Set number of groups in the OTP Input. /// Set number of groups in the OTP Input.

View file

@ -13,7 +13,7 @@ use gpui::{
ListSizingBehavior, MouseButton, ParentElement, Render, SharedString, Styled, Task, ListSizingBehavior, MouseButton, ParentElement, Render, SharedString, Styled, Task,
UniformListScrollHandle, Window, UniformListScrollHandle, Window,
}; };
use gpui::{px, App, Context, EventEmitter, ScrollStrategy}; use gpui::{px, App, Context, EventEmitter, ScrollStrategy, Subscription};
use smol::Timer; use smol::Timer;
use super::loading::Loading; use super::loading::Loading;
@ -158,6 +158,7 @@ pub struct List<D: ListDelegate> {
right_clicked_index: Option<usize>, right_clicked_index: Option<usize>,
_search_task: Task<()>, _search_task: Task<()>,
_load_more_task: Task<()>, _load_more_task: Task<()>,
_query_input_subscription: Subscription,
} }
impl<D> List<D> impl<D> List<D>
@ -173,8 +174,8 @@ where
.cleanable() .cleanable()
}); });
cx.subscribe_in(&query_input, window, Self::on_query_input_event) let _query_input_subscription =
.detach(); cx.subscribe_in(&query_input, window, Self::on_query_input_event);
Self { Self {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
@ -192,6 +193,7 @@ where
size: Size::default(), size: Size::default(),
_search_task: Task::ready(()), _search_task: Task::ready(()),
_load_more_task: Task::ready(()), _load_more_task: Task::ready(()),
_query_input_subscription,
} }
} }
@ -233,8 +235,8 @@ where
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
cx.subscribe_in(&query_input, window, Self::on_query_input_event) self._query_input_subscription =
.detach(); cx.subscribe_in(&query_input, window, Self::on_query_input_event);
self.query_input = Some(query_input); self.query_input = Some(query_input);
} }

View file

@ -1,9 +1,15 @@
use std::{any::TypeId, collections::VecDeque, sync::Arc, time::Duration}; use std::{
any::TypeId,
collections::{HashMap, VecDeque},
sync::Arc,
time::Duration,
};
use gpui::{ use gpui::{
div, prelude::FluentBuilder, px, Animation, AnimationExt, App, AppContext, ClickEvent, Context, div, prelude::FluentBuilder, px, Animation, AnimationExt, App, AppContext, ClickEvent, Context,
DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement, DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, Window, ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
Window,
}; };
use smol::Timer; use smol::Timer;
@ -20,7 +26,7 @@ pub enum NotificationType {
Error, Error,
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone, Hash, Eq)]
pub(crate) enum NotificationId { pub(crate) enum NotificationId {
Id(TypeId), Id(TypeId),
IdAndElementId(TypeId, ElementId), IdAndElementId(TypeId, ElementId),
@ -286,6 +292,7 @@ pub struct NotificationList {
/// Notifications that will be auto hidden. /// Notifications that will be auto hidden.
pub(crate) notifications: VecDeque<Entity<Notification>>, pub(crate) notifications: VecDeque<Entity<Notification>>,
expanded: bool, expanded: bool,
_subscriptions: HashMap<NotificationId, Subscription>,
} }
impl NotificationList { impl NotificationList {
@ -293,6 +300,7 @@ impl NotificationList {
Self { Self {
notifications: VecDeque::new(), notifications: VecDeque::new(),
expanded: false, expanded: false,
_subscriptions: HashMap::new(),
} }
} }
@ -310,10 +318,14 @@ impl NotificationList {
self.notifications.retain(|note| note.read(cx).id != id); self.notifications.retain(|note| note.read(cx).id != id);
let notification = cx.new(|_| notification); let notification = cx.new(|_| notification);
cx.subscribe(&notification, move |view, _, _: &DismissEvent, cx| {
view.notifications.retain(|note| id != note.read(cx).id); self._subscriptions.insert(
}) id.clone(),
.detach(); cx.subscribe(&notification, move |view, _, _: &DismissEvent, cx| {
view.notifications.retain(|note| id != note.read(cx).id);
view._subscriptions.remove(&id);
}),
);
self.notifications.push_back(notification.clone()); self.notifications.push_back(notification.clone());
if autohide { if autohide {

View file

@ -4,6 +4,7 @@ use crate::{
button::Button, h_flex, list::ListItem, popover::Popover, v_flex, ActiveTheme, Icon, IconName, button::Button, h_flex, list::ListItem, popover::Popover, v_flex, ActiveTheme, Icon, IconName,
Selectable, Sizable as _, Selectable, Sizable as _,
}; };
use gpui::Subscription;
use gpui::{ use gpui::{
actions, anchored, canvas, div, prelude::FluentBuilder, px, rems, Action, AnyElement, App, actions, anchored, canvas, div, prelude::FluentBuilder, px, rems, Action, AnyElement, App,
AppContext, Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, AppContext, Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle,
@ -107,7 +108,7 @@ pub struct PopupMenu {
scroll_state: Rc<Cell<ScrollbarState>>, scroll_state: Rc<Cell<ScrollbarState>>,
action_focus_handle: Option<FocusHandle>, action_focus_handle: Option<FocusHandle>,
_subscriptions: [gpui::Subscription; 1], _subscriptions: Vec<Subscription>,
} }
impl PopupMenu { impl PopupMenu {
@ -118,10 +119,12 @@ impl PopupMenu {
) -> Entity<Self> { ) -> Entity<Self> {
cx.new(|cx| { cx.new(|cx| {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
let _on_blur_subscription = let _subscriptions =
cx.on_blur(&focus_handle, window, |this: &mut PopupMenu, window, cx| { vec![
this.dismiss(&Dismiss, window, cx) cx.on_blur(&focus_handle, window, |this: &mut PopupMenu, window, cx| {
}); this.dismiss(&Dismiss, window, cx)
}),
];
let menu = Self { let menu = Self {
focus_handle, focus_handle,
@ -138,7 +141,7 @@ impl PopupMenu {
scrollable: false, scrollable: false,
scroll_handle: ScrollHandle::default(), scroll_handle: ScrollHandle::default(),
scroll_state: Rc::new(Cell::new(ScrollbarState::default())), scroll_state: Rc::new(Cell::new(ScrollbarState::default())),
_subscriptions: [_on_blur_subscription], _subscriptions,
}; };
window.refresh(); window.refresh();
f(menu, window, cx) f(menu, window, cx)

View file

@ -3,7 +3,7 @@ use gpui::{
anchored, deferred, div, prelude::FluentBuilder as _, px, App, AppContext, Context, ElementId, anchored, deferred, div, prelude::FluentBuilder as _, px, App, AppContext, Context, ElementId,
Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, KeyBinding, Length, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, KeyBinding, Length,
MouseButton, ParentElement as _, Render, SharedString, StatefulInteractiveElement as _, Styled, MouseButton, ParentElement as _, Render, SharedString, StatefulInteractiveElement as _, Styled,
Window, Subscription, Window,
}; };
use rust_i18n::t; use rust_i18n::t;
@ -68,6 +68,7 @@ pub struct DatePicker {
calendar: Entity<Calendar>, calendar: Entity<Calendar>,
number_of_months: usize, number_of_months: usize,
presets: Option<Vec<DateRangePreset>>, presets: Option<Vec<DateRangePreset>>,
_subscriptions: Vec<Subscription>,
} }
impl DatePicker { impl DatePicker {
@ -103,7 +104,7 @@ impl DatePicker {
this this
}); });
cx.subscribe_in( let _subscriptions = vec![cx.subscribe_in(
&calendar, &calendar,
window, window,
|this, _, ev: &CalendarEvent, window, cx| match ev { |this, _, ev: &CalendarEvent, window, cx| match ev {
@ -112,8 +113,7 @@ impl DatePicker {
this.focus_handle.focus(window); this.focus_handle.focus(window);
} }
}, },
) )];
.detach();
Self { Self {
id: id.into(), id: id.into(),
@ -128,6 +128,7 @@ impl DatePicker {
number_of_months: 1, number_of_months: 1,
placeholder: None, placeholder: None,
presets: None, presets: None,
_subscriptions,
} }
} }