1use gpui::{
2 div, img, prelude::*, px, rems, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
3 SharedString, Stateful, View, ViewContext, WindowContext,
4};
5use theme::ActiveFabricTheme;
6use ui::{
7 popover_menu, Button, ButtonCommon, ButtonStyle, Clickable, Color, LabelSize, PopoverMenu,
8 Tooltip,
9};
10
11#[derive(Clone, Copy, Debug)]
12pub struct PeerId(pub u32);
13
14#[derive(Clone, Copy, Debug)]
15pub struct ProjectId(u64);
16
17pub trait TitlebarDelegate: 'static + Sized {
18 fn toggle_following(&mut self, peer_index: PeerId, cx: &mut ViewContext<Self>);
19}
20
21impl TitlebarDelegate for () {
22 fn toggle_following(&mut self, peer: PeerId, _cx: &mut ViewContext<Self>) {
23 log::info!("toggle following {:?}", peer);
24 }
25}
26
27#[derive(IntoElement)]
28pub struct Titlebar<D: TitlebarDelegate = ()> {
29 pub delegate: View<D>,
30 pub full_screen: bool,
31 pub project_host: Option<ProjectHost<D>>,
32 pub projects: Projects<D>,
33 pub branches: Option<Branches>,
34 pub collaborators: Vec<FacePile>,
35}
36
37#[derive(IntoElement)]
38pub struct ProjectHost<D: TitlebarDelegate> {
39 pub delegate: View<D>,
40 pub id: PeerId,
41 pub login: SharedString,
42 pub peer_index: u32,
43}
44
45#[derive(IntoElement)]
46pub struct Projects<D: TitlebarDelegate> {
47 pub delegate: View<D>,
48 pub current: SharedString,
49 pub recent: Vec<Project>,
50}
51
52#[derive(Clone)]
53pub struct Project {
54 pub name: SharedString,
55 pub id: ProjectId,
56}
57
58pub struct ProjectsMenu<D> {
59 delegate: View<D>,
60 focus: FocusHandle,
61 recent: Vec<Project>,
62}
63
64pub struct Branches {
65 pub current: SharedString,
66}
67
68#[derive(IntoElement, Default)]
69pub struct FacePile {
70 pub faces: Vec<Avatar>,
71}
72
73#[derive(IntoElement)]
74pub struct Avatar {
75 pub image_uri: SharedString,
76 pub audio_status: AudioStatus,
77 pub available: Option<bool>,
78 pub shape: AvatarShape,
79}
80
81pub enum AvatarShape {
82 Square,
83 Circle,
84}
85
86impl<D: TitlebarDelegate> RenderOnce for Titlebar<D> {
87 type Output = Stateful<Div>;
88
89 fn render(self, cx: &mut ui::prelude::WindowContext) -> Self::Output {
90 div()
91 .flex()
92 .flex_col()
93 .id("titlebar")
94 .justify_between()
95 .w_full()
96 .h(rems(1.75))
97 // Set a non-scaling min-height here to ensure the titlebar is
98 // always at least the height of the traffic lights.
99 .min_h(px(32.))
100 .pl_2()
101 .when(self.full_screen, |this| {
102 // Use pixels here instead of a rem-based size because the macOS traffic
103 // lights are a static size, and don't scale with the rest of the UI.
104 this.pl(px(80.))
105 })
106 .bg(cx.theme().denim.default.background)
107 .on_click(|event, cx| {
108 if event.up.click_count == 2 {
109 cx.zoom_window();
110 }
111 })
112 // left side
113 .child(
114 div()
115 .flex()
116 .flex_row()
117 .gap_1()
118 .children(self.project_host)
119 .child(self.projects), // .children(self.render_project_branch(cx))
120 // .children(self.render_collaborators(cx)),
121 )
122 // right side
123 // .child(
124 // div()
125 // .flex()
126 // .flex_row()
127 // .gap_1()
128 // .pr_1()
129 // .when_some(room, |this, room| {
130 // let room = room.read(cx);
131 // let project = self.project.read(cx);
132 // let is_local = project.is_local();
133 // let is_shared = is_local && project.is_shared();
134 // let is_muted = room.is_muted(cx);
135 // let is_deafened = room.is_deafened().unwrap_or(false);
136 // let is_screen_sharing = room.is_screen_sharing();
137
138 // this.when(is_local, |this| {
139 // this.child(
140 // Button::new(
141 // "toggle_sharing",
142 // if is_shared { "Unshare" } else { "Share" },
143 // )
144 // .style(ButtonStyle::Subtle)
145 // .label_size(LabelSize::Small)
146 // .on_click(cx.listener(
147 // move |this, _, cx| {
148 // if is_shared {
149 // this.unshare_project(&Default::default(), cx);
150 // } else {
151 // this.share_project(&Default::default(), cx);
152 // }
153 // },
154 // )),
155 // )
156 // })
157 // .child(
158 // IconButton::new("leave-call", ui::Icon::Exit)
159 // .style(ButtonStyle::Subtle)
160 // .icon_size(IconSize::Small)
161 // .on_click(move |_, cx| {
162 // ActiveCall::global(cx)
163 // .update(cx, |call, cx| call.hang_up(cx))
164 // .detach_and_log_err(cx);
165 // }),
166 // )
167 // .child(
168 // IconButton::new(
169 // "mute-microphone",
170 // if is_muted {
171 // ui::Icon::MicMute
172 // } else {
173 // ui::Icon::Mic
174 // },
175 // )
176 // .style(ButtonStyle::Subtle)
177 // .icon_size(IconSize::Small)
178 // .selected(is_muted)
179 // .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
180 // )
181 // .child(
182 // IconButton::new(
183 // "mute-sound",
184 // if is_deafened {
185 // ui::Icon::AudioOff
186 // } else {
187 // ui::Icon::AudioOn
188 // },
189 // )
190 // .style(ButtonStyle::Subtle)
191 // .icon_size(IconSize::Small)
192 // .selected(is_deafened)
193 // .tooltip(move |cx| {
194 // Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx)
195 // })
196 // .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
197 // )
198 // .child(
199 // IconButton::new("screen-share", ui::Icon::Screen)
200 // .style(ButtonStyle::Subtle)
201 // .icon_size(IconSize::Small)
202 // .selected(is_screen_sharing)
203 // .on_click(move |_, cx| {
204 // crate::toggle_screen_sharing(&Default::default(), cx)
205 // }),
206 // )
207 // })
208 // .map(|el| {
209 // let status = self.client.status();
210 // let status = &*status.borrow();
211 // if matches!(status, client::Status::Connected { .. }) {
212 // el.child(self.render_user_menu_button(cx))
213 // } else {
214 // el.children(self.render_connection_status(status, cx))
215 // .child(self.render_sign_in_button(cx))
216 // .child(self.render_user_menu_button(cx))
217 // }
218 // }),
219 // )
220 }
221}
222
223impl<D: TitlebarDelegate> RenderOnce for ProjectHost<D> {
224 type Output = Button;
225
226 fn render(self, _: &mut WindowContext) -> Self::Output {
227 let delegate = self.delegate;
228 Button::new("project-host", self.login)
229 .color(Color::Player(self.peer_index))
230 .style(ButtonStyle::Subtle)
231 .label_size(LabelSize::Small)
232 .tooltip(move |cx| Tooltip::text("Toggle following", cx))
233 .on_click(move |_, cx| {
234 let host_id = self.id;
235 delegate.update(cx, |this, cx| this.toggle_following(host_id, cx))
236 })
237 }
238}
239
240impl<D: 'static> EventEmitter<DismissEvent> for ProjectsMenu<D> {}
241
242impl<D: TitlebarDelegate> Render for ProjectsMenu<D> {
243 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
244 todo!()
245 }
246}
247
248impl<D: TitlebarDelegate> FocusableView for ProjectsMenu<D> {
249 fn focus_handle(&self, _cx: &gpui::AppContext) -> gpui::FocusHandle {
250 self.focus.clone()
251 }
252}
253
254impl<D: TitlebarDelegate> RenderOnce for Projects<D> {
255 type Output = PopoverMenu<ProjectsMenu<D>>;
256
257 fn render(self, _cx: &mut WindowContext) -> Self::Output {
258 let delegate = self.delegate;
259 let recent_projects = self.recent;
260
261 popover_menu("recent-projects")
262 .trigger(
263 Button::new("trigger", self.current)
264 .style(ButtonStyle::Subtle)
265 .label_size(LabelSize::Small)
266 .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
267 )
268 .menu(move |cx| {
269 if recent_projects.is_empty() {
270 None
271 } else {
272 Some(cx.new_view(|cx| ProjectsMenu {
273 delegate: delegate.clone(),
274 focus: cx.focus_handle(),
275 recent: recent_projects.clone(),
276 }))
277 }
278 })
279 }
280}
281
282impl RenderOnce for FacePile {
283 type Output = Div;
284
285 fn render(self, _: &mut WindowContext) -> Self::Output {
286 let face_count = self.faces.len();
287 div()
288 .p_1()
289 .flex()
290 .items_center()
291 .children(self.faces.into_iter().enumerate().map(|(ix, avatar)| {
292 let last_child = ix == face_count - 1;
293 div()
294 .z_index((face_count - ix) as u8)
295 .when(!last_child, |div| div.neg_mr_1())
296 .child(avatar)
297 }))
298 }
299}
300
301impl RenderOnce for Avatar {
302 type Output = Div;
303
304 fn render(self, cx: &mut WindowContext) -> Self::Output {
305 div()
306 .map(|this| match self.shape {
307 AvatarShape::Square => this.rounded_md(),
308 AvatarShape::Circle => this.rounded_full(),
309 })
310 .map(|this| match self.audio_status {
311 AudioStatus::None => this,
312 AudioStatus::Muted => this.border_color(cx.theme().muted),
313 AudioStatus::Speaking => this.border_color(cx.theme().speaking),
314 })
315 .size(cx.rem_size() + px(2.))
316 .child(
317 img(self.image_uri)
318 .size(cx.rem_size())
319 .bg(cx.theme().cotton.disabled.background),
320 )
321 .children(self.available.map(|is_free| {
322 // Non-integer sizes result in non-round indicators.
323 let indicator_size = (cx.rem_size() * 0.4).round();
324
325 div()
326 .absolute()
327 .z_index(1)
328 .bg(if is_free {
329 cx.theme().positive.default.background
330 } else {
331 cx.theme().negative.default.background
332 })
333 .size(indicator_size)
334 .rounded(indicator_size)
335 .bottom_0()
336 .right_0()
337 }))
338 }
339}
340
341pub enum AudioStatus {
342 None,
343 Muted,
344 Speaking,
345}
346
347// impl Titlebar {
348// pub fn render_project_host(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
349// let host = self.project.read(cx).host()?;
350// let host = self.user_store.read(cx).get_cached_user(host.user_id)?;
351// let participant_index = self
352// .user_store
353// .read(cx)
354// .participant_indices()
355// .get(&host.id)?;
356// Some(
357// div().border().border_color(gpui::red()).child(
358// Button::new("project_owner_trigger", host.github_login.clone())
359// .color(Color::Player(participant_index.0))
360// .style(ButtonStyle::Subtle)
361// .label_size(LabelSize::Small)
362// .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
363// ),
364// )
365// }
366
367// pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
368// let entry = {
369// let mut names_and_branches =
370// self.project.read(cx).visible_worktrees(cx).map(|worktree| {
371// let worktree = worktree.read(cx);
372// worktree.root_git_entry()
373// });
374
375// names_and_branches.next().flatten()
376// };
377// let workspace = self.workspace.upgrade()?;
378// let branch_name = entry
379// .as_ref()
380// .and_then(RepositoryEntry::branch)
381// .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
382// Some(
383// popover_menu("project_branch_trigger")
384// .trigger(
385// Button::new("project_branch_trigger", branch_name)
386// .color(Color::Muted)
387// .style(ButtonStyle::Subtle)
388// .label_size(LabelSize::Small)
389// .tooltip(move |cx| {
390// Tooltip::with_meta(
391// "Recent Branches",
392// Some(&ToggleVcsMenu),
393// "Local branches only",
394// cx,
395// )
396// }),
397// )
398// .menu(move |cx| Self::render_vcs_popover(workspace.clone(), cx)),
399// )
400// }
401
402// fn render_collaborator(
403// &self,
404// user: &Arc<User>,
405// peer_id: PeerId,
406// is_present: bool,
407// is_speaking: bool,
408// is_muted: bool,
409// room: &Room,
410// project_id: Option<u64>,
411// current_user: &Arc<User>,
412// ) -> Option<FacePile> {
413// let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
414
415// let pile = FacePile::default()
416// .child(
417// Avatar::new(user.avatar_uri.clone())
418// .grayscale(!is_present)
419// .border_color(if is_speaking {
420// gpui::blue()
421// } else if is_muted {
422// gpui::red()
423// } else {
424// Hsla::default()
425// }),
426// )
427// .children(followers.iter().filter_map(|follower_peer_id| {
428// let follower = room
429// .remote_participants()
430// .values()
431// .find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user))
432// .or_else(|| {
433// (self.client.peer_id() == Some(*follower_peer_id)).then_some(current_user)
434// })?
435// .clone();
436
437// Some(Avatar::new(follower.avatar_uri.clone()))
438// }));
439
440// Some(pile)
441// }
442// }