lib.rs

  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}