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