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