1use client::{Collaborator, UserStore};
2use gpui::{
3 action,
4 elements::*,
5 geometry::{rect::RectF, vector::vec2f},
6 platform::CursorStyle,
7 Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext,
8 Subscription, View, ViewContext,
9};
10use postage::watch;
11use theme::Theme;
12use workspace::{Settings, Workspace};
13
14action!(JoinWorktree, u64);
15action!(LeaveWorktree, u64);
16action!(ShareWorktree, u64);
17action!(UnshareWorktree, u64);
18
19pub fn init(cx: &mut MutableAppContext) {
20 cx.add_action(PeoplePanel::share_worktree);
21 cx.add_action(PeoplePanel::unshare_worktree);
22 cx.add_action(PeoplePanel::join_worktree);
23 cx.add_action(PeoplePanel::leave_worktree);
24}
25
26pub struct PeoplePanel {
27 collaborators: ListState,
28 user_store: ModelHandle<UserStore>,
29 settings: watch::Receiver<Settings>,
30 _maintain_collaborators: Subscription,
31}
32
33impl PeoplePanel {
34 pub fn new(
35 user_store: ModelHandle<UserStore>,
36 settings: watch::Receiver<Settings>,
37 cx: &mut ViewContext<Self>,
38 ) -> Self {
39 Self {
40 collaborators: ListState::new(
41 user_store.read(cx).collaborators().len(),
42 Orientation::Top,
43 1000.,
44 {
45 let user_store = user_store.clone();
46 let settings = settings.clone();
47 move |ix, cx| {
48 let user_store = user_store.read(cx);
49 let collaborators = user_store.collaborators().clone();
50 let current_user_id = user_store.current_user().map(|user| user.id);
51 Self::render_collaborator(
52 &collaborators[ix],
53 current_user_id,
54 &settings.borrow().theme,
55 cx,
56 )
57 }
58 },
59 ),
60 _maintain_collaborators: cx.observe(&user_store, Self::update_collaborators),
61 user_store,
62 settings,
63 }
64 }
65
66 fn share_worktree(
67 workspace: &mut Workspace,
68 action: &ShareWorktree,
69 cx: &mut ViewContext<Workspace>,
70 ) {
71 workspace
72 .project()
73 .update(cx, |p, cx| p.share_worktree(action.0, cx));
74 }
75
76 fn unshare_worktree(
77 workspace: &mut Workspace,
78 action: &UnshareWorktree,
79 cx: &mut ViewContext<Workspace>,
80 ) {
81 workspace
82 .project()
83 .update(cx, |p, cx| p.unshare_worktree(action.0, cx));
84 }
85
86 fn join_worktree(
87 workspace: &mut Workspace,
88 action: &JoinWorktree,
89 cx: &mut ViewContext<Workspace>,
90 ) {
91 workspace
92 .project()
93 .update(cx, |p, cx| p.add_remote_worktree(action.0, cx).detach());
94 }
95
96 fn leave_worktree(
97 workspace: &mut Workspace,
98 action: &LeaveWorktree,
99 cx: &mut ViewContext<Workspace>,
100 ) {
101 workspace
102 .project()
103 .update(cx, |p, cx| p.close_remote_worktree(action.0, cx));
104 }
105
106 fn update_collaborators(&mut self, _: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) {
107 self.collaborators
108 .reset(self.user_store.read(cx).collaborators().len());
109 cx.notify();
110 }
111
112 fn render_collaborator(
113 collaborator: &Collaborator,
114 current_user_id: Option<u64>,
115 theme: &Theme,
116 cx: &mut LayoutContext,
117 ) -> ElementBox {
118 let theme = &theme.people_panel;
119 let worktree_count = collaborator.worktrees.len();
120 let font_cache = cx.font_cache();
121 let line_height = theme.unshared_worktree.name.text.line_height(font_cache);
122 let cap_height = theme.unshared_worktree.name.text.cap_height(font_cache);
123 let baseline_offset = theme
124 .unshared_worktree
125 .name
126 .text
127 .baseline_offset(font_cache)
128 + (theme.unshared_worktree.height - line_height) / 2.;
129 let tree_branch_width = theme.tree_branch_width;
130 let tree_branch_color = theme.tree_branch_color;
131 let host_avatar_height = theme
132 .host_avatar
133 .width
134 .or(theme.host_avatar.height)
135 .unwrap_or(0.);
136
137 Flex::column()
138 .with_child(
139 Flex::row()
140 .with_children(collaborator.user.avatar.clone().map(|avatar| {
141 Image::new(avatar)
142 .with_style(theme.host_avatar)
143 .aligned()
144 .left()
145 .boxed()
146 }))
147 .with_child(
148 Label::new(
149 collaborator.user.github_login.clone(),
150 theme.host_username.text.clone(),
151 )
152 .contained()
153 .with_style(theme.host_username.container)
154 .aligned()
155 .left()
156 .boxed(),
157 )
158 .constrained()
159 .with_height(theme.host_row_height)
160 .boxed(),
161 )
162 .with_children(
163 collaborator
164 .worktrees
165 .iter()
166 .enumerate()
167 .map(|(ix, worktree)| {
168 let worktree_id = worktree.id;
169
170 Flex::row()
171 .with_child(
172 Canvas::new(move |bounds, _, cx| {
173 let start_x = bounds.min_x() + (bounds.width() / 2.)
174 - (tree_branch_width / 2.);
175 let end_x = bounds.max_x();
176 let start_y = bounds.min_y();
177 let end_y =
178 bounds.min_y() + baseline_offset - (cap_height / 2.);
179
180 cx.scene.push_quad(gpui::Quad {
181 bounds: RectF::from_points(
182 vec2f(start_x, start_y),
183 vec2f(
184 start_x + tree_branch_width,
185 if ix + 1 == worktree_count {
186 end_y
187 } else {
188 bounds.max_y()
189 },
190 ),
191 ),
192 background: Some(tree_branch_color),
193 border: gpui::Border::default(),
194 corner_radius: 0.,
195 });
196 cx.scene.push_quad(gpui::Quad {
197 bounds: RectF::from_points(
198 vec2f(start_x, end_y),
199 vec2f(end_x, end_y + tree_branch_width),
200 ),
201 background: Some(tree_branch_color),
202 border: gpui::Border::default(),
203 corner_radius: 0.,
204 });
205 })
206 .constrained()
207 .with_width(host_avatar_height)
208 .boxed(),
209 )
210 .with_child({
211 let is_host = Some(collaborator.user.id) == current_user_id;
212 let is_guest = !is_host
213 && worktree
214 .guests
215 .iter()
216 .any(|guest| Some(guest.id) == current_user_id);
217 let is_shared = worktree.is_shared;
218
219 MouseEventHandler::new::<PeoplePanel, _, _, _>(
220 worktree_id as usize,
221 cx,
222 |mouse_state, _| {
223 let style = match (worktree.is_shared, mouse_state.hovered)
224 {
225 (false, false) => &theme.unshared_worktree,
226 (false, true) => &theme.hovered_unshared_worktree,
227 (true, false) => &theme.shared_worktree,
228 (true, true) => &theme.hovered_shared_worktree,
229 };
230
231 Flex::row()
232 .with_child(
233 Label::new(
234 worktree.root_name.clone(),
235 style.name.text.clone(),
236 )
237 .aligned()
238 .left()
239 .contained()
240 .with_style(style.name.container)
241 .boxed(),
242 )
243 .with_children(worktree.guests.iter().filter_map(
244 |participant| {
245 participant.avatar.clone().map(|avatar| {
246 Image::new(avatar)
247 .with_style(style.guest_avatar)
248 .aligned()
249 .left()
250 .contained()
251 .with_margin_right(
252 style.guest_avatar_spacing,
253 )
254 .boxed()
255 })
256 },
257 ))
258 .contained()
259 .with_style(style.container)
260 .constrained()
261 .with_height(style.height)
262 .boxed()
263 },
264 )
265 .with_cursor_style(if is_host || is_shared {
266 CursorStyle::PointingHand
267 } else {
268 CursorStyle::Arrow
269 })
270 .on_click(move |cx| {
271 if is_shared {
272 if is_host {
273 cx.dispatch_action(UnshareWorktree(worktree_id));
274 } else if is_guest {
275 cx.dispatch_action(LeaveWorktree(worktree_id));
276 } else {
277 cx.dispatch_action(JoinWorktree(worktree_id))
278 }
279 } else if is_host {
280 cx.dispatch_action(ShareWorktree(worktree_id));
281 }
282 })
283 .expanded(1.0)
284 .boxed()
285 })
286 .constrained()
287 .with_height(theme.unshared_worktree.height)
288 .boxed()
289 }),
290 )
291 .boxed()
292 }
293}
294
295pub enum Event {}
296
297impl Entity for PeoplePanel {
298 type Event = Event;
299}
300
301impl View for PeoplePanel {
302 fn ui_name() -> &'static str {
303 "PeoplePanel"
304 }
305
306 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
307 let theme = &self.settings.borrow().theme.people_panel;
308 Container::new(List::new(self.collaborators.clone()).boxed())
309 .with_style(theme.container)
310 .boxed()
311 }
312}