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