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 settings::Settings;
 12use theme::Theme;
 13
 14#[derive(Clone, Debug, Eq, PartialEq)]
 15pub struct PaneGroup {
 16    root: Member,
 17}
 18
 19impl PaneGroup {
 20    pub fn new(pane: ViewHandle<Pane>) -> Self {
 21        Self {
 22            root: Member::Pane(pane),
 23        }
 24    }
 25
 26    pub fn split(
 27        &mut self,
 28        old_pane: &ViewHandle<Pane>,
 29        new_pane: &ViewHandle<Pane>,
 30        direction: SplitDirection,
 31    ) -> Result<()> {
 32        match &mut self.root {
 33            Member::Pane(pane) => {
 34                if pane == old_pane {
 35                    self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
 36                    Ok(())
 37                } else {
 38                    Err(anyhow!("Pane not found"))
 39                }
 40            }
 41            Member::Axis(axis) => axis.split(old_pane, new_pane, direction),
 42        }
 43    }
 44
 45    /// Returns:
 46    /// - Ok(true) if it found and removed a pane
 47    /// - Ok(false) if it found but did not remove the pane
 48    /// - Err(_) if it did not find the pane
 49    pub fn remove(&mut self, pane: &ViewHandle<Pane>) -> Result<bool> {
 50        match &mut self.root {
 51            Member::Pane(_) => Ok(false),
 52            Member::Axis(axis) => {
 53                if let Some(last_pane) = axis.remove(pane)? {
 54                    self.root = last_pane;
 55                }
 56                Ok(true)
 57            }
 58        }
 59    }
 60
 61    pub(crate) fn render(
 62        &self,
 63        project: &ModelHandle<Project>,
 64        theme: &Theme,
 65        follower_states: &FollowerStatesByLeader,
 66        active_call: Option<&ModelHandle<ActiveCall>>,
 67        active_pane: &ViewHandle<Pane>,
 68        cx: &mut RenderContext<Workspace>,
 69    ) -> ElementBox {
 70        self.root.render(
 71            project,
 72            theme,
 73            follower_states,
 74            active_call,
 75            active_pane,
 76            cx,
 77        )
 78    }
 79
 80    pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
 81        let mut panes = Vec::new();
 82        self.root.collect_panes(&mut panes);
 83        panes
 84    }
 85}
 86
 87#[derive(Clone, Debug, Eq, PartialEq)]
 88enum Member {
 89    Axis(PaneAxis),
 90    Pane(ViewHandle<Pane>),
 91}
 92
 93impl Member {
 94    fn new_axis(
 95        old_pane: ViewHandle<Pane>,
 96        new_pane: ViewHandle<Pane>,
 97        direction: SplitDirection,
 98    ) -> Self {
 99        use Axis::*;
100        use SplitDirection::*;
101
102        let axis = match direction {
103            Up | Down => Vertical,
104            Left | Right => Horizontal,
105        };
106
107        let members = match direction {
108            Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)],
109            Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)],
110        };
111
112        Member::Axis(PaneAxis { axis, members })
113    }
114
115    fn contains(&self, needle: &ViewHandle<Pane>) -> bool {
116        match self {
117            Member::Axis(axis) => axis.members.iter().any(|member| member.contains(needle)),
118            Member::Pane(pane) => pane == needle,
119        }
120    }
121
122    pub fn render(
123        &self,
124        project: &ModelHandle<Project>,
125        theme: &Theme,
126        follower_states: &FollowerStatesByLeader,
127        active_call: Option<&ModelHandle<ActiveCall>>,
128        active_pane: &ViewHandle<Pane>,
129        cx: &mut RenderContext<Workspace>,
130    ) -> ElementBox {
131        enum FollowIntoExternalProject {}
132
133        match self {
134            Member::Pane(pane) => {
135                let leader = follower_states
136                    .iter()
137                    .find_map(|(leader_id, follower_states)| {
138                        if follower_states.contains_key(pane) {
139                            Some(leader_id)
140                        } else {
141                            None
142                        }
143                    })
144                    .and_then(|leader_id| {
145                        let room = active_call?.read(cx).room()?.read(cx);
146                        let collaborator = project.read(cx).collaborators().get(leader_id)?;
147                        let participant = room.remote_participants().get(&leader_id)?;
148                        Some((collaborator.replica_id, participant))
149                    });
150
151                let border = if let Some((replica_id, _)) = leader.as_ref() {
152                    let leader_color = theme.editor.replica_selection_style(*replica_id).cursor;
153                    let mut border = Border::all(theme.workspace.leader_border_width, leader_color);
154                    border
155                        .color
156                        .fade_out(1. - theme.workspace.leader_border_opacity);
157                    border.overlay = true;
158                    border
159                } else {
160                    Border::default()
161                };
162
163                let prompt = if let Some((_, leader)) = leader {
164                    match leader.location {
165                        ParticipantLocation::SharedProject {
166                            project_id: leader_project_id,
167                        } => {
168                            if Some(leader_project_id) == project.read(cx).remote_id() {
169                                None
170                            } else {
171                                let leader_user = leader.user.clone();
172                                let leader_user_id = leader.user.id;
173                                Some(
174                                    MouseEventHandler::<FollowIntoExternalProject>::new(
175                                        pane.id(),
176                                        cx,
177                                        |_, _| {
178                                            Label::new(
179                                                format!(
180                                                    "Follow {} on their active project",
181                                                    leader_user.github_login,
182                                                ),
183                                                theme
184                                                    .workspace
185                                                    .external_location_message
186                                                    .text
187                                                    .clone(),
188                                            )
189                                            .contained()
190                                            .with_style(
191                                                theme.workspace.external_location_message.container,
192                                            )
193                                            .boxed()
194                                        },
195                                    )
196                                    .with_cursor_style(CursorStyle::PointingHand)
197                                    .on_click(MouseButton::Left, move |_, cx| {
198                                        cx.dispatch_action(JoinProject {
199                                            project_id: leader_project_id,
200                                            follow_user_id: leader_user_id,
201                                        })
202                                    })
203                                    .aligned()
204                                    .bottom()
205                                    .right()
206                                    .boxed(),
207                                )
208                            }
209                        }
210                        ParticipantLocation::UnsharedProject => Some(
211                            Label::new(
212                                format!(
213                                    "{} is viewing an unshared Zed project",
214                                    leader.user.github_login
215                                ),
216                                theme.workspace.external_location_message.text.clone(),
217                            )
218                            .contained()
219                            .with_style(theme.workspace.external_location_message.container)
220                            .aligned()
221                            .bottom()
222                            .right()
223                            .boxed(),
224                        ),
225                        ParticipantLocation::External => Some(
226                            Label::new(
227                                format!(
228                                    "{} is viewing a window outside of Zed",
229                                    leader.user.github_login
230                                ),
231                                theme.workspace.external_location_message.text.clone(),
232                            )
233                            .contained()
234                            .with_style(theme.workspace.external_location_message.container)
235                            .aligned()
236                            .bottom()
237                            .right()
238                            .boxed(),
239                        ),
240                    }
241                } else {
242                    None
243                };
244
245                Stack::new()
246                    .with_child(
247                        ChildView::new(pane, cx)
248                            .contained()
249                            .with_border(border)
250                            .boxed(),
251                    )
252                    .with_children(prompt)
253                    .boxed()
254            }
255            Member::Axis(axis) => axis.render(
256                project,
257                theme,
258                follower_states,
259                active_call,
260                active_pane,
261                cx,
262            ),
263        }
264    }
265
266    fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
267        match self {
268            Member::Axis(axis) => {
269                for member in &axis.members {
270                    member.collect_panes(panes);
271                }
272            }
273            Member::Pane(pane) => panes.push(pane),
274        }
275    }
276}
277
278#[derive(Clone, Debug, Eq, PartialEq)]
279struct PaneAxis {
280    axis: Axis,
281    members: Vec<Member>,
282}
283
284impl PaneAxis {
285    fn split(
286        &mut self,
287        old_pane: &ViewHandle<Pane>,
288        new_pane: &ViewHandle<Pane>,
289        direction: SplitDirection,
290    ) -> Result<()> {
291        for (mut idx, member) in self.members.iter_mut().enumerate() {
292            match member {
293                Member::Axis(axis) => {
294                    if axis.split(old_pane, new_pane, direction).is_ok() {
295                        return Ok(());
296                    }
297                }
298                Member::Pane(pane) => {
299                    if pane == old_pane {
300                        if direction.axis() == self.axis {
301                            if direction.increasing() {
302                                idx += 1;
303                            }
304
305                            self.members.insert(idx, Member::Pane(new_pane.clone()));
306                        } else {
307                            *member =
308                                Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
309                        }
310                        return Ok(());
311                    }
312                }
313            }
314        }
315        Err(anyhow!("Pane not found"))
316    }
317
318    fn remove(&mut self, pane_to_remove: &ViewHandle<Pane>) -> Result<Option<Member>> {
319        let mut found_pane = false;
320        let mut remove_member = None;
321        for (idx, member) in self.members.iter_mut().enumerate() {
322            match member {
323                Member::Axis(axis) => {
324                    if let Ok(last_pane) = axis.remove(pane_to_remove) {
325                        if let Some(last_pane) = last_pane {
326                            *member = last_pane;
327                        }
328                        found_pane = true;
329                        break;
330                    }
331                }
332                Member::Pane(pane) => {
333                    if pane == pane_to_remove {
334                        found_pane = true;
335                        remove_member = Some(idx);
336                        break;
337                    }
338                }
339            }
340        }
341
342        if found_pane {
343            if let Some(idx) = remove_member {
344                self.members.remove(idx);
345            }
346
347            if self.members.len() == 1 {
348                Ok(self.members.pop())
349            } else {
350                Ok(None)
351            }
352        } else {
353            Err(anyhow!("Pane not found"))
354        }
355    }
356
357    fn render(
358        &self,
359        project: &ModelHandle<Project>,
360        theme: &Theme,
361        follower_state: &FollowerStatesByLeader,
362        active_call: Option<&ModelHandle<ActiveCall>>,
363        active_pane: &ViewHandle<Pane>,
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 flex = 1.0;
370                if member.contains(active_pane) {
371                    flex = cx.global::<Settings>().active_pane_magnification;
372                }
373
374                let mut member =
375                    member.render(project, theme, follower_state, active_call, active_pane, cx);
376                if ix < last_member_ix {
377                    let mut border = theme.workspace.pane_divider;
378                    border.left = false;
379                    border.right = false;
380                    border.top = false;
381                    border.bottom = false;
382                    match self.axis {
383                        Axis::Vertical => border.bottom = true,
384                        Axis::Horizontal => border.right = true,
385                    }
386                    member = Container::new(member).with_border(border).boxed();
387                }
388
389                FlexItem::new(member).flex(flex, true).boxed()
390            }))
391            .boxed()
392    }
393}
394
395#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
396pub enum SplitDirection {
397    Up,
398    Down,
399    Left,
400    Right,
401}
402
403impl SplitDirection {
404    pub fn all() -> [Self; 4] {
405        [Self::Up, Self::Down, Self::Left, Self::Right]
406    }
407
408    pub fn edge(&self, rect: RectF) -> f32 {
409        match self {
410            Self::Up => rect.min_y(),
411            Self::Down => rect.max_y(),
412            Self::Left => rect.min_x(),
413            Self::Right => rect.max_x(),
414        }
415    }
416
417    // Returns a new rectangle which shares an edge in SplitDirection and has `size` along SplitDirection
418    pub fn along_edge(&self, rect: RectF, size: f32) -> RectF {
419        match self {
420            Self::Up => RectF::new(rect.origin(), Vector2F::new(rect.width(), size)),
421            Self::Down => RectF::new(
422                rect.lower_left() - Vector2F::new(0., size),
423                Vector2F::new(rect.width(), size),
424            ),
425            Self::Left => RectF::new(rect.origin(), Vector2F::new(size, rect.height())),
426            Self::Right => RectF::new(
427                rect.upper_right() - Vector2F::new(size, 0.),
428                Vector2F::new(size, rect.height()),
429            ),
430        }
431    }
432
433    pub fn axis(&self) -> Axis {
434        match self {
435            Self::Up | Self::Down => Axis::Vertical,
436            Self::Left | Self::Right => Axis::Horizontal,
437        }
438    }
439
440    pub fn increasing(&self) -> bool {
441        match self {
442            Self::Left | Self::Up => false,
443            Self::Down | Self::Right => true,
444        }
445    }
446}