pane_group.rs

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