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}