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