1use gpui::{App, Global, SharedString};
25use std::{cell::Cell, rc::Rc};
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum Cue {
30 MessageArrived {
32 message_id: SharedString,
34 },
35 ResponseSettled {
37 message_id: SharedString,
39 succeeded: bool,
41 },
42 Copied,
44 Submitted,
46 Cancelled,
48 SuggestionSelected,
50 ThreadSelected,
52 Decided {
54 approved: bool,
56 },
57}
58
59type Observer = Rc<dyn Fn(&Cue, &mut App)>;
60
61struct ObserverRegistration {
62 active: Rc<Cell<bool>>,
63 observer: Observer,
64}
65
66#[derive(Default)]
67struct CueHub {
68 observers: Vec<ObserverRegistration>,
69}
70
71impl Global for CueHub {}
72
73#[must_use = "dropping the subscription stops cue delivery"]
75pub struct CueSubscription {
76 active: Option<Rc<Cell<bool>>>,
77}
78
79impl CueSubscription {
80 pub fn detach(mut self) {
82 self.active.take();
83 }
84}
85
86impl Drop for CueSubscription {
87 fn drop(&mut self) {
88 if let Some(active) = self.active.as_ref() {
91 active.set(false);
92 }
93 }
94}
95
96pub fn observe(cx: &mut App, observer: impl Fn(&Cue, &mut App) + 'static) -> CueSubscription {
98 let active = Rc::new(Cell::new(true));
99 cx.default_global::<CueHub>()
100 .observers
101 .push(ObserverRegistration {
102 active: active.clone(),
103 observer: Rc::new(observer),
104 });
105 CueSubscription {
106 active: Some(active),
107 }
108}
109
110pub fn emit(cx: &mut App, cue: Cue) {
113 if !cx.has_global::<CueHub>() {
114 return;
115 }
116 let observers: Vec<Observer> = {
117 let hub = cx.global_mut::<CueHub>();
118 hub.observers
119 .retain(|registration| registration.active.get());
120 hub.observers
121 .iter()
122 .map(|registration| registration.observer.clone())
123 .collect()
124 };
125 for observer in observers {
128 observer(&cue, cx);
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use gpui::TestAppContext;
136 use std::{cell::RefCell, rc::Rc};
137
138 #[gpui::test]
139 fn observers_receive_cues_until_dropped(cx: &mut TestAppContext) {
140 let received = Rc::new(RefCell::new(Vec::new()));
141 let subscription = cx.update(|cx| {
142 let received = received.clone();
143 observe(cx, move |cue, _| received.borrow_mut().push(cue.clone()))
144 });
145 cx.update(|cx| emit(cx, Cue::Copied));
146 assert_eq!(received.borrow().as_slice(), &[Cue::Copied]);
147
148 drop(subscription);
149 cx.update(|cx| emit(cx, Cue::Submitted));
150 assert_eq!(received.borrow().as_slice(), &[Cue::Copied]);
151 }
152
153 #[gpui::test]
154 fn emitting_without_observers_is_a_no_op(cx: &mut TestAppContext) {
155 cx.update(|cx| emit(cx, Cue::Cancelled));
156 }
157}