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}