1mod call_controls;
2mod collab;
3mod platforms;
4mod window_controls;
5
6use crate::platforms::{platform_linux, platform_mac, platform_windows};
7use auto_update::AutoUpdateStatus;
8use call::{ActiveCall, ParticipantLocation};
9use client::{Client, UserStore};
10use collab::render_color_ribbon;
11use gpui::{
12 actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement,
13 Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
14 StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
15};
16use project::{Project, RepositoryEntry};
17use recent_projects::RecentProjects;
18use rpc::proto::DevServerStatus;
19use settings::Settings;
20use smallvec::SmallVec;
21use std::sync::Arc;
22use theme::{ActiveTheme, ThemeSettings};
23use ui::{
24 h_flex, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconButton,
25 IconName, Indicator, PopoverMenu, TintColor, Tooltip,
26};
27use util::ResultExt;
28use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
29use workspace::{notifications::NotifyResultExt, Workspace};
30
31const MAX_PROJECT_NAME_LENGTH: usize = 40;
32const MAX_BRANCH_NAME_LENGTH: usize = 40;
33
34actions!(
35 collab,
36 [
37 ShareProject,
38 UnshareProject,
39 ToggleUserMenu,
40 ToggleProjectMenu,
41 SwitchBranch
42 ]
43);
44
45pub fn init(cx: &mut AppContext) {
46 cx.observe_new_views(|workspace: &mut Workspace, cx| {
47 let item = cx.new_view(|cx| TitleBar::new("title-bar", workspace, cx));
48 workspace.set_titlebar_item(item.into(), cx)
49 })
50 .detach();
51}
52
53pub struct TitleBar {
54 platform_style: PlatformStyle,
55 content: Stateful<Div>,
56 children: SmallVec<[AnyElement; 2]>,
57 project: Model<Project>,
58 user_store: Model<UserStore>,
59 client: Arc<Client>,
60 workspace: WeakView<Workspace>,
61 should_move: bool,
62 _subscriptions: Vec<Subscription>,
63}
64
65impl Render for TitleBar {
66 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
67 let room = ActiveCall::global(cx).read(cx).room().cloned();
68 let current_user = self.user_store.read(cx).current_user();
69 let client = self.client.clone();
70 let project_id = self.project.read(cx).remote_id();
71 let workspace = self.workspace.upgrade();
72 let close_action = Box::new(workspace::CloseWindow);
73
74 let platform_supported = cfg!(target_os = "macos");
75
76 let height = Self::height(cx);
77 let supported_controls = cx.window_controls();
78 let decorations = cx.window_decorations();
79
80 h_flex()
81 .id("titlebar")
82 .w_full()
83 .pt(Self::top_padding(cx))
84 .h(height + Self::top_padding(cx))
85 .map(|this| {
86 if cx.is_fullscreen() {
87 this.pl_2()
88 } else if self.platform_style == PlatformStyle::Mac {
89 this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING))
90 } else {
91 this.pl_2()
92 }
93 })
94 .map(|el| {
95 match decorations {
96 Decorations::Server => el,
97 Decorations::Client { tiling, .. } => el
98 .when(!(tiling.top || tiling.right), |el| {
99 el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
100 })
101 .when(!(tiling.top || tiling.left), |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING))
102 }
103 })
104 .bg(cx.theme().colors().title_bar_background)
105 .content_stretch()
106 .child(
107 div()
108 .id("titlebar-content")
109 .flex()
110 .flex_row()
111 .justify_between()
112 .w_full()
113 // note: on windows titlebar behaviour is handled by the platform implementation
114 .when(cfg!(not(windows)), |this| {
115 this.on_click(|event, cx| {
116 if event.up.click_count == 2 {
117 cx.zoom_window();
118 }
119 })
120 })
121 // left side
122 .child(
123 h_flex()
124 .gap_1()
125 .children(self.render_application_menu(cx))
126 .children(self.render_project_host(cx))
127 .child(self.render_project_name(cx))
128 .children(self.render_project_branch(cx))
129 .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
130 )
131 .child(
132 h_flex()
133 .id("collaborator-list")
134 .w_full()
135 .gap_1()
136 .overflow_x_scroll()
137 .when_some(
138 current_user.clone().zip(client.peer_id()).zip(room.clone()),
139 |this, ((current_user, peer_id), room)| {
140 let player_colors = cx.theme().players();
141 let room = room.read(cx);
142 let mut remote_participants =
143 room.remote_participants().values().collect::<Vec<_>>();
144 remote_participants.sort_by_key(|p| p.participant_index.0);
145
146 let current_user_face_pile = self.render_collaborator(
147 ¤t_user,
148 peer_id,
149 true,
150 room.is_speaking(),
151 room.is_muted(),
152 None,
153 &room,
154 project_id,
155 ¤t_user,
156 cx,
157 );
158
159 this.children(current_user_face_pile.map(|face_pile| {
160 v_flex()
161 .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
162 .child(face_pile)
163 .child(render_color_ribbon(player_colors.local().cursor))
164 }))
165 .children(
166 remote_participants.iter().filter_map(|collaborator| {
167 let player_color = player_colors
168 .color_for_participant(collaborator.participant_index.0);
169 let is_following = workspace
170 .as_ref()?
171 .read(cx)
172 .is_being_followed(collaborator.peer_id);
173 let is_present = project_id.map_or(false, |project_id| {
174 collaborator.location
175 == ParticipantLocation::SharedProject { project_id }
176 });
177
178 let facepile = self.render_collaborator(
179 &collaborator.user,
180 collaborator.peer_id,
181 is_present,
182 collaborator.speaking,
183 collaborator.muted,
184 is_following.then_some(player_color.selection),
185 &room,
186 project_id,
187 ¤t_user,
188 cx,
189 )?;
190
191 Some(
192 v_flex()
193 .id(("collaborator", collaborator.user.id))
194 .child(facepile)
195 .child(render_color_ribbon(player_color.cursor))
196 .cursor_pointer()
197 .on_click({
198 let peer_id = collaborator.peer_id;
199 cx.listener(move |this, _, cx| {
200 this.workspace
201 .update(cx, |workspace, cx| {
202 workspace.follow(peer_id, cx);
203 })
204 .ok();
205 })
206 })
207 .tooltip({
208 let login = collaborator.user.github_login.clone();
209 move |cx| {
210 Tooltip::text(format!("Follow {login}"), cx)
211 }
212 }),
213 )
214 }),
215 )
216 },
217 ),
218 )
219 // right side
220 .child(
221 h_flex()
222 .gap_1()
223 .pr_1()
224 .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
225 .when_some(room, |this, room| {
226 let room = room.read(cx);
227 let project = self.project.read(cx);
228 let is_local = project.is_local();
229 let is_dev_server_project = project.dev_server_project_id().is_some();
230 let is_shared = (is_local || is_dev_server_project) && project.is_shared();
231 let is_muted = room.is_muted();
232 let is_deafened = room.is_deafened().unwrap_or(false);
233 let is_screen_sharing = room.is_screen_sharing();
234 let can_use_microphone = room.can_use_microphone();
235 let can_share_projects = room.can_share_projects();
236
237 this.when(
238 (is_local || is_dev_server_project) && can_share_projects,
239 |this| {
240 this.child(
241 Button::new(
242 "toggle_sharing",
243 if is_shared { "Unshare" } else { "Share" },
244 )
245 .tooltip(move |cx| {
246 Tooltip::text(
247 if is_shared {
248 "Stop sharing project with call participants"
249 } else {
250 "Share project with call participants"
251 },
252 cx,
253 )
254 })
255 .style(ButtonStyle::Subtle)
256 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
257 .selected(is_shared)
258 .label_size(LabelSize::Small)
259 .on_click(cx.listener(
260 move |this, _, cx| {
261 if is_shared {
262 this.unshare_project(&Default::default(), cx);
263 } else {
264 this.share_project(&Default::default(), cx);
265 }
266 },
267 )),
268 )
269 },
270 )
271 .child(
272 div()
273 .child(
274 IconButton::new("leave-call", ui::IconName::Exit)
275 .style(ButtonStyle::Subtle)
276 .tooltip(|cx| Tooltip::text("Leave call", cx))
277 .icon_size(IconSize::Small)
278 .on_click(move |_, cx| {
279 ActiveCall::global(cx)
280 .update(cx, |call, cx| call.hang_up(cx))
281 .detach_and_log_err(cx);
282 }),
283 )
284 .pr_2(),
285 )
286 .when(can_use_microphone, |this| {
287 this.child(
288 IconButton::new(
289 "mute-microphone",
290 if is_muted {
291 ui::IconName::MicMute
292 } else {
293 ui::IconName::Mic
294 },
295 )
296 .tooltip(move |cx| {
297 Tooltip::text(
298 if !platform_supported {
299 "Cannot share microphone"
300 } else if is_muted {
301 "Unmute microphone"
302 } else {
303 "Mute microphone"
304 },
305 cx,
306 )
307 })
308 .style(ButtonStyle::Subtle)
309 .icon_size(IconSize::Small)
310 .selected(platform_supported && is_muted)
311 .disabled(!platform_supported)
312 .selected_style(ButtonStyle::Tinted(TintColor::Negative))
313 .on_click(move |_, cx| {
314 call_controls::toggle_mute(&Default::default(), cx);
315 }),
316 )
317 })
318 .child(
319 IconButton::new(
320 "mute-sound",
321 if is_deafened {
322 ui::IconName::AudioOff
323 } else {
324 ui::IconName::AudioOn
325 },
326 )
327 .style(ButtonStyle::Subtle)
328 .selected_style(ButtonStyle::Tinted(TintColor::Negative))
329 .icon_size(IconSize::Small)
330 .selected(is_deafened)
331 .disabled(!platform_supported)
332 .tooltip(move |cx| {
333 if !platform_supported {
334 Tooltip::text("Cannot share microphone", cx)
335 } else if can_use_microphone {
336 Tooltip::with_meta(
337 "Deafen Audio",
338 None,
339 "Mic will be muted",
340 cx,
341 )
342 } else {
343 Tooltip::text("Deafen Audio", cx)
344 }
345 })
346 .on_click(move |_, cx| {
347 call_controls::toggle_deafen(&Default::default(), cx)
348 }),
349 )
350 .when(can_share_projects, |this| {
351 this.child(
352 IconButton::new("screen-share", ui::IconName::Screen)
353 .style(ButtonStyle::Subtle)
354 .icon_size(IconSize::Small)
355 .selected(is_screen_sharing)
356 .disabled(!platform_supported)
357 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
358 .tooltip(move |cx| {
359 Tooltip::text(
360 if !platform_supported {
361 "Cannot share screen"
362 } else if is_screen_sharing {
363 "Stop Sharing Screen"
364 } else {
365 "Share Screen"
366 },
367 cx,
368 )
369 })
370 .on_click(move |_, cx| {
371 call_controls::toggle_screen_sharing(&Default::default(), cx)
372 }),
373 )
374 })
375 .child(div().pr_2())
376 })
377 .map(|el| {
378 let status = self.client.status();
379 let status = &*status.borrow();
380 if matches!(status, client::Status::Connected { .. }) {
381 el.child(self.render_user_menu_button(cx))
382 } else {
383 el.children(self.render_connection_status(status, cx))
384 .child(self.render_sign_in_button(cx))
385 .child(self.render_user_menu_button(cx))
386 }
387 }),
388 )
389
390 ).when(
391 self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(),
392 |title_bar| title_bar.child(platform_windows::WindowsWindowControls::new(height)),
393 ).when(
394 self.platform_style == PlatformStyle::Linux
395 && !cx.is_fullscreen()
396 && matches!(decorations, Decorations::Client { .. }),
397 |title_bar| {
398 title_bar
399 .child(platform_linux::LinuxWindowControls::new(close_action))
400 .when(supported_controls.window_menu, |titlebar| {
401 titlebar.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
402 cx.show_window_menu(ev.position)
403 })
404 })
405
406 .on_mouse_move(cx.listener(move |this, _ev, cx| {
407 if this.should_move {
408 this.should_move = false;
409 cx.start_window_move();
410 }
411 }))
412 .on_mouse_down_out(cx.listener(move |this, _ev, _cx| {
413 this.should_move = false;
414 }))
415 .on_mouse_down(gpui::MouseButton::Left, cx.listener(move |this, _ev, _cx| {
416 this.should_move = true;
417 }))
418
419 },
420 )
421 }
422}
423
424impl TitleBar {
425 pub fn new(
426 id: impl Into<ElementId>,
427 workspace: &Workspace,
428 cx: &mut ViewContext<Self>,
429 ) -> Self {
430 let project = workspace.project().clone();
431 let user_store = workspace.app_state().user_store.clone();
432 let client = workspace.app_state().client.clone();
433 let active_call = ActiveCall::global(cx);
434 let mut subscriptions = Vec::new();
435 subscriptions.push(
436 cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
437 cx.notify()
438 }),
439 );
440 subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
441 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
442 subscriptions.push(cx.observe_window_activation(Self::window_activation_changed));
443 subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
444
445 Self {
446 platform_style: PlatformStyle::platform(),
447 content: div().id(id.into()),
448 children: SmallVec::new(),
449 workspace: workspace.weak_handle(),
450 should_move: false,
451 project,
452 user_store,
453 client,
454 _subscriptions: subscriptions,
455 }
456 }
457
458 #[cfg(not(target_os = "windows"))]
459 pub fn height(cx: &mut WindowContext) -> Pixels {
460 (1.75 * cx.rem_size()).max(px(34.))
461 }
462
463 #[cfg(target_os = "windows")]
464 pub fn height(_cx: &mut WindowContext) -> Pixels {
465 // todo(windows) instead of hard coded size report the actual size to the Windows platform API
466 px(32.)
467 }
468
469 #[cfg(not(target_os = "windows"))]
470 fn top_padding(_cx: &WindowContext) -> Pixels {
471 px(0.)
472 }
473
474 #[cfg(target_os = "windows")]
475 fn top_padding(cx: &WindowContext) -> Pixels {
476 use windows::Win32::UI::{
477 HiDpi::GetSystemMetricsForDpi,
478 WindowsAndMessaging::{SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI},
479 };
480
481 // This top padding is not dependent on the title bar style and is instead a quirk of maximized windows on Windows:
482 // https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543
483 let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI) };
484 if cx.is_maximized() {
485 px((padding * 2) as f32)
486 } else {
487 px(0.)
488 }
489 }
490
491 /// Sets the platform style.
492 pub fn platform_style(mut self, style: PlatformStyle) -> Self {
493 self.platform_style = style;
494 self
495 }
496
497 pub fn render_application_menu(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
498 cfg!(not(target_os = "macos")).then(|| {
499 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
500 let font = cx.text_style().font();
501 let font_id = cx.text_system().resolve_font(&font);
502 let width = cx
503 .text_system()
504 .typographic_bounds(font_id, ui_font_size, 'm')
505 .unwrap()
506 .size
507 .width
508 * 3.0;
509
510 PopoverMenu::new("application-menu")
511 .menu(move |cx| {
512 let width = width;
513 ContextMenu::build(cx, move |menu, _cx| {
514 let width = width;
515 menu.header("Workspace")
516 .action("Open Command Palette", Box::new(command_palette::Toggle))
517 .custom_row(move |cx| {
518 div()
519 .w_full()
520 .flex()
521 .flex_row()
522 .justify_between()
523 .cursor(gpui::CursorStyle::Arrow)
524 .child(Label::new("Buffer Font Size"))
525 .child(
526 div()
527 .flex()
528 .flex_row()
529 .child(div().w(px(16.0)))
530 .child(
531 IconButton::new(
532 "reset-buffer-zoom",
533 IconName::RotateCcw,
534 )
535 .on_click(|_, cx| {
536 cx.dispatch_action(Box::new(
537 zed_actions::ResetBufferFontSize,
538 ))
539 }),
540 )
541 .child(
542 IconButton::new("--buffer-zoom", IconName::Dash)
543 .on_click(|_, cx| {
544 cx.dispatch_action(Box::new(
545 zed_actions::DecreaseBufferFontSize,
546 ))
547 }),
548 )
549 .child(
550 div()
551 .w(width)
552 .flex()
553 .flex_row()
554 .justify_around()
555 .child(Label::new(
556 theme::get_buffer_font_size(cx).to_string(),
557 )),
558 )
559 .child(
560 IconButton::new("+-buffer-zoom", IconName::Plus)
561 .on_click(|_, cx| {
562 cx.dispatch_action(Box::new(
563 zed_actions::IncreaseBufferFontSize,
564 ))
565 }),
566 ),
567 )
568 .into_any_element()
569 })
570 .custom_row(move |cx| {
571 div()
572 .w_full()
573 .flex()
574 .flex_row()
575 .justify_between()
576 .cursor(gpui::CursorStyle::Arrow)
577 .child(Label::new("UI Font Size"))
578 .child(
579 div()
580 .flex()
581 .flex_row()
582 .child(
583 IconButton::new(
584 "reset-ui-zoom",
585 IconName::RotateCcw,
586 )
587 .on_click(|_, cx| {
588 cx.dispatch_action(Box::new(
589 zed_actions::ResetUiFontSize,
590 ))
591 }),
592 )
593 .child(
594 IconButton::new("--ui-zoom", IconName::Dash)
595 .on_click(|_, cx| {
596 cx.dispatch_action(Box::new(
597 zed_actions::DecreaseUiFontSize,
598 ))
599 }),
600 )
601 .child(
602 div()
603 .w(width)
604 .flex()
605 .flex_row()
606 .justify_around()
607 .child(Label::new(
608 theme::get_ui_font_size(cx).to_string(),
609 )),
610 )
611 .child(
612 IconButton::new("+-ui-zoom", IconName::Plus)
613 .on_click(|_, cx| {
614 cx.dispatch_action(Box::new(
615 zed_actions::IncreaseUiFontSize,
616 ))
617 }),
618 ),
619 )
620 .into_any_element()
621 })
622 .header("Project")
623 .action(
624 "Add Folder to Project...",
625 Box::new(workspace::AddFolderToProject),
626 )
627 .action("Open a new Project...", Box::new(workspace::Open))
628 .action(
629 "Open Recent Projects...",
630 Box::new(recent_projects::OpenRecent {
631 create_new_window: false,
632 }),
633 )
634 .header("Help")
635 .action("About Zed", Box::new(zed_actions::About))
636 .action("Welcome", Box::new(workspace::Welcome))
637 .link(
638 "Documentation",
639 Box::new(zed_actions::OpenBrowser {
640 url: "https://zed.dev/docs".into(),
641 }),
642 )
643 .action("Give Feedback", Box::new(feedback::GiveFeedback))
644 .action("Check for Updates", Box::new(auto_update::Check))
645 .action("View Telemetry", Box::new(zed_actions::OpenTelemetryLog))
646 .action(
647 "View Dependency Licenses",
648 Box::new(zed_actions::OpenLicenses),
649 )
650 .separator()
651 .action("Quit", Box::new(zed_actions::Quit))
652 })
653 .into()
654 })
655 .trigger(
656 IconButton::new("application-menu", ui::IconName::Menu)
657 .style(ButtonStyle::Subtle)
658 .tooltip(|cx| Tooltip::text("Open Application Menu", cx))
659 .icon_size(IconSize::Small),
660 )
661 .into_any_element()
662 })
663 }
664
665 pub fn render_project_host(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
666 if let Some(dev_server) =
667 self.project
668 .read(cx)
669 .dev_server_project_id()
670 .and_then(|dev_server_project_id| {
671 dev_server_projects::Store::global(cx)
672 .read(cx)
673 .dev_server_for_project(dev_server_project_id)
674 })
675 {
676 return Some(
677 ButtonLike::new("dev_server_trigger")
678 .child(Indicator::dot().color(
679 if dev_server.status == DevServerStatus::Online {
680 Color::Created
681 } else {
682 Color::Disabled
683 },
684 ))
685 .child(
686 Label::new(dev_server.name.clone())
687 .size(LabelSize::Small)
688 .line_height_style(LineHeightStyle::UiLabel),
689 )
690 .tooltip(move |cx| Tooltip::text("Project is hosted on a dev server", cx))
691 .on_click(cx.listener(|this, _, cx| {
692 if let Some(workspace) = this.workspace.upgrade() {
693 recent_projects::DevServerProjects::open(workspace, cx)
694 }
695 }))
696 .into_any_element(),
697 );
698 }
699
700 if self.project.read(cx).is_disconnected() {
701 return Some(
702 Button::new("disconnected", "Disconnected")
703 .disabled(true)
704 .color(Color::Disabled)
705 .style(ButtonStyle::Subtle)
706 .label_size(LabelSize::Small)
707 .into_any_element(),
708 );
709 }
710
711 let host = self.project.read(cx).host()?;
712 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
713 let participant_index = self
714 .user_store
715 .read(cx)
716 .participant_indices()
717 .get(&host_user.id)?;
718 Some(
719 Button::new("project_owner_trigger", host_user.github_login.clone())
720 .color(Color::Player(participant_index.0))
721 .style(ButtonStyle::Subtle)
722 .label_size(LabelSize::Small)
723 .tooltip(move |cx| {
724 Tooltip::text(
725 format!(
726 "{} is sharing this project. Click to follow.",
727 host_user.github_login.clone()
728 ),
729 cx,
730 )
731 })
732 .on_click({
733 let host_peer_id = host.peer_id;
734 cx.listener(move |this, _, cx| {
735 this.workspace
736 .update(cx, |workspace, cx| {
737 workspace.follow(host_peer_id, cx);
738 })
739 .log_err();
740 })
741 })
742 .into_any_element(),
743 )
744 }
745
746 pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
747 let name = {
748 let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
749 let worktree = worktree.read(cx);
750 worktree.root_name()
751 });
752
753 names.next()
754 };
755 let is_project_selected = name.is_some();
756 let name = if let Some(name) = name {
757 util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
758 } else {
759 "Open recent project".to_string()
760 };
761
762 let workspace = self.workspace.clone();
763 Button::new("project_name_trigger", name)
764 .when(!is_project_selected, |b| b.color(Color::Muted))
765 .style(ButtonStyle::Subtle)
766 .label_size(LabelSize::Small)
767 .tooltip(move |cx| {
768 Tooltip::for_action(
769 "Recent Projects",
770 &recent_projects::OpenRecent {
771 create_new_window: false,
772 },
773 cx,
774 )
775 })
776 .on_click(cx.listener(move |_, _, cx| {
777 if let Some(workspace) = workspace.upgrade() {
778 workspace.update(cx, |workspace, cx| {
779 RecentProjects::open(workspace, false, cx);
780 })
781 }
782 }))
783 }
784
785 pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
786 let entry = {
787 let mut names_and_branches =
788 self.project.read(cx).visible_worktrees(cx).map(|worktree| {
789 let worktree = worktree.read(cx);
790 worktree.root_git_entry()
791 });
792
793 names_and_branches.next().flatten()
794 };
795 let workspace = self.workspace.upgrade()?;
796 let branch_name = entry
797 .as_ref()
798 .and_then(RepositoryEntry::branch)
799 .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
800 Some(
801 Button::new("project_branch_trigger", branch_name)
802 .color(Color::Muted)
803 .style(ButtonStyle::Subtle)
804 .label_size(LabelSize::Small)
805 .tooltip(move |cx| {
806 Tooltip::with_meta(
807 "Recent Branches",
808 Some(&ToggleVcsMenu),
809 "Local branches only",
810 cx,
811 )
812 })
813 .on_click(move |_, cx| {
814 let _ = workspace.update(cx, |this, cx| {
815 BranchList::open(this, &Default::default(), cx)
816 });
817 }),
818 )
819 }
820
821 fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
822 if cx.is_window_active() {
823 ActiveCall::global(cx)
824 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
825 .detach_and_log_err(cx);
826 } else if cx.active_window().is_none() {
827 ActiveCall::global(cx)
828 .update(cx, |call, cx| call.set_location(None, cx))
829 .detach_and_log_err(cx);
830 }
831 self.workspace
832 .update(cx, |workspace, cx| {
833 workspace.update_active_view_for_followers(cx);
834 })
835 .ok();
836 }
837
838 fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
839 cx.notify();
840 }
841
842 fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
843 let active_call = ActiveCall::global(cx);
844 let project = self.project.clone();
845 active_call
846 .update(cx, |call, cx| call.share_project(project, cx))
847 .detach_and_log_err(cx);
848 }
849
850 fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
851 let active_call = ActiveCall::global(cx);
852 let project = self.project.clone();
853 active_call
854 .update(cx, |call, cx| call.unshare_project(project, cx))
855 .log_err();
856 }
857
858 fn render_connection_status(
859 &self,
860 status: &client::Status,
861 cx: &mut ViewContext<Self>,
862 ) -> Option<AnyElement> {
863 match status {
864 client::Status::ConnectionError
865 | client::Status::ConnectionLost
866 | client::Status::Reauthenticating { .. }
867 | client::Status::Reconnecting { .. }
868 | client::Status::ReconnectionError { .. } => Some(
869 div()
870 .id("disconnected")
871 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
872 .tooltip(|cx| Tooltip::text("Disconnected", cx))
873 .into_any_element(),
874 ),
875 client::Status::UpgradeRequired => {
876 let auto_updater = auto_update::AutoUpdater::get(cx);
877 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
878 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
879 Some(AutoUpdateStatus::Installing)
880 | Some(AutoUpdateStatus::Downloading)
881 | Some(AutoUpdateStatus::Checking) => "Updating...",
882 Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
883 "Please update Zed to Collaborate"
884 }
885 };
886
887 Some(
888 Button::new("connection-status", label)
889 .label_size(LabelSize::Small)
890 .on_click(|_, cx| {
891 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
892 if auto_updater.read(cx).status().is_updated() {
893 workspace::reload(&Default::default(), cx);
894 return;
895 }
896 }
897 auto_update::check(&Default::default(), cx);
898 })
899 .into_any_element(),
900 )
901 }
902 _ => None,
903 }
904 }
905
906 pub fn render_sign_in_button(&mut self, _: &mut ViewContext<Self>) -> Button {
907 let client = self.client.clone();
908 Button::new("sign_in", "Sign in")
909 .label_size(LabelSize::Small)
910 .on_click(move |_, cx| {
911 let client = client.clone();
912 cx.spawn(move |mut cx| async move {
913 client
914 .authenticate_and_connect(true, &cx)
915 .await
916 .notify_async_err(&mut cx);
917 })
918 .detach();
919 })
920 }
921
922 pub fn render_user_menu_button(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
923 if let Some(user) = self.user_store.read(cx).current_user() {
924 PopoverMenu::new("user-menu")
925 .menu(|cx| {
926 ContextMenu::build(cx, |menu, _| {
927 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
928 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
929 .action("Themes…", theme_selector::Toggle::default().boxed_clone())
930 .action("Extensions", extensions_ui::Extensions.boxed_clone())
931 .separator()
932 .action("Sign Out", client::SignOut.boxed_clone())
933 })
934 .into()
935 })
936 .trigger(
937 ButtonLike::new("user-menu")
938 .child(
939 h_flex()
940 .gap_0p5()
941 .child(Avatar::new(user.avatar_uri.clone()))
942 .child(
943 Icon::new(IconName::ChevronDown)
944 .size(IconSize::Small)
945 .color(Color::Muted),
946 ),
947 )
948 .style(ButtonStyle::Subtle)
949 .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
950 )
951 .anchor(gpui::AnchorCorner::TopRight)
952 } else {
953 PopoverMenu::new("user-menu")
954 .menu(|cx| {
955 ContextMenu::build(cx, |menu, _| {
956 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
957 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
958 .action("Themes…", theme_selector::Toggle::default().boxed_clone())
959 .action("Extensions", extensions_ui::Extensions.boxed_clone())
960 })
961 .into()
962 })
963 .trigger(
964 ButtonLike::new("user-menu")
965 .child(
966 h_flex().gap_0p5().child(
967 Icon::new(IconName::ChevronDown)
968 .size(IconSize::Small)
969 .color(Color::Muted),
970 ),
971 )
972 .style(ButtonStyle::Subtle)
973 .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
974 )
975 }
976 }
977}
978
979impl InteractiveElement for TitleBar {
980 fn interactivity(&mut self) -> &mut Interactivity {
981 self.content.interactivity()
982 }
983}
984
985impl StatefulInteractiveElement for TitleBar {}
986
987impl ParentElement for TitleBar {
988 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
989 self.children.extend(elements)
990 }
991}