pane_group.rs

  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}