use std::sync::Arc; use crate::{AppState, FollowerStatesByLeader, Pane, Workspace, WorkspaceSettings}; use anyhow::{anyhow, Result}; use call::{ActiveCall, ParticipantLocation}; use gpui::{ elements::*, geometry::{rect::RectF, vector::Vector2F}, platform::{CursorStyle, MouseButton}, AnyViewHandle, Axis, Border, ModelHandle, ViewContext, ViewHandle, }; use project::Project; use serde::Deserialize; use theme::Theme; #[derive(Clone, Debug, Eq, PartialEq)] pub struct PaneGroup { pub(crate) root: Member, } impl PaneGroup { pub(crate) fn with_root(root: Member) -> Self { Self { root } } pub fn new(pane: ViewHandle) -> Self { Self { root: Member::Pane(pane), } } pub fn split( &mut self, old_pane: &ViewHandle, new_pane: &ViewHandle, direction: SplitDirection, ) -> Result<()> { match &mut self.root { Member::Pane(pane) => { if pane == old_pane { self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction); Ok(()) } else { Err(anyhow!("Pane not found")) } } Member::Axis(axis) => axis.split(old_pane, new_pane, direction), } } /// Returns: /// - Ok(true) if it found and removed a pane /// - Ok(false) if it found but did not remove the pane /// - Err(_) if it did not find the pane pub fn remove(&mut self, pane: &ViewHandle) -> Result { match &mut self.root { Member::Pane(_) => Ok(false), Member::Axis(axis) => { if let Some(last_pane) = axis.remove(pane)? { self.root = last_pane; } Ok(true) } } } pub(crate) fn render( &self, project: &ModelHandle, theme: &Theme, follower_states: &FollowerStatesByLeader, active_call: Option<&ModelHandle>, active_pane: &ViewHandle, zoomed: Option<&AnyViewHandle>, app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { self.root.render( project, theme, follower_states, active_call, active_pane, zoomed, app_state, cx, ) } pub(crate) fn panes(&self) -> Vec<&ViewHandle> { let mut panes = Vec::new(); self.root.collect_panes(&mut panes); panes } } #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum Member { Axis(PaneAxis), Pane(ViewHandle), } impl Member { fn new_axis( old_pane: ViewHandle, new_pane: ViewHandle, direction: SplitDirection, ) -> Self { use Axis::*; use SplitDirection::*; let axis = match direction { Up | Down => Vertical, Left | Right => Horizontal, }; let members = match direction { Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)], Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)], }; Member::Axis(PaneAxis { axis, members }) } fn contains(&self, needle: &ViewHandle) -> bool { match self { Member::Axis(axis) => axis.members.iter().any(|member| member.contains(needle)), Member::Pane(pane) => pane == needle, } } pub fn render( &self, project: &ModelHandle, theme: &Theme, follower_states: &FollowerStatesByLeader, active_call: Option<&ModelHandle>, active_pane: &ViewHandle, zoomed: Option<&AnyViewHandle>, app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { enum FollowIntoExternalProject {} match self { Member::Pane(pane) => { let pane_element = if Some(&**pane) == zoomed { Empty::new().into_any() } else { ChildView::new(pane, cx).into_any() }; let leader = follower_states .iter() .find_map(|(leader_id, follower_states)| { if follower_states.contains_key(pane) { Some(leader_id) } else { None } }) .and_then(|leader_id| { let room = active_call?.read(cx).room()?.read(cx); let collaborator = project.read(cx).collaborators().get(leader_id)?; let participant = room.remote_participant_for_peer_id(*leader_id)?; Some((collaborator.replica_id, participant)) }); let border = if let Some((replica_id, _)) = leader.as_ref() { let leader_color = theme.editor.replica_selection_style(*replica_id).cursor; let mut border = Border::all(theme.workspace.leader_border_width, leader_color); border .color .fade_out(1. - theme.workspace.leader_border_opacity); border.overlay = true; border } else { Border::default() }; let leader_status_box = if let Some((_, leader)) = leader { match leader.location { ParticipantLocation::SharedProject { project_id: leader_project_id, } => { if Some(leader_project_id) == project.read(cx).remote_id() { None } else { let leader_user = leader.user.clone(); let leader_user_id = leader.user.id; let app_state = Arc::downgrade(app_state); Some( MouseEventHandler::::new( pane.id(), cx, |_, _| { Label::new( format!( "Follow {} on their active project", leader_user.github_login, ), theme .workspace .external_location_message .text .clone(), ) .contained() .with_style( theme.workspace.external_location_message.container, ) }, ) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, _, cx| { if let Some(app_state) = app_state.upgrade() { crate::join_remote_project( leader_project_id, leader_user_id, app_state, cx, ) .detach_and_log_err(cx); } }) .aligned() .bottom() .right() .into_any(), ) } } ParticipantLocation::UnsharedProject => Some( Label::new( format!( "{} is viewing an unshared Zed project", leader.user.github_login ), theme.workspace.external_location_message.text.clone(), ) .contained() .with_style(theme.workspace.external_location_message.container) .aligned() .bottom() .right() .into_any(), ), ParticipantLocation::External => Some( Label::new( format!( "{} is viewing a window outside of Zed", leader.user.github_login ), theme.workspace.external_location_message.text.clone(), ) .contained() .with_style(theme.workspace.external_location_message.container) .aligned() .bottom() .right() .into_any(), ), } } else { None }; Stack::new() .with_child(pane_element.contained().with_border(border)) .with_children(leader_status_box) .into_any() } Member::Axis(axis) => axis.render( project, theme, follower_states, active_call, active_pane, zoomed, app_state, cx, ), } } fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle>) { match self { Member::Axis(axis) => { for member in &axis.members { member.collect_panes(panes); } } Member::Pane(pane) => panes.push(pane), } } } #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct PaneAxis { pub axis: Axis, pub members: Vec, } impl PaneAxis { fn split( &mut self, old_pane: &ViewHandle, new_pane: &ViewHandle, direction: SplitDirection, ) -> Result<()> { for (mut idx, member) in self.members.iter_mut().enumerate() { match member { Member::Axis(axis) => { if axis.split(old_pane, new_pane, direction).is_ok() { return Ok(()); } } Member::Pane(pane) => { if pane == old_pane { if direction.axis() == self.axis { if direction.increasing() { idx += 1; } self.members.insert(idx, Member::Pane(new_pane.clone())); } else { *member = Member::new_axis(old_pane.clone(), new_pane.clone(), direction); } return Ok(()); } } } } Err(anyhow!("Pane not found")) } fn remove(&mut self, pane_to_remove: &ViewHandle) -> Result> { let mut found_pane = false; let mut remove_member = None; for (idx, member) in self.members.iter_mut().enumerate() { match member { Member::Axis(axis) => { if let Ok(last_pane) = axis.remove(pane_to_remove) { if let Some(last_pane) = last_pane { *member = last_pane; } found_pane = true; break; } } Member::Pane(pane) => { if pane == pane_to_remove { found_pane = true; remove_member = Some(idx); break; } } } } if found_pane { if let Some(idx) = remove_member { self.members.remove(idx); } if self.members.len() == 1 { Ok(self.members.pop()) } else { Ok(None) } } else { Err(anyhow!("Pane not found")) } } fn render( &self, project: &ModelHandle, theme: &Theme, follower_state: &FollowerStatesByLeader, active_call: Option<&ModelHandle>, active_pane: &ViewHandle, zoomed: Option<&AnyViewHandle>, app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { let last_member_ix = self.members.len() - 1; Flex::new(self.axis) .with_children(self.members.iter().enumerate().map(|(ix, member)| { let mut flex = 1.0; if member.contains(active_pane) { flex = settings::get::(cx).active_pane_magnification; } let mut member = member.render( project, theme, follower_state, active_call, active_pane, zoomed, app_state, cx, ); if ix < last_member_ix { let mut border = theme.workspace.pane_divider; border.left = false; border.right = false; border.top = false; border.bottom = false; match self.axis { Axis::Vertical => border.bottom = true, Axis::Horizontal => border.right = true, } member = member.contained().with_border(border).into_any(); } FlexItem::new(member).flex(flex, true) })) .into_any() } } #[derive(Clone, Copy, Debug, Deserialize, PartialEq)] pub enum SplitDirection { Up, Down, Left, Right, } impl SplitDirection { pub fn all() -> [Self; 4] { [Self::Up, Self::Down, Self::Left, Self::Right] } pub fn edge(&self, rect: RectF) -> f32 { match self { Self::Up => rect.min_y(), Self::Down => rect.max_y(), Self::Left => rect.min_x(), Self::Right => rect.max_x(), } } // Returns a new rectangle which shares an edge in SplitDirection and has `size` along SplitDirection pub fn along_edge(&self, rect: RectF, size: f32) -> RectF { match self { Self::Up => RectF::new(rect.origin(), Vector2F::new(rect.width(), size)), Self::Down => RectF::new( rect.lower_left() - Vector2F::new(0., size), Vector2F::new(rect.width(), size), ), Self::Left => RectF::new(rect.origin(), Vector2F::new(size, rect.height())), Self::Right => RectF::new( rect.upper_right() - Vector2F::new(size, 0.), Vector2F::new(size, rect.height()), ), } } pub fn axis(&self) -> Axis { match self { Self::Up | Self::Down => Axis::Vertical, Self::Left | Self::Right => Axis::Horizontal, } } pub fn increasing(&self) -> bool { match self { Self::Left | Self::Up => false, Self::Down | Self::Right => true, } } }