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