pane_group.rs

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