1use crate::input::Copy;
3use crate::{StyledExt, TextSelectionLayer};
4use gpui::{
5 AnyElement, AnyView, App, AppContext, ClipboardItem, Context, Div, Entity, Global,
6 InteractiveElement, IntoElement, KeyBinding, ParentElement, Render, Stateful, StyleRefinement,
7 Styled, Window, actions, div,
8};
9use std::{any::TypeId, rc::Rc};
10
11actions!(root, [Tab, TabPrev]);
12const CONTEXT: &str = "Root";
13
14pub(crate) fn init(cx: &mut App) {
15 cx.bind_keys([
16 KeyBinding::new("tab", Tab, Some(CONTEXT)),
17 KeyBinding::new("shift-tab", TabPrev, Some(CONTEXT)),
18 #[cfg(target_os = "macos")]
19 KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
20 #[cfg(not(target_os = "macos"))]
21 KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
22 ]);
23}
24
25pub trait RootPlugin: Render + Sized {
47 fn prepare(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
53
54 fn style(&self, _surface: &mut Stateful<Div>, _window: &mut Window, _cx: &mut App) {}
59
60 fn decorate(
67 &self,
68 surface: AnyElement,
69 _root: &Root,
70 _window: &mut Window,
71 _cx: &mut App,
72 ) -> impl IntoElement {
73 surface
74 }
75}
76
77type PluginFactory = Rc<dyn Fn(&mut Window, &mut Context<Root>) -> Plugin>;
78type Prepare = Rc<dyn Fn(&mut Window, &mut App)>;
79type SurfaceStyle = Rc<dyn Fn(&mut Stateful<Div>, &mut Window, &mut App)>;
80type Decorate = Rc<dyn Fn(AnyElement, &Root, &mut Window, &mut App) -> AnyElement>;
81#[derive(Default)]
82struct PluginRegistry(Vec<(TypeId, PluginFactory)>);
83impl Global for PluginRegistry {}
84struct Plugin {
85 view: AnyView,
86 prepare: Prepare,
87 style: SurfaceStyle,
88 decorate: Decorate,
89}
90
91pub struct Root {
93 view: AnyView,
94 style: StyleRefinement,
95 plugins: Vec<Plugin>,
96}
97
98impl Root {
99 pub fn register_plugin<V: RootPlugin>(
102 cx: &mut App,
103 build: fn(&mut Window, &mut Context<V>) -> V,
104 ) {
105 if !cx.has_global::<PluginRegistry>() {
106 cx.set_global(PluginRegistry::default());
107 }
108 let factory: PluginFactory = Rc::new(move |window, cx| {
109 let entity = cx.new(|cx| build(window, cx));
110 let root = cx.weak_entity();
111 let observed = entity.clone();
112 cx.defer(move |cx| {
116 cx.observe(&observed, move |_, cx| {
117 let _ = root.update(cx, |_, cx| cx.notify());
118 })
119 .detach();
120 });
121 let prepare = entity.clone();
122 let style = entity.clone();
123 let decorate = entity.clone();
124 Plugin {
125 view: entity.into(),
126 prepare: Rc::new(move |window, cx| {
127 prepare.update(cx, |state, cx| state.prepare(window, cx))
128 }),
129 style: Rc::new(move |surface, window, cx| {
130 style.update(cx, |state, cx| state.style(surface, window, cx))
131 }),
132 decorate: Rc::new(move |surface, root, window, cx| {
133 decorate.update(cx, |state, cx| {
134 state.decorate(surface, root, window, cx).into_any_element()
135 })
136 }),
137 }
138 });
139 let plugins = &mut cx.global_mut::<PluginRegistry>().0;
140 if let Some(entry) = plugins.iter_mut().find(|(id, _)| *id == TypeId::of::<V>()) {
141 entry.1 = factory;
142 } else {
143 plugins.push((TypeId::of::<V>(), factory));
144 }
145 }
146
147 pub fn new(view: impl Into<AnyView>, window: &mut Window, cx: &mut Context<Self>) -> Self {
148 #[cfg(all(target_os = "macos", not(test)))]
149 crate::install_window_hit_test_forwarder(window);
150 let factories = cx
151 .try_global::<PluginRegistry>()
152 .map(|e| e.0.clone())
153 .unwrap_or_default();
154 Self {
155 view: view.into(),
156 style: StyleRefinement::default(),
157 plugins: factories
158 .into_iter()
159 .map(|(_, build)| build(window, cx))
160 .collect(),
161 }
162 }
163
164 pub fn view(&self) -> &AnyView {
166 &self.view
167 }
168
169 pub fn plugin<V: RootPlugin>(&self) -> Option<Entity<V>> {
171 self.plugins
172 .iter()
173 .find_map(|entry| entry.view.clone().downcast::<V>().ok())
174 }
175
176 pub fn read<'a>(window: &'a Window, cx: &'a App) -> &'a Self {
177 window
178 .root::<Self>()
179 .flatten()
180 .expect("window must have a Base Root")
181 .read(cx)
182 }
183 pub fn update<R>(
184 window: &mut Window,
185 cx: &mut App,
186 f: impl FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R,
187 ) -> R {
188 let root = window
189 .root::<Self>()
190 .flatten()
191 .expect("window must have a Base Root");
192 root.update(cx, |root, cx| f(root, window, cx))
193 }
194 fn on_action_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
195 if let Some(container_focus_handle) = crate::active_focus_trap(window, cx) {
197 let before_focus = window.focused(cx);
199
200 window.focus_next(cx);
202
203 if !container_focus_handle.contains_focused(window, cx) {
205 let mut attempts = 0;
208 const MAX_ATTEMPTS: usize = 100; while !container_focus_handle.contains_focused(window, cx)
211 && attempts < MAX_ATTEMPTS
212 {
213 window.focus_next(cx);
214 attempts += 1;
215
216 if window.focused(cx) == before_focus {
218 break;
219 }
220 }
221 }
222 return;
223 }
224
225 window.focus_next(cx);
227 }
228
229 fn on_action_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
230 if let Some(container_focus_handle) = crate::active_focus_trap(window, cx) {
232 let before_focus = window.focused(cx);
234
235 window.focus_prev(cx);
237
238 if !container_focus_handle.contains_focused(window, cx) {
240 let mut attempts = 0;
243 const MAX_ATTEMPTS: usize = 100; while !container_focus_handle.contains_focused(window, cx)
246 && attempts < MAX_ATTEMPTS
247 {
248 window.focus_prev(cx);
249 attempts += 1;
250
251 if window.focused(cx) == before_focus {
253 break;
254 }
255 }
256 }
257 return;
258 }
259
260 window.focus_prev(cx);
262 }
263
264 fn on_action_copy(&mut self, _: &Copy, window: &mut Window, cx: &mut Context<Self>) {
265 let text = crate::TextSelection::selected_text(window, cx)
266 .trim()
267 .to_string();
268 if text.is_empty() {
269 cx.propagate();
270 return;
271 }
272 cx.write_to_clipboard(ClipboardItem::new_string(text));
273 }
274}
275impl Styled for Root {
276 fn style(&mut self) -> &mut StyleRefinement {
277 &mut self.style
278 }
279}
280impl Render for Root {
281 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
282 for plugin in &self.plugins {
283 (plugin.prepare)(window, cx);
284 }
285 let mut content = div()
286 .id("root")
287 .key_context(CONTEXT)
288 .on_action(cx.listener(Self::on_action_tab))
289 .on_action(cx.listener(Self::on_action_tab_prev))
290 .on_action(cx.listener(Self::on_action_copy))
291 .relative()
292 .size_full()
293 .child(TextSelectionLayer)
294 .child(self.view.clone())
295 .child(
296 div()
297 .absolute()
298 .inset_0()
299 .children(self.plugins.iter().map(|plugin| plugin.view.clone())),
300 );
301 for plugin in &self.plugins {
302 (plugin.style)(&mut content, window, cx);
303 }
304 let mut content = content.refine_style(&self.style).into_any_element();
305 for plugin in &self.plugins {
306 content = (plugin.decorate)(content, self, window, cx);
307 }
308 content
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use gpui::TestAppContext;
316
317 struct Content;
318 impl Render for Content {
319 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
320 div()
321 }
322 }
323 struct Layer;
324 impl Render for Layer {
325 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
326 div()
327 }
328 }
329 impl RootPlugin for Layer {}
330 fn layer(_: &mut Window, _: &mut Context<Layer>) -> Layer {
331 Layer
332 }
333
334 #[gpui::test]
335 fn plugin_registration_is_idempotent_and_state_is_per_window(cx: &mut TestAppContext) {
336 cx.update(|cx| {
337 crate::init(cx);
338 Root::register_plugin(cx, layer);
339 Root::register_plugin(cx, layer);
340 });
341 let mut ids = Vec::new();
342 for _ in 0..2 {
343 let (root, _) = cx.add_window_view(|window, cx| {
344 let content = cx.new(|_| Content);
345 Root::new(content, window, cx)
346 });
347 let id = root.read_with(cx, |root, _| {
348 assert_eq!(root.plugins.len(), 1);
349 root.plugin::<Layer>().unwrap().entity_id()
350 });
351 ids.push(id);
352 }
353 assert_ne!(ids[0], ids[1]);
354 }
355}