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, RenderContext, 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 RenderContext<Workspace>,
74 ) -> ElementBox {
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 RenderContext<Workspace>,
135 ) -> ElementBox {
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 prompt = 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 .boxed()
199 },
200 )
201 .with_cursor_style(CursorStyle::PointingHand)
202 .on_click(MouseButton::Left, move |_, cx| {
203 cx.dispatch_action(JoinProject {
204 project_id: leader_project_id,
205 follow_user_id: leader_user_id,
206 })
207 })
208 .aligned()
209 .bottom()
210 .right()
211 .boxed(),
212 )
213 }
214 }
215 ParticipantLocation::UnsharedProject => Some(
216 Label::new(
217 format!(
218 "{} is viewing an unshared Zed project",
219 leader.user.github_login
220 ),
221 theme.workspace.external_location_message.text.clone(),
222 )
223 .contained()
224 .with_style(theme.workspace.external_location_message.container)
225 .aligned()
226 .bottom()
227 .right()
228 .boxed(),
229 ),
230 ParticipantLocation::External => Some(
231 Label::new(
232 format!(
233 "{} is viewing a window outside of Zed",
234 leader.user.github_login
235 ),
236 theme.workspace.external_location_message.text.clone(),
237 )
238 .contained()
239 .with_style(theme.workspace.external_location_message.container)
240 .aligned()
241 .bottom()
242 .right()
243 .boxed(),
244 ),
245 }
246 } else {
247 None
248 };
249
250 Stack::new()
251 .with_child(
252 ChildView::new(pane, cx)
253 .contained()
254 .with_border(border)
255 .boxed(),
256 )
257 .with_children(prompt)
258 .boxed()
259 }
260 Member::Axis(axis) => axis.render(
261 project,
262 theme,
263 follower_states,
264 active_call,
265 active_pane,
266 cx,
267 ),
268 }
269 }
270
271 fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
272 match self {
273 Member::Axis(axis) => {
274 for member in &axis.members {
275 member.collect_panes(panes);
276 }
277 }
278 Member::Pane(pane) => panes.push(pane),
279 }
280 }
281}
282
283#[derive(Clone, Debug, Eq, PartialEq)]
284pub(crate) struct PaneAxis {
285 pub axis: Axis,
286 pub members: Vec<Member>,
287}
288
289impl PaneAxis {
290 fn split(
291 &mut self,
292 old_pane: &ViewHandle<Pane>,
293 new_pane: &ViewHandle<Pane>,
294 direction: SplitDirection,
295 ) -> Result<()> {
296 for (mut idx, member) in self.members.iter_mut().enumerate() {
297 match member {
298 Member::Axis(axis) => {
299 if axis.split(old_pane, new_pane, direction).is_ok() {
300 return Ok(());
301 }
302 }
303 Member::Pane(pane) => {
304 if pane == old_pane {
305 if direction.axis() == self.axis {
306 if direction.increasing() {
307 idx += 1;
308 }
309
310 self.members.insert(idx, Member::Pane(new_pane.clone()));
311 } else {
312 *member =
313 Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
314 }
315 return Ok(());
316 }
317 }
318 }
319 }
320 Err(anyhow!("Pane not found"))
321 }
322
323 fn remove(&mut self, pane_to_remove: &ViewHandle<Pane>) -> Result<Option<Member>> {
324 let mut found_pane = false;
325 let mut remove_member = None;
326 for (idx, member) in self.members.iter_mut().enumerate() {
327 match member {
328 Member::Axis(axis) => {
329 if let Ok(last_pane) = axis.remove(pane_to_remove) {
330 if let Some(last_pane) = last_pane {
331 *member = last_pane;
332 }
333 found_pane = true;
334 break;
335 }
336 }
337 Member::Pane(pane) => {
338 if pane == pane_to_remove {
339 found_pane = true;
340 remove_member = Some(idx);
341 break;
342 }
343 }
344 }
345 }
346
347 if found_pane {
348 if let Some(idx) = remove_member {
349 self.members.remove(idx);
350 }
351
352 if self.members.len() == 1 {
353 Ok(self.members.pop())
354 } else {
355 Ok(None)
356 }
357 } else {
358 Err(anyhow!("Pane not found"))
359 }
360 }
361
362 fn render(
363 &self,
364 project: &ModelHandle<Project>,
365 theme: &Theme,
366 follower_state: &FollowerStatesByLeader,
367 active_call: Option<&ModelHandle<ActiveCall>>,
368 active_pane: &ViewHandle<Pane>,
369 cx: &mut RenderContext<Workspace>,
370 ) -> ElementBox {
371 let last_member_ix = self.members.len() - 1;
372 Flex::new(self.axis)
373 .with_children(self.members.iter().enumerate().map(|(ix, member)| {
374 let mut flex = 1.0;
375 if member.contains(active_pane) {
376 flex = cx.global::<Settings>().active_pane_magnification;
377 }
378
379 let mut member =
380 member.render(project, theme, follower_state, active_call, active_pane, cx);
381 if ix < last_member_ix {
382 let mut border = theme.workspace.pane_divider;
383 border.left = false;
384 border.right = false;
385 border.top = false;
386 border.bottom = false;
387 match self.axis {
388 Axis::Vertical => border.bottom = true,
389 Axis::Horizontal => border.right = true,
390 }
391 member = Container::new(member).with_border(border).boxed();
392 }
393
394 FlexItem::new(member).flex(flex, true).boxed()
395 }))
396 .boxed()
397 }
398}
399
400#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
401pub enum SplitDirection {
402 Up,
403 Down,
404 Left,
405 Right,
406}
407
408impl SplitDirection {
409 pub fn all() -> [Self; 4] {
410 [Self::Up, Self::Down, Self::Left, Self::Right]
411 }
412
413 pub fn edge(&self, rect: RectF) -> f32 {
414 match self {
415 Self::Up => rect.min_y(),
416 Self::Down => rect.max_y(),
417 Self::Left => rect.min_x(),
418 Self::Right => rect.max_x(),
419 }
420 }
421
422 // Returns a new rectangle which shares an edge in SplitDirection and has `size` along SplitDirection
423 pub fn along_edge(&self, rect: RectF, size: f32) -> RectF {
424 match self {
425 Self::Up => RectF::new(rect.origin(), Vector2F::new(rect.width(), size)),
426 Self::Down => RectF::new(
427 rect.lower_left() - Vector2F::new(0., size),
428 Vector2F::new(rect.width(), size),
429 ),
430 Self::Left => RectF::new(rect.origin(), Vector2F::new(size, rect.height())),
431 Self::Right => RectF::new(
432 rect.upper_right() - Vector2F::new(size, 0.),
433 Vector2F::new(size, rect.height()),
434 ),
435 }
436 }
437
438 pub fn axis(&self) -> Axis {
439 match self {
440 Self::Up | Self::Down => Axis::Vertical,
441 Self::Left | Self::Right => Axis::Horizontal,
442 }
443 }
444
445 pub fn increasing(&self) -> bool {
446 match self {
447 Self::Left | Self::Up => false,
448 Self::Down | Self::Right => true,
449 }
450 }
451}