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}