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