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