title_bar.rs

  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                                            &current_user,
148                                            peer_id,
149                                            true,
150                                            room.is_speaking(),
151                                            room.is_muted(),
152                                            None,
153                                            &room,
154                                            project_id,
155                                            &current_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                                                    &current_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}