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