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 if let Some((replica_id, leader)) = leader {
131 let view = match leader.location {
132 call::ParticipantLocation::Project {
133 project_id: leader_project_id,
134 } => {
135 if Some(leader_project_id) == project.read(cx).remote_id() {
136 ChildView::new(pane).boxed()
137 } else {
138 let leader_user = leader.user.clone();
139 let leader_user_id = leader.user.id;
140 MouseEventHandler::<FollowIntoExternalProject>::new(
141 pane.id(),
142 cx,
143 |_, _| {
144 Label::new(
145 format!(
146 "Follow {} on their currently active project",
147 leader_user.github_login,
148 ),
149 theme.workspace.external_location_message.text.clone(),
150 )
151 .contained()
152 .with_style(
153 theme.workspace.external_location_message.container,
154 )
155 .boxed()
156 },
157 )
158 .with_cursor_style(CursorStyle::PointingHand)
159 .on_click(MouseButton::Left, move |_, cx| {
160 cx.dispatch_action(JoinProject {
161 project_id: leader_project_id,
162 follow_user_id: leader_user_id,
163 })
164 })
165 .aligned()
166 .boxed()
167 }
168 }
169 call::ParticipantLocation::External => Label::new(
170 format!(
171 "{} is viewing a window outside of Zed",
172 leader.user.github_login
173 ),
174 theme.workspace.external_location_message.text.clone(),
175 )
176 .contained()
177 .with_style(theme.workspace.external_location_message.container)
178 .aligned()
179 .boxed(),
180 };
181
182 let leader_color = theme.editor.replica_selection_style(replica_id).cursor;
183 let mut border = Border::all(theme.workspace.leader_border_width, leader_color);
184 border
185 .color
186 .fade_out(1. - theme.workspace.leader_border_opacity);
187 border.overlay = true;
188 Container::new(view).with_border(border).boxed()
189 } else {
190 ChildView::new(pane).boxed()
191 }
192 }
193 Member::Axis(axis) => axis.render(project, theme, follower_states, cx),
194 }
195 }
196
197 fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
198 match self {
199 Member::Axis(axis) => {
200 for member in &axis.members {
201 member.collect_panes(panes);
202 }
203 }
204 Member::Pane(pane) => panes.push(pane),
205 }
206 }
207}
208
209#[derive(Clone, Debug, Eq, PartialEq)]
210struct PaneAxis {
211 axis: Axis,
212 members: Vec<Member>,
213}
214
215impl PaneAxis {
216 fn split(
217 &mut self,
218 old_pane: &ViewHandle<Pane>,
219 new_pane: &ViewHandle<Pane>,
220 direction: SplitDirection,
221 ) -> Result<()> {
222 use SplitDirection::*;
223
224 for (idx, member) in self.members.iter_mut().enumerate() {
225 match member {
226 Member::Axis(axis) => {
227 if axis.split(old_pane, new_pane, direction).is_ok() {
228 return Ok(());
229 }
230 }
231 Member::Pane(pane) => {
232 if pane == old_pane {
233 if direction.matches_axis(self.axis) {
234 match direction {
235 Up | Left => {
236 self.members.insert(idx, Member::Pane(new_pane.clone()));
237 }
238 Down | Right => {
239 self.members.insert(idx + 1, Member::Pane(new_pane.clone()));
240 }
241 }
242 } else {
243 *member =
244 Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
245 }
246 return Ok(());
247 }
248 }
249 }
250 }
251 Err(anyhow!("Pane not found"))
252 }
253
254 fn remove(&mut self, pane_to_remove: &ViewHandle<Pane>) -> Result<Option<Member>> {
255 let mut found_pane = false;
256 let mut remove_member = None;
257 for (idx, member) in self.members.iter_mut().enumerate() {
258 match member {
259 Member::Axis(axis) => {
260 if let Ok(last_pane) = axis.remove(pane_to_remove) {
261 if let Some(last_pane) = last_pane {
262 *member = last_pane;
263 }
264 found_pane = true;
265 break;
266 }
267 }
268 Member::Pane(pane) => {
269 if pane == pane_to_remove {
270 found_pane = true;
271 remove_member = Some(idx);
272 break;
273 }
274 }
275 }
276 }
277
278 if found_pane {
279 if let Some(idx) = remove_member {
280 self.members.remove(idx);
281 }
282
283 if self.members.len() == 1 {
284 Ok(self.members.pop())
285 } else {
286 Ok(None)
287 }
288 } else {
289 Err(anyhow!("Pane not found"))
290 }
291 }
292
293 fn render(
294 &self,
295 project: &ModelHandle<Project>,
296 theme: &Theme,
297 follower_state: &FollowerStatesByLeader,
298 cx: &mut RenderContext<Workspace>,
299 ) -> ElementBox {
300 let last_member_ix = self.members.len() - 1;
301 Flex::new(self.axis)
302 .with_children(self.members.iter().enumerate().map(|(ix, member)| {
303 let mut member = member.render(project, theme, follower_state, cx);
304 if ix < last_member_ix {
305 let mut border = theme.workspace.pane_divider;
306 border.left = false;
307 border.right = false;
308 border.top = false;
309 border.bottom = false;
310 match self.axis {
311 Axis::Vertical => border.bottom = true,
312 Axis::Horizontal => border.right = true,
313 }
314 member = Container::new(member).with_border(border).boxed();
315 }
316
317 FlexItem::new(member).flex(1.0, true).boxed()
318 }))
319 .boxed()
320 }
321}
322
323#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
324pub enum SplitDirection {
325 Up,
326 Down,
327 Left,
328 Right,
329}
330
331impl SplitDirection {
332 fn matches_axis(self, orientation: Axis) -> bool {
333 use Axis::*;
334 use SplitDirection::*;
335
336 match self {
337 Up | Down => match orientation {
338 Vertical => true,
339 Horizontal => false,
340 },
341 Left | Right => match orientation {
342 Vertical => false,
343 Horizontal => true,
344 },
345 }
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 // use super::*;
352 // use serde_json::json;
353
354 // #[test]
355 // fn test_split_and_remove() -> Result<()> {
356 // let mut group = PaneGroup::new(1);
357 // assert_eq!(
358 // serde_json::to_value(&group)?,
359 // json!({
360 // "type": "pane",
361 // "paneId": 1,
362 // })
363 // );
364
365 // group.split(1, 2, SplitDirection::Right)?;
366 // assert_eq!(
367 // serde_json::to_value(&group)?,
368 // json!({
369 // "type": "axis",
370 // "orientation": "horizontal",
371 // "members": [
372 // {"type": "pane", "paneId": 1},
373 // {"type": "pane", "paneId": 2},
374 // ]
375 // })
376 // );
377
378 // group.split(2, 3, SplitDirection::Up)?;
379 // assert_eq!(
380 // serde_json::to_value(&group)?,
381 // json!({
382 // "type": "axis",
383 // "orientation": "horizontal",
384 // "members": [
385 // {"type": "pane", "paneId": 1},
386 // {
387 // "type": "axis",
388 // "orientation": "vertical",
389 // "members": [
390 // {"type": "pane", "paneId": 3},
391 // {"type": "pane", "paneId": 2},
392 // ]
393 // },
394 // ]
395 // })
396 // );
397
398 // group.split(1, 4, SplitDirection::Right)?;
399 // assert_eq!(
400 // serde_json::to_value(&group)?,
401 // json!({
402 // "type": "axis",
403 // "orientation": "horizontal",
404 // "members": [
405 // {"type": "pane", "paneId": 1},
406 // {"type": "pane", "paneId": 4},
407 // {
408 // "type": "axis",
409 // "orientation": "vertical",
410 // "members": [
411 // {"type": "pane", "paneId": 3},
412 // {"type": "pane", "paneId": 2},
413 // ]
414 // },
415 // ]
416 // })
417 // );
418
419 // group.split(2, 5, SplitDirection::Up)?;
420 // assert_eq!(
421 // serde_json::to_value(&group)?,
422 // json!({
423 // "type": "axis",
424 // "orientation": "horizontal",
425 // "members": [
426 // {"type": "pane", "paneId": 1},
427 // {"type": "pane", "paneId": 4},
428 // {
429 // "type": "axis",
430 // "orientation": "vertical",
431 // "members": [
432 // {"type": "pane", "paneId": 3},
433 // {"type": "pane", "paneId": 5},
434 // {"type": "pane", "paneId": 2},
435 // ]
436 // },
437 // ]
438 // })
439 // );
440
441 // assert_eq!(true, group.remove(5)?);
442 // assert_eq!(
443 // serde_json::to_value(&group)?,
444 // json!({
445 // "type": "axis",
446 // "orientation": "horizontal",
447 // "members": [
448 // {"type": "pane", "paneId": 1},
449 // {"type": "pane", "paneId": 4},
450 // {
451 // "type": "axis",
452 // "orientation": "vertical",
453 // "members": [
454 // {"type": "pane", "paneId": 3},
455 // {"type": "pane", "paneId": 2},
456 // ]
457 // },
458 // ]
459 // })
460 // );
461
462 // assert_eq!(true, group.remove(4)?);
463 // assert_eq!(
464 // serde_json::to_value(&group)?,
465 // json!({
466 // "type": "axis",
467 // "orientation": "horizontal",
468 // "members": [
469 // {"type": "pane", "paneId": 1},
470 // {
471 // "type": "axis",
472 // "orientation": "vertical",
473 // "members": [
474 // {"type": "pane", "paneId": 3},
475 // {"type": "pane", "paneId": 2},
476 // ]
477 // },
478 // ]
479 // })
480 // );
481
482 // assert_eq!(true, group.remove(3)?);
483 // assert_eq!(
484 // serde_json::to_value(&group)?,
485 // json!({
486 // "type": "axis",
487 // "orientation": "horizontal",
488 // "members": [
489 // {"type": "pane", "paneId": 1},
490 // {"type": "pane", "paneId": 2},
491 // ]
492 // })
493 // );
494
495 // assert_eq!(true, group.remove(2)?);
496 // assert_eq!(
497 // serde_json::to_value(&group)?,
498 // json!({
499 // "type": "pane",
500 // "paneId": 1,
501 // })
502 // );
503
504 // assert_eq!(false, group.remove(1)?);
505 // assert_eq!(
506 // serde_json::to_value(&group)?,
507 // json!({
508 // "type": "pane",
509 // "paneId": 1,
510 // })
511 // );
512
513 // Ok(())
514 // }
515}