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