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