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::Vector2F},
  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 prompt = if let Some((_, leader)) = leader {
148                    match leader.location {
149                        ParticipantLocation::SharedProject {
150                            project_id: leader_project_id,
151                        } => {
152                            if Some(leader_project_id) == project.read(cx).remote_id() {
153                                None
154                            } else {
155                                let leader_user = leader.user.clone();
156                                let leader_user_id = leader.user.id;
157                                Some(
158                                    MouseEventHandler::<FollowIntoExternalProject>::new(
159                                        pane.id(),
160                                        cx,
161                                        |_, _| {
162                                            Label::new(
163                                                format!(
164                                                    "Follow {} on their active project",
165                                                    leader_user.github_login,
166                                                ),
167                                                theme
168                                                    .workspace
169                                                    .external_location_message
170                                                    .text
171                                                    .clone(),
172                                            )
173                                            .contained()
174                                            .with_style(
175                                                theme.workspace.external_location_message.container,
176                                            )
177                                            .boxed()
178                                        },
179                                    )
180                                    .with_cursor_style(CursorStyle::PointingHand)
181                                    .on_click(MouseButton::Left, move |_, cx| {
182                                        cx.dispatch_action(JoinProject {
183                                            project_id: leader_project_id,
184                                            follow_user_id: leader_user_id,
185                                        })
186                                    })
187                                    .aligned()
188                                    .bottom()
189                                    .right()
190                                    .boxed(),
191                                )
192                            }
193                        }
194                        ParticipantLocation::UnsharedProject => Some(
195                            Label::new(
196                                format!(
197                                    "{} is viewing an unshared Zed project",
198                                    leader.user.github_login
199                                ),
200                                theme.workspace.external_location_message.text.clone(),
201                            )
202                            .contained()
203                            .with_style(theme.workspace.external_location_message.container)
204                            .aligned()
205                            .bottom()
206                            .right()
207                            .boxed(),
208                        ),
209                        ParticipantLocation::External => Some(
210                            Label::new(
211                                format!(
212                                    "{} is viewing a window outside of Zed",
213                                    leader.user.github_login
214                                ),
215                                theme.workspace.external_location_message.text.clone(),
216                            )
217                            .contained()
218                            .with_style(theme.workspace.external_location_message.container)
219                            .aligned()
220                            .bottom()
221                            .right()
222                            .boxed(),
223                        ),
224                    }
225                } else {
226                    None
227                };
228
229                Stack::new()
230                    .with_child(
231                        ChildView::new(pane, cx)
232                            .contained()
233                            .with_border(border)
234                            .boxed(),
235                    )
236                    .with_children(prompt)
237                    .boxed()
238            }
239            Member::Axis(axis) => axis.render(project, theme, follower_states, active_call, cx),
240        }
241    }
242
243    fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
244        match self {
245            Member::Axis(axis) => {
246                for member in &axis.members {
247                    member.collect_panes(panes);
248                }
249            }
250            Member::Pane(pane) => panes.push(pane),
251        }
252    }
253}
254
255#[derive(Clone, Debug, Eq, PartialEq)]
256struct PaneAxis {
257    axis: Axis,
258    members: Vec<Member>,
259}
260
261impl PaneAxis {
262    fn split(
263        &mut self,
264        old_pane: &ViewHandle<Pane>,
265        new_pane: &ViewHandle<Pane>,
266        direction: SplitDirection,
267    ) -> Result<()> {
268        for (mut 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.axis() == self.axis {
278                            if direction.increasing() {
279                                idx += 1;
280                            }
281
282                            self.members.insert(idx, Member::Pane(new_pane.clone()));
283                        } else {
284                            *member =
285                                Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
286                        }
287                        return Ok(());
288                    }
289                }
290            }
291        }
292        Err(anyhow!("Pane not found"))
293    }
294
295    fn remove(&mut self, pane_to_remove: &ViewHandle<Pane>) -> Result<Option<Member>> {
296        let mut found_pane = false;
297        let mut remove_member = None;
298        for (idx, member) in self.members.iter_mut().enumerate() {
299            match member {
300                Member::Axis(axis) => {
301                    if let Ok(last_pane) = axis.remove(pane_to_remove) {
302                        if let Some(last_pane) = last_pane {
303                            *member = last_pane;
304                        }
305                        found_pane = true;
306                        break;
307                    }
308                }
309                Member::Pane(pane) => {
310                    if pane == pane_to_remove {
311                        found_pane = true;
312                        remove_member = Some(idx);
313                        break;
314                    }
315                }
316            }
317        }
318
319        if found_pane {
320            if let Some(idx) = remove_member {
321                self.members.remove(idx);
322            }
323
324            if self.members.len() == 1 {
325                Ok(self.members.pop())
326            } else {
327                Ok(None)
328            }
329        } else {
330            Err(anyhow!("Pane not found"))
331        }
332    }
333
334    fn render(
335        &self,
336        project: &ModelHandle<Project>,
337        theme: &Theme,
338        follower_state: &FollowerStatesByLeader,
339        active_call: Option<&ModelHandle<ActiveCall>>,
340        cx: &mut RenderContext<Workspace>,
341    ) -> ElementBox {
342        let last_member_ix = self.members.len() - 1;
343        Flex::new(self.axis)
344            .with_children(self.members.iter().enumerate().map(|(ix, member)| {
345                let mut member = member.render(project, theme, follower_state, active_call, cx);
346                if ix < last_member_ix {
347                    let mut border = theme.workspace.pane_divider;
348                    border.left = false;
349                    border.right = false;
350                    border.top = false;
351                    border.bottom = false;
352                    match self.axis {
353                        Axis::Vertical => border.bottom = true,
354                        Axis::Horizontal => border.right = true,
355                    }
356                    member = Container::new(member).with_border(border).boxed();
357                }
358
359                FlexItem::new(member).flex(1.0, true).boxed()
360            }))
361            .boxed()
362    }
363}
364
365#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
366pub enum SplitDirection {
367    Up,
368    Down,
369    Left,
370    Right,
371}
372
373impl SplitDirection {
374    pub fn all() -> [Self; 4] {
375        [Self::Up, Self::Down, Self::Left, Self::Right]
376    }
377
378    pub fn edge(&self, rect: RectF) -> f32 {
379        match self {
380            Self::Up => rect.min_y(),
381            Self::Down => rect.max_y(),
382            Self::Left => rect.min_x(),
383            Self::Right => rect.max_x(),
384        }
385    }
386
387    // Returns a new rectangle which shares an edge in SplitDirection and has `size` along SplitDirection
388    pub fn along_edge(&self, rect: RectF, size: f32) -> RectF {
389        match self {
390            Self::Up => RectF::new(rect.origin(), Vector2F::new(rect.width(), size)),
391            Self::Down => RectF::new(
392                rect.lower_left() - Vector2F::new(0., size),
393                Vector2F::new(rect.width(), size),
394            ),
395            Self::Left => RectF::new(rect.origin(), Vector2F::new(size, rect.height())),
396            Self::Right => RectF::new(
397                rect.upper_right() - Vector2F::new(size, 0.),
398                Vector2F::new(size, rect.height()),
399            ),
400        }
401    }
402
403    pub fn axis(&self) -> Axis {
404        match self {
405            Self::Up | Self::Down => Axis::Vertical,
406            Self::Left | Self::Right => Axis::Horizontal,
407        }
408    }
409
410    pub fn increasing(&self) -> bool {
411        match self {
412            Self::Left | Self::Up => false,
413            Self::Down | Self::Right => true,
414        }
415    }
416}