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}