1use crate::{FollowerStatesByLeader, JoinProject, Pane, Workspace};
2use anyhow::{anyhow, Result};
3use call::ActiveCall;
4use gpui::{
5 elements::*, Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle,
6};
7use project::Project;
8use serde::Deserialize;
9use theme::Theme;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct PaneGroup {
13 root: Member,
14}
15
16impl PaneGroup {
17 pub fn new(pane: ViewHandle<Pane>) -> Self {
18 Self {
19 root: Member::Pane(pane),
20 }
21 }
22
23 pub fn split(
24 &mut self,
25 old_pane: &ViewHandle<Pane>,
26 new_pane: &ViewHandle<Pane>,
27 direction: SplitDirection,
28 ) -> Result<()> {
29 match &mut self.root {
30 Member::Pane(pane) => {
31 if pane == old_pane {
32 self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
33 Ok(())
34 } else {
35 Err(anyhow!("Pane not found"))
36 }
37 }
38 Member::Axis(axis) => axis.split(old_pane, new_pane, direction),
39 }
40 }
41
42 /// Returns:
43 /// - Ok(true) if it found and removed a pane
44 /// - Ok(false) if it found but did not remove the pane
45 /// - Err(_) if it did not find the pane
46 pub fn remove(&mut self, pane: &ViewHandle<Pane>) -> Result<bool> {
47 match &mut self.root {
48 Member::Pane(_) => Ok(false),
49 Member::Axis(axis) => {
50 if let Some(last_pane) = axis.remove(pane)? {
51 self.root = last_pane;
52 }
53 Ok(true)
54 }
55 }
56 }
57
58 pub(crate) fn render(
59 &self,
60 project: &ModelHandle<Project>,
61 theme: &Theme,
62 follower_states: &FollowerStatesByLeader,
63 cx: &mut RenderContext<Workspace>,
64 ) -> ElementBox {
65 self.root.render(project, theme, follower_states, cx)
66 }
67
68 pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
69 let mut panes = Vec::new();
70 self.root.collect_panes(&mut panes);
71 panes
72 }
73}
74
75#[derive(Clone, Debug, Eq, PartialEq)]
76enum Member {
77 Axis(PaneAxis),
78 Pane(ViewHandle<Pane>),
79}
80
81impl Member {
82 fn new_axis(
83 old_pane: ViewHandle<Pane>,
84 new_pane: ViewHandle<Pane>,
85 direction: SplitDirection,
86 ) -> Self {
87 use Axis::*;
88 use SplitDirection::*;
89
90 let axis = match direction {
91 Up | Down => Vertical,
92 Left | Right => Horizontal,
93 };
94
95 let members = match direction {
96 Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)],
97 Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)],
98 };
99
100 Member::Axis(PaneAxis { axis, members })
101 }
102
103 pub fn render(
104 &self,
105 project: &ModelHandle<Project>,
106 theme: &Theme,
107 follower_states: &FollowerStatesByLeader,
108 cx: &mut RenderContext<Workspace>,
109 ) -> ElementBox {
110 enum FollowIntoExternalProject {}
111
112 match self {
113 Member::Pane(pane) => {
114 let leader = follower_states
115 .iter()
116 .find_map(|(leader_id, follower_states)| {
117 if follower_states.contains_key(pane) {
118 Some(leader_id)
119 } else {
120 None
121 }
122 })
123 .and_then(|leader_id| {
124 let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
125 let collaborator = project.read(cx).collaborators().get(leader_id)?;
126 let participant = room.remote_participants().get(&leader_id)?;
127 Some((collaborator.replica_id, participant))
128 });
129
130 let mut border = Border::default();
131
132 let prompt = if let Some((replica_id, leader)) = leader {
133 let leader_color = theme.editor.replica_selection_style(replica_id).cursor;
134 border = Border::all(theme.workspace.leader_border_width, leader_color);
135 border
136 .color
137 .fade_out(1. - theme.workspace.leader_border_opacity);
138 border.overlay = true;
139
140 match leader.location {
141 call::ParticipantLocation::Project {
142 project_id: leader_project_id,
143 } => {
144 if Some(leader_project_id) == project.read(cx).remote_id() {
145 None
146 } else {
147 let leader_user = leader.user.clone();
148 let leader_user_id = leader.user.id;
149 Some(
150 MouseEventHandler::<FollowIntoExternalProject>::new(
151 pane.id(),
152 cx,
153 |_, _| {
154 Label::new(
155 format!(
156 "Follow {} on their active project",
157 leader_user.github_login,
158 ),
159 theme
160 .workspace
161 .external_location_message
162 .text
163 .clone(),
164 )
165 .contained()
166 .with_style(
167 theme.workspace.external_location_message.container,
168 )
169 .boxed()
170 },
171 )
172 .with_cursor_style(CursorStyle::PointingHand)
173 .on_click(MouseButton::Left, move |_, cx| {
174 cx.dispatch_action(JoinProject {
175 project_id: leader_project_id,
176 follow_user_id: leader_user_id,
177 })
178 })
179 .aligned()
180 .bottom()
181 .right()
182 .boxed(),
183 )
184 }
185 }
186 call::ParticipantLocation::External => Some(
187 Label::new(
188 format!(
189 "{} is viewing a window outside of Zed",
190 leader.user.github_login
191 ),
192 theme.workspace.external_location_message.text.clone(),
193 )
194 .contained()
195 .with_style(theme.workspace.external_location_message.container)
196 .aligned()
197 .bottom()
198 .right()
199 .boxed(),
200 ),
201 }
202 } else {
203 None
204 };
205
206 Stack::new()
207 .with_child(ChildView::new(pane).contained().with_border(border).boxed())
208 .with_children(prompt)
209 .boxed()
210 }
211 Member::Axis(axis) => axis.render(project, theme, follower_states, cx),
212 }
213 }
214
215 fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
216 match self {
217 Member::Axis(axis) => {
218 for member in &axis.members {
219 member.collect_panes(panes);
220 }
221 }
222 Member::Pane(pane) => panes.push(pane),
223 }
224 }
225}
226
227#[derive(Clone, Debug, Eq, PartialEq)]
228struct PaneAxis {
229 axis: Axis,
230 members: Vec<Member>,
231}
232
233impl PaneAxis {
234 fn split(
235 &mut self,
236 old_pane: &ViewHandle<Pane>,
237 new_pane: &ViewHandle<Pane>,
238 direction: SplitDirection,
239 ) -> Result<()> {
240 use SplitDirection::*;
241
242 for (idx, member) in self.members.iter_mut().enumerate() {
243 match member {
244 Member::Axis(axis) => {
245 if axis.split(old_pane, new_pane, direction).is_ok() {
246 return Ok(());
247 }
248 }
249 Member::Pane(pane) => {
250 if pane == old_pane {
251 if direction.matches_axis(self.axis) {
252 match direction {
253 Up | Left => {
254 self.members.insert(idx, Member::Pane(new_pane.clone()));
255 }
256 Down | Right => {
257 self.members.insert(idx + 1, Member::Pane(new_pane.clone()));
258 }
259 }
260 } else {
261 *member =
262 Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
263 }
264 return Ok(());
265 }
266 }
267 }
268 }
269 Err(anyhow!("Pane not found"))
270 }
271
272 fn remove(&mut self, pane_to_remove: &ViewHandle<Pane>) -> Result<Option<Member>> {
273 let mut found_pane = false;
274 let mut remove_member = None;
275 for (idx, member) in self.members.iter_mut().enumerate() {
276 match member {
277 Member::Axis(axis) => {
278 if let Ok(last_pane) = axis.remove(pane_to_remove) {
279 if let Some(last_pane) = last_pane {
280 *member = last_pane;
281 }
282 found_pane = true;
283 break;
284 }
285 }
286 Member::Pane(pane) => {
287 if pane == pane_to_remove {
288 found_pane = true;
289 remove_member = Some(idx);
290 break;
291 }
292 }
293 }
294 }
295
296 if found_pane {
297 if let Some(idx) = remove_member {
298 self.members.remove(idx);
299 }
300
301 if self.members.len() == 1 {
302 Ok(self.members.pop())
303 } else {
304 Ok(None)
305 }
306 } else {
307 Err(anyhow!("Pane not found"))
308 }
309 }
310
311 fn render(
312 &self,
313 project: &ModelHandle<Project>,
314 theme: &Theme,
315 follower_state: &FollowerStatesByLeader,
316 cx: &mut RenderContext<Workspace>,
317 ) -> ElementBox {
318 let last_member_ix = self.members.len() - 1;
319 Flex::new(self.axis)
320 .with_children(self.members.iter().enumerate().map(|(ix, member)| {
321 let mut member = member.render(project, theme, follower_state, cx);
322 if ix < last_member_ix {
323 let mut border = theme.workspace.pane_divider;
324 border.left = false;
325 border.right = false;
326 border.top = false;
327 border.bottom = false;
328 match self.axis {
329 Axis::Vertical => border.bottom = true,
330 Axis::Horizontal => border.right = true,
331 }
332 member = Container::new(member).with_border(border).boxed();
333 }
334
335 FlexItem::new(member).flex(1.0, true).boxed()
336 }))
337 .boxed()
338 }
339}
340
341#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
342pub enum SplitDirection {
343 Up,
344 Down,
345 Left,
346 Right,
347}
348
349impl SplitDirection {
350 fn matches_axis(self, orientation: Axis) -> bool {
351 use Axis::*;
352 use SplitDirection::*;
353
354 match self {
355 Up | Down => match orientation {
356 Vertical => true,
357 Horizontal => false,
358 },
359 Left | Right => match orientation {
360 Vertical => false,
361 Horizontal => true,
362 },
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 // use super::*;
370 // use serde_json::json;
371
372 // #[test]
373 // fn test_split_and_remove() -> Result<()> {
374 // let mut group = PaneGroup::new(1);
375 // assert_eq!(
376 // serde_json::to_value(&group)?,
377 // json!({
378 // "type": "pane",
379 // "paneId": 1,
380 // })
381 // );
382
383 // group.split(1, 2, SplitDirection::Right)?;
384 // assert_eq!(
385 // serde_json::to_value(&group)?,
386 // json!({
387 // "type": "axis",
388 // "orientation": "horizontal",
389 // "members": [
390 // {"type": "pane", "paneId": 1},
391 // {"type": "pane", "paneId": 2},
392 // ]
393 // })
394 // );
395
396 // group.split(2, 3, SplitDirection::Up)?;
397 // assert_eq!(
398 // serde_json::to_value(&group)?,
399 // json!({
400 // "type": "axis",
401 // "orientation": "horizontal",
402 // "members": [
403 // {"type": "pane", "paneId": 1},
404 // {
405 // "type": "axis",
406 // "orientation": "vertical",
407 // "members": [
408 // {"type": "pane", "paneId": 3},
409 // {"type": "pane", "paneId": 2},
410 // ]
411 // },
412 // ]
413 // })
414 // );
415
416 // group.split(1, 4, SplitDirection::Right)?;
417 // assert_eq!(
418 // serde_json::to_value(&group)?,
419 // json!({
420 // "type": "axis",
421 // "orientation": "horizontal",
422 // "members": [
423 // {"type": "pane", "paneId": 1},
424 // {"type": "pane", "paneId": 4},
425 // {
426 // "type": "axis",
427 // "orientation": "vertical",
428 // "members": [
429 // {"type": "pane", "paneId": 3},
430 // {"type": "pane", "paneId": 2},
431 // ]
432 // },
433 // ]
434 // })
435 // );
436
437 // group.split(2, 5, SplitDirection::Up)?;
438 // assert_eq!(
439 // serde_json::to_value(&group)?,
440 // json!({
441 // "type": "axis",
442 // "orientation": "horizontal",
443 // "members": [
444 // {"type": "pane", "paneId": 1},
445 // {"type": "pane", "paneId": 4},
446 // {
447 // "type": "axis",
448 // "orientation": "vertical",
449 // "members": [
450 // {"type": "pane", "paneId": 3},
451 // {"type": "pane", "paneId": 5},
452 // {"type": "pane", "paneId": 2},
453 // ]
454 // },
455 // ]
456 // })
457 // );
458
459 // assert_eq!(true, group.remove(5)?);
460 // assert_eq!(
461 // serde_json::to_value(&group)?,
462 // json!({
463 // "type": "axis",
464 // "orientation": "horizontal",
465 // "members": [
466 // {"type": "pane", "paneId": 1},
467 // {"type": "pane", "paneId": 4},
468 // {
469 // "type": "axis",
470 // "orientation": "vertical",
471 // "members": [
472 // {"type": "pane", "paneId": 3},
473 // {"type": "pane", "paneId": 2},
474 // ]
475 // },
476 // ]
477 // })
478 // );
479
480 // assert_eq!(true, group.remove(4)?);
481 // assert_eq!(
482 // serde_json::to_value(&group)?,
483 // json!({
484 // "type": "axis",
485 // "orientation": "horizontal",
486 // "members": [
487 // {"type": "pane", "paneId": 1},
488 // {
489 // "type": "axis",
490 // "orientation": "vertical",
491 // "members": [
492 // {"type": "pane", "paneId": 3},
493 // {"type": "pane", "paneId": 2},
494 // ]
495 // },
496 // ]
497 // })
498 // );
499
500 // assert_eq!(true, group.remove(3)?);
501 // assert_eq!(
502 // serde_json::to_value(&group)?,
503 // json!({
504 // "type": "axis",
505 // "orientation": "horizontal",
506 // "members": [
507 // {"type": "pane", "paneId": 1},
508 // {"type": "pane", "paneId": 2},
509 // ]
510 // })
511 // );
512
513 // assert_eq!(true, group.remove(2)?);
514 // assert_eq!(
515 // serde_json::to_value(&group)?,
516 // json!({
517 // "type": "pane",
518 // "paneId": 1,
519 // })
520 // );
521
522 // assert_eq!(false, group.remove(1)?);
523 // assert_eq!(
524 // serde_json::to_value(&group)?,
525 // json!({
526 // "type": "pane",
527 // "paneId": 1,
528 // })
529 // );
530
531 // Ok(())
532 // }
533}