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