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}