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