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