pane_group.rs

  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}