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