workspace.rs

  1use std::marker::PhantomData;
  2use std::sync::atomic::{AtomicBool, Ordering};
  3use std::sync::{Arc, OnceLock};
  4
  5use chrono::DateTime;
  6use gpui3::{px, relative, rems, Size};
  7
  8use crate::prelude::*;
  9use crate::{
 10    hello_world_rust_editor_with_status_example, random_players_with_call_status, theme, v_stack,
 11    AssistantPanel, ChatMessage, ChatPanel, CollabPanel, EditorPane, Label, LanguageSelector,
 12    Livestream, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide, ProjectPanel, SplitDirection,
 13    StatusBar, Terminal, TitleBar, Toast, ToastOrigin,
 14};
 15
 16pub struct WorkspaceState {
 17    pub show_project_panel: Arc<AtomicBool>,
 18    pub show_collab_panel: Arc<AtomicBool>,
 19    pub show_chat_panel: Arc<AtomicBool>,
 20    pub show_assistant_panel: Arc<AtomicBool>,
 21    pub show_terminal: Arc<AtomicBool>,
 22    pub show_language_selector: Arc<AtomicBool>,
 23}
 24
 25impl WorkspaceState {
 26    fn toggle_value(current_value: &AtomicBool) {
 27        let value = current_value.load(Ordering::SeqCst);
 28
 29        current_value
 30            .compare_exchange(value, !value, Ordering::SeqCst, Ordering::SeqCst)
 31            .unwrap();
 32    }
 33
 34    pub fn is_project_panel_open(&self) -> bool {
 35        self.show_project_panel.load(Ordering::SeqCst)
 36    }
 37
 38    pub fn toggle_project_panel(&self) {
 39        Self::toggle_value(&self.show_project_panel);
 40
 41        self.show_collab_panel.store(false, Ordering::SeqCst);
 42    }
 43
 44    pub fn is_collab_panel_open(&self) -> bool {
 45        self.show_collab_panel.load(Ordering::SeqCst)
 46    }
 47
 48    pub fn toggle_collab_panel(&self) {
 49        Self::toggle_value(&self.show_collab_panel);
 50
 51        self.show_project_panel.store(false, Ordering::SeqCst);
 52    }
 53
 54    pub fn is_terminal_open(&self) -> bool {
 55        self.show_terminal.load(Ordering::SeqCst)
 56    }
 57
 58    pub fn toggle_terminal(&self) {
 59        Self::toggle_value(&self.show_terminal);
 60    }
 61
 62    pub fn is_chat_panel_open(&self) -> bool {
 63        self.show_chat_panel.load(Ordering::SeqCst)
 64    }
 65
 66    pub fn toggle_chat_panel(&self) {
 67        Self::toggle_value(&self.show_chat_panel);
 68
 69        self.show_assistant_panel.store(false, Ordering::SeqCst);
 70    }
 71
 72    pub fn is_assistant_panel_open(&self) -> bool {
 73        self.show_assistant_panel.load(Ordering::SeqCst)
 74    }
 75
 76    pub fn toggle_assistant_panel(&self) {
 77        Self::toggle_value(&self.show_assistant_panel);
 78
 79        self.show_chat_panel.store(false, Ordering::SeqCst);
 80    }
 81
 82    pub fn is_language_selector_open(&self) -> bool {
 83        self.show_language_selector.load(Ordering::SeqCst)
 84    }
 85
 86    pub fn toggle_language_selector(&self) {
 87        Self::toggle_value(&self.show_language_selector);
 88    }
 89}
 90
 91/// HACK: This is just a temporary way to start hooking up interactivity until
 92/// I can get an explainer on how we should actually be managing state.
 93static WORKSPACE_STATE: OnceLock<WorkspaceState> = OnceLock::new();
 94
 95pub fn get_workspace_state() -> &'static WorkspaceState {
 96    let state = WORKSPACE_STATE.get_or_init(|| WorkspaceState {
 97        show_project_panel: Arc::new(AtomicBool::new(true)),
 98        show_collab_panel: Arc::new(AtomicBool::new(false)),
 99        show_chat_panel: Arc::new(AtomicBool::new(true)),
100        show_assistant_panel: Arc::new(AtomicBool::new(false)),
101        show_terminal: Arc::new(AtomicBool::new(true)),
102        show_language_selector: Arc::new(AtomicBool::new(false)),
103    });
104
105    state
106}
107
108#[derive(Element)]
109pub struct WorkspaceElement<S: 'static + Send + Sync + Clone> {
110    state_type: PhantomData<S>,
111    left_panel_scroll_state: ScrollState,
112    right_panel_scroll_state: ScrollState,
113    tab_bar_scroll_state: ScrollState,
114    bottom_panel_scroll_state: ScrollState,
115}
116
117impl<S: 'static + Send + Sync + Clone> WorkspaceElement<S> {
118    pub fn new() -> Self {
119        Self {
120            state_type: PhantomData,
121            left_panel_scroll_state: ScrollState::default(),
122            right_panel_scroll_state: ScrollState::default(),
123            tab_bar_scroll_state: ScrollState::default(),
124            bottom_panel_scroll_state: ScrollState::default(),
125        }
126    }
127
128    pub fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
129        let theme = theme(cx).clone();
130
131        let workspace_state = get_workspace_state();
132
133        let temp_size = rems(36.).into();
134
135        let root_group = PaneGroup::new_groups(
136            vec![
137                PaneGroup::new_panes(
138                    vec![
139                        Pane::new(
140                            ScrollState::default(),
141                            Size {
142                                width: relative(1.).into(),
143                                height: temp_size,
144                            },
145                            |_, payload| {
146                                let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
147
148                                vec![EditorPane::new(hello_world_rust_editor_with_status_example(
149                                    &theme,
150                                ))
151                                .into_any()]
152                            },
153                            Box::new(theme.clone()),
154                        ),
155                        Pane::new(
156                            ScrollState::default(),
157                            Size {
158                                width: relative(1.).into(),
159                                height: temp_size,
160                            },
161                            |_, _| vec![Terminal::new().into_any()],
162                            Box::new(()),
163                        ),
164                    ],
165                    SplitDirection::Vertical,
166                ),
167                PaneGroup::new_panes(
168                    vec![Pane::new(
169                        ScrollState::default(),
170                        Size {
171                            width: relative(1.).into(),
172                            height: relative(1.).into(),
173                        },
174                        |_, payload| {
175                            let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
176
177                            vec![EditorPane::new(hello_world_rust_editor_with_status_example(
178                                &theme,
179                            ))
180                            .into_any()]
181                        },
182                        Box::new(theme.clone()),
183                    )],
184                    SplitDirection::Vertical,
185                ),
186            ],
187            SplitDirection::Horizontal,
188        );
189
190        div()
191            .relative()
192            .size_full()
193            .flex()
194            .flex_col()
195            .font("Zed Sans Extended")
196            .gap_0()
197            .justify_start()
198            .items_start()
199            .text_color(theme.lowest.base.default.foreground)
200            .fill(theme.lowest.base.default.background)
201            .child(TitleBar::new(cx).set_livestream(Some(Livestream {
202                players: random_players_with_call_status(7),
203                channel: Some("gpui2-ui".to_string()),
204            })))
205            .child(
206                div()
207                    .flex_1()
208                    .w_full()
209                    .flex()
210                    .flex_row()
211                    .overflow_hidden()
212                    .border_t()
213                    .border_b()
214                    .border_color(theme.lowest.base.default.border)
215                    .children(
216                        Some(
217                            Panel::new(
218                                self.left_panel_scroll_state.clone(),
219                                |_, payload| {
220                                    vec![ProjectPanel::new(ScrollState::default()).into_any()]
221                                },
222                                Box::new(()),
223                            )
224                            .side(PanelSide::Left),
225                        )
226                        .filter(|_| workspace_state.is_project_panel_open()),
227                    )
228                    .children(
229                        Some(
230                            Panel::new(
231                                self.left_panel_scroll_state.clone(),
232                                |_, payload| {
233                                    vec![CollabPanel::new(ScrollState::default()).into_any()]
234                                },
235                                Box::new(()),
236                            )
237                            .side(PanelSide::Left),
238                        )
239                        .filter(|_| workspace_state.is_collab_panel_open()),
240                    )
241                    .child(
242                        v_stack()
243                            .flex_1()
244                            .h_full()
245                            .child(
246                                div()
247                                    .flex()
248                                    .flex_1()
249                                    // CSS Hack: Flex 1 has to have a set height to properly fill the space
250                                    // Or it will give you a height of 0
251                                    // Marshall: We may not need this anymore with `gpui3`. It seems to render
252                                    //           fine without it.
253                                    .h_px()
254                                    .child(root_group),
255                            )
256                            .children(
257                                Some(
258                                    Panel::new(
259                                        self.bottom_panel_scroll_state.clone(),
260                                        |_, _| vec![Terminal::new().into_any()],
261                                        Box::new(()),
262                                    )
263                                    .allowed_sides(PanelAllowedSides::BottomOnly)
264                                    .side(PanelSide::Bottom),
265                                )
266                                .filter(|_| workspace_state.show_terminal.load(Ordering::SeqCst)),
267                            ),
268                    )
269                    .children(
270                        Some(
271                            Panel::new(
272                                self.right_panel_scroll_state.clone(),
273                                |_, payload| {
274                                    vec![ChatPanel::new(ScrollState::default())
275                                        .with_messages(vec![
276                                            ChatMessage::new(
277                                                "osiewicz".to_string(),
278                                                "is this thing on?".to_string(),
279                                                DateTime::parse_from_rfc3339(
280                                                    "2023-09-27T15:40:52.707Z",
281                                                )
282                                                .unwrap()
283                                                .naive_local(),
284                                            ),
285                                            ChatMessage::new(
286                                                "maxdeviant".to_string(),
287                                                "Reading you loud and clear!".to_string(),
288                                                DateTime::parse_from_rfc3339(
289                                                    "2023-09-28T15:40:52.707Z",
290                                                )
291                                                .unwrap()
292                                                .naive_local(),
293                                            ),
294                                        ])
295                                        .into_any()]
296                                },
297                                Box::new(()),
298                            )
299                            .side(PanelSide::Right),
300                        )
301                        .filter(|_| workspace_state.is_chat_panel_open()),
302                    )
303                    .children(
304                        Some(Panel::new(
305                            self.right_panel_scroll_state.clone(),
306                            |_, _| vec![AssistantPanel::new().into_any()],
307                            Box::new(()),
308                        ))
309                        .filter(|_| workspace_state.is_assistant_panel_open()),
310                    ),
311            )
312            .child(StatusBar::new())
313            .children(
314                Some(
315                    div()
316                        .absolute()
317                        .top(px(50.))
318                        .left(px(640.))
319                        .z_index(999)
320                        .child(LanguageSelector::new()),
321                )
322                .filter(|_| workspace_state.is_language_selector_open()),
323            )
324            .child(Toast::new(
325                ToastOrigin::Bottom,
326                |_, _| vec![Label::new("A toast").into_any()],
327                Box::new(()),
328            ))
329            .child(Toast::new(
330                ToastOrigin::BottomRight,
331                |_, _| vec![Label::new("Another toast").into_any()],
332                Box::new(()),
333            ))
334    }
335}
336
337#[cfg(feature = "stories")]
338pub use stories::*;
339
340#[cfg(feature = "stories")]
341mod stories {
342    use super::*;
343
344    #[derive(Element)]
345    pub struct WorkspaceStory<S: 'static + Send + Sync + Clone> {
346        state_type: PhantomData<S>,
347    }
348
349    impl<S: 'static + Send + Sync + Clone> WorkspaceStory<S> {
350        pub fn new() -> Self {
351            Self {
352                state_type: PhantomData,
353            }
354        }
355
356        fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
357            // Just render the workspace without any story boilerplate.
358            WorkspaceElement::new()
359        }
360    }
361}