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