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