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