contacts_panel.rs

  1use std::sync::Arc;
  2
  3use client::{Contact, UserStore};
  4use gpui::{
  5    elements::*,
  6    geometry::{rect::RectF, vector::vec2f},
  7    platform::CursorStyle,
  8    Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View,
  9    ViewContext,
 10};
 11use postage::watch;
 12use workspace::{AppState, JoinProject, JoinProjectParams, Settings};
 13
 14pub struct ContactsPanel {
 15    contacts: ListState,
 16    user_store: ModelHandle<UserStore>,
 17    settings: watch::Receiver<Settings>,
 18    _maintain_contacts: Subscription,
 19}
 20
 21impl ContactsPanel {
 22    pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
 23        Self {
 24            contacts: ListState::new(
 25                app_state.user_store.read(cx).contacts().len(),
 26                Orientation::Top,
 27                1000.,
 28                {
 29                    let app_state = app_state.clone();
 30                    let view_id = cx.view_id();
 31                    move |ix, cx| {
 32                        let user_store = app_state.user_store.read(cx);
 33                        let contacts = user_store.contacts().clone();
 34                        let current_user_id = user_store.current_user().map(|user| user.id);
 35                        Self::render_collaborator(
 36                            &contacts[ix],
 37                            current_user_id,
 38                            app_state.clone(),
 39                            view_id,
 40                            cx,
 41                        )
 42                    }
 43                },
 44            ),
 45            _maintain_contacts: cx.observe(&app_state.user_store, Self::update_contacts),
 46            user_store: app_state.user_store.clone(),
 47            settings: app_state.settings.clone(),
 48        }
 49    }
 50
 51    fn update_contacts(&mut self, _: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) {
 52        self.contacts
 53            .reset(self.user_store.read(cx).contacts().len());
 54        cx.notify();
 55    }
 56
 57    fn render_collaborator(
 58        collaborator: &Contact,
 59        current_user_id: Option<u64>,
 60        app_state: Arc<AppState>,
 61        view_id: usize,
 62        cx: &mut LayoutContext,
 63    ) -> ElementBox {
 64        let theme = &app_state.settings.borrow().theme.contacts_panel;
 65        let project_count = collaborator.projects.len();
 66        let font_cache = cx.font_cache();
 67        let line_height = theme.unshared_project.name.text.line_height(font_cache);
 68        let cap_height = theme.unshared_project.name.text.cap_height(font_cache);
 69        let baseline_offset = theme.unshared_project.name.text.baseline_offset(font_cache)
 70            + (theme.unshared_project.height - line_height) / 2.;
 71        let tree_branch_width = theme.tree_branch_width;
 72        let tree_branch_color = theme.tree_branch_color;
 73        let host_avatar_height = theme
 74            .host_avatar
 75            .width
 76            .or(theme.host_avatar.height)
 77            .unwrap_or(0.);
 78
 79        Flex::column()
 80            .with_child(
 81                Flex::row()
 82                    .with_children(collaborator.user.avatar.clone().map(|avatar| {
 83                        Image::new(avatar)
 84                            .with_style(theme.host_avatar)
 85                            .aligned()
 86                            .left()
 87                            .boxed()
 88                    }))
 89                    .with_child(
 90                        Label::new(
 91                            collaborator.user.github_login.clone(),
 92                            theme.host_username.text.clone(),
 93                        )
 94                        .contained()
 95                        .with_style(theme.host_username.container)
 96                        .aligned()
 97                        .left()
 98                        .boxed(),
 99                    )
100                    .constrained()
101                    .with_height(theme.host_row_height)
102                    .boxed(),
103            )
104            .with_children(
105                collaborator
106                    .projects
107                    .iter()
108                    .enumerate()
109                    .map(|(ix, project)| {
110                        let project_id = project.id;
111
112                        Flex::row()
113                            .with_child(
114                                Canvas::new(move |bounds, _, cx| {
115                                    let start_x = bounds.min_x() + (bounds.width() / 2.)
116                                        - (tree_branch_width / 2.);
117                                    let end_x = bounds.max_x();
118                                    let start_y = bounds.min_y();
119                                    let end_y =
120                                        bounds.min_y() + baseline_offset - (cap_height / 2.);
121
122                                    cx.scene.push_quad(gpui::Quad {
123                                        bounds: RectF::from_points(
124                                            vec2f(start_x, start_y),
125                                            vec2f(
126                                                start_x + tree_branch_width,
127                                                if ix + 1 == project_count {
128                                                    end_y
129                                                } else {
130                                                    bounds.max_y()
131                                                },
132                                            ),
133                                        ),
134                                        background: Some(tree_branch_color),
135                                        border: gpui::Border::default(),
136                                        corner_radius: 0.,
137                                    });
138                                    cx.scene.push_quad(gpui::Quad {
139                                        bounds: RectF::from_points(
140                                            vec2f(start_x, end_y),
141                                            vec2f(end_x, end_y + tree_branch_width),
142                                        ),
143                                        background: Some(tree_branch_color),
144                                        border: gpui::Border::default(),
145                                        corner_radius: 0.,
146                                    });
147                                })
148                                .constrained()
149                                .with_width(host_avatar_height)
150                                .boxed(),
151                            )
152                            .with_child({
153                                let is_host = Some(collaborator.user.id) == current_user_id;
154                                let is_guest = !is_host
155                                    && project
156                                        .guests
157                                        .iter()
158                                        .any(|guest| Some(guest.id) == current_user_id);
159                                let is_shared = project.is_shared;
160                                let app_state = app_state.clone();
161
162                                MouseEventHandler::new::<ContactsPanel, _, _, _>(
163                                    (view_id, project_id as usize),
164                                    cx,
165                                    |mouse_state, _| {
166                                        let style = match (project.is_shared, mouse_state.hovered) {
167                                            (false, false) => &theme.unshared_project,
168                                            (false, true) => &theme.hovered_unshared_project,
169                                            (true, false) => &theme.shared_project,
170                                            (true, true) => &theme.hovered_shared_project,
171                                        };
172
173                                        Flex::row()
174                                            .with_child(
175                                                Label::new(
176                                                    project.worktree_root_names.join(", "),
177                                                    style.name.text.clone(),
178                                                )
179                                                .aligned()
180                                                .left()
181                                                .contained()
182                                                .with_style(style.name.container)
183                                                .boxed(),
184                                            )
185                                            .with_children(project.guests.iter().filter_map(
186                                                |participant| {
187                                                    participant.avatar.clone().map(|avatar| {
188                                                        Image::new(avatar)
189                                                            .with_style(style.guest_avatar)
190                                                            .aligned()
191                                                            .left()
192                                                            .contained()
193                                                            .with_margin_right(
194                                                                style.guest_avatar_spacing,
195                                                            )
196                                                            .boxed()
197                                                    })
198                                                },
199                                            ))
200                                            .contained()
201                                            .with_style(style.container)
202                                            .constrained()
203                                            .with_height(style.height)
204                                            .boxed()
205                                    },
206                                )
207                                .with_cursor_style(if is_host || is_shared {
208                                    CursorStyle::PointingHand
209                                } else {
210                                    CursorStyle::Arrow
211                                })
212                                .on_click(move |cx| {
213                                    if !is_host && !is_guest {
214                                        cx.dispatch_global_action(JoinProject(JoinProjectParams {
215                                            project_id,
216                                            app_state: app_state.clone(),
217                                        }));
218                                    }
219                                })
220                                .flexible(1., true)
221                                .boxed()
222                            })
223                            .constrained()
224                            .with_height(theme.unshared_project.height)
225                            .boxed()
226                    }),
227            )
228            .boxed()
229    }
230}
231
232pub enum Event {}
233
234impl Entity for ContactsPanel {
235    type Event = Event;
236}
237
238impl View for ContactsPanel {
239    fn ui_name() -> &'static str {
240        "ContactsPanel"
241    }
242
243    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
244        let theme = &self.settings.borrow().theme.contacts_panel;
245        Container::new(List::new(self.contacts.clone()).boxed())
246            .with_style(theme.container)
247            .boxed()
248    }
249}