pane_group.rs

  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}