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, ViewContext, 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 ViewContext<Workspace>,
 74    ) -> AnyElement<Workspace> {
 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 ViewContext<Workspace>,
135    ) -> AnyElement<Workspace> {
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 leader_status_box = 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                                        },
199                                    )
200                                    .with_cursor_style(CursorStyle::PointingHand)
201                                    .on_click(MouseButton::Left, move |_, _, cx| {
202                                        cx.dispatch_action(JoinProject {
203                                            project_id: leader_project_id,
204                                            follow_user_id: leader_user_id,
205                                        })
206                                    })
207                                    .aligned()
208                                    .bottom()
209                                    .right()
210                                    .into_any(),
211                                )
212                            }
213                        }
214                        ParticipantLocation::UnsharedProject => Some(
215                            Label::new(
216                                format!(
217                                    "{} is viewing an unshared Zed project",
218                                    leader.user.github_login
219                                ),
220                                theme.workspace.external_location_message.text.clone(),
221                            )
222                            .contained()
223                            .with_style(theme.workspace.external_location_message.container)
224                            .aligned()
225                            .bottom()
226                            .right()
227                            .into_any(),
228                        ),
229                        ParticipantLocation::External => Some(
230                            Label::new(
231                                format!(
232                                    "{} is viewing a window outside of Zed",
233                                    leader.user.github_login
234                                ),
235                                theme.workspace.external_location_message.text.clone(),
236                            )
237                            .contained()
238                            .with_style(theme.workspace.external_location_message.container)
239                            .aligned()
240                            .bottom()
241                            .right()
242                            .into_any(),
243                        ),
244                    }
245                } else {
246                    None
247                };
248
249                Stack::new()
250                    .with_child(ChildView::new(pane, cx).contained().with_border(border))
251                    .with_children(leader_status_box)
252                    .into_any()
253            }
254            Member::Axis(axis) => axis.render(
255                project,
256                theme,
257                follower_states,
258                active_call,
259                active_pane,
260                cx,
261            ),
262        }
263    }
264
265    fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
266        match self {
267            Member::Axis(axis) => {
268                for member in &axis.members {
269                    member.collect_panes(panes);
270                }
271            }
272            Member::Pane(pane) => panes.push(pane),
273        }
274    }
275}
276
277#[derive(Clone, Debug, Eq, PartialEq)]
278pub(crate) struct PaneAxis {
279    pub axis: Axis,
280    pub members: Vec<Member>,
281}
282
283impl PaneAxis {
284    fn split(
285        &mut self,
286        old_pane: &ViewHandle<Pane>,
287        new_pane: &ViewHandle<Pane>,
288        direction: SplitDirection,
289    ) -> Result<()> {
290        for (mut idx, member) in self.members.iter_mut().enumerate() {
291            match member {
292                Member::Axis(axis) => {
293                    if axis.split(old_pane, new_pane, direction).is_ok() {
294                        return Ok(());
295                    }
296                }
297                Member::Pane(pane) => {
298                    if pane == old_pane {
299                        if direction.axis() == self.axis {
300                            if direction.increasing() {
301                                idx += 1;
302                            }
303
304                            self.members.insert(idx, Member::Pane(new_pane.clone()));
305                        } else {
306                            *member =
307                                Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
308                        }
309                        return Ok(());
310                    }
311                }
312            }
313        }
314        Err(anyhow!("Pane not found"))
315    }
316
317    fn remove(&mut self, pane_to_remove: &ViewHandle<Pane>) -> Result<Option<Member>> {
318        let mut found_pane = false;
319        let mut remove_member = None;
320        for (idx, member) in self.members.iter_mut().enumerate() {
321            match member {
322                Member::Axis(axis) => {
323                    if let Ok(last_pane) = axis.remove(pane_to_remove) {
324                        if let Some(last_pane) = last_pane {
325                            *member = last_pane;
326                        }
327                        found_pane = true;
328                        break;
329                    }
330                }
331                Member::Pane(pane) => {
332                    if pane == pane_to_remove {
333                        found_pane = true;
334                        remove_member = Some(idx);
335                        break;
336                    }
337                }
338            }
339        }
340
341        if found_pane {
342            if let Some(idx) = remove_member {
343                self.members.remove(idx);
344            }
345
346            if self.members.len() == 1 {
347                Ok(self.members.pop())
348            } else {
349                Ok(None)
350            }
351        } else {
352            Err(anyhow!("Pane not found"))
353        }
354    }
355
356    fn render(
357        &self,
358        project: &ModelHandle<Project>,
359        theme: &Theme,
360        follower_state: &FollowerStatesByLeader,
361        active_call: Option<&ModelHandle<ActiveCall>>,
362        active_pane: &ViewHandle<Pane>,
363        cx: &mut ViewContext<Workspace>,
364    ) -> AnyElement<Workspace> {
365        let last_member_ix = self.members.len() - 1;
366        Flex::new(self.axis)
367            .with_children(self.members.iter().enumerate().map(|(ix, member)| {
368                let mut flex = 1.0;
369                if member.contains(active_pane) {
370                    flex = cx.global::<Settings>().active_pane_magnification;
371                }
372
373                let mut member =
374                    member.render(project, theme, follower_state, active_call, active_pane, cx);
375                if ix < last_member_ix {
376                    let mut border = theme.workspace.pane_divider;
377                    border.left = false;
378                    border.right = false;
379                    border.top = false;
380                    border.bottom = false;
381                    match self.axis {
382                        Axis::Vertical => border.bottom = true,
383                        Axis::Horizontal => border.right = true,
384                    }
385                    member = member.contained().with_border(border).into_any();
386                }
387
388                FlexItem::new(member).flex(flex, true)
389            }))
390            .into_any()
391    }
392}
393
394#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
395pub enum SplitDirection {
396    Up,
397    Down,
398    Left,
399    Right,
400}
401
402impl SplitDirection {
403    pub fn all() -> [Self; 4] {
404        [Self::Up, Self::Down, Self::Left, Self::Right]
405    }
406
407    pub fn edge(&self, rect: RectF) -> f32 {
408        match self {
409            Self::Up => rect.min_y(),
410            Self::Down => rect.max_y(),
411            Self::Left => rect.min_x(),
412            Self::Right => rect.max_x(),
413        }
414    }
415
416    // Returns a new rectangle which shares an edge in SplitDirection and has `size` along SplitDirection
417    pub fn along_edge(&self, rect: RectF, size: f32) -> RectF {
418        match self {
419            Self::Up => RectF::new(rect.origin(), Vector2F::new(rect.width(), size)),
420            Self::Down => RectF::new(
421                rect.lower_left() - Vector2F::new(0., size),
422                Vector2F::new(rect.width(), size),
423            ),
424            Self::Left => RectF::new(rect.origin(), Vector2F::new(size, rect.height())),
425            Self::Right => RectF::new(
426                rect.upper_right() - Vector2F::new(size, 0.),
427                Vector2F::new(size, rect.height()),
428            ),
429        }
430    }
431
432    pub fn axis(&self) -> Axis {
433        match self {
434            Self::Up | Self::Down => Axis::Vertical,
435            Self::Left | Self::Right => Axis::Horizontal,
436        }
437    }
438
439    pub fn increasing(&self) -> bool {
440        match self {
441            Self::Left | Self::Up => false,
442            Self::Down | Self::Right => true,
443        }
444    }
445}