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