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}