contacts_panel.rs

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