1mod application_menu;
2mod collab;
3mod platforms;
4mod window_controls;
5
6#[cfg(feature = "stories")]
7mod stories;
8
9use crate::application_menu::ApplicationMenu;
10use crate::platforms::{platform_linux, platform_mac, platform_windows};
11use auto_update::AutoUpdateStatus;
12use call::ActiveCall;
13use client::{Client, UserStore};
14use gpui::{
15 actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement,
16 Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
17 StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
18};
19use project::{Project, RepositoryEntry};
20use recent_projects::RecentProjects;
21use rpc::proto::DevServerStatus;
22use smallvec::SmallVec;
23use std::sync::Arc;
24use theme::ActiveTheme;
25use ui::{
26 h_flex, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName,
27 Indicator, PopoverMenu, Tooltip,
28};
29use util::ResultExt;
30use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
31use workspace::{notifications::NotifyResultExt, Workspace};
32
33#[cfg(feature = "stories")]
34pub use stories::*;
35
36const MAX_PROJECT_NAME_LENGTH: usize = 40;
37const MAX_BRANCH_NAME_LENGTH: usize = 40;
38
39actions!(
40 collab,
41 [
42 ShareProject,
43 UnshareProject,
44 ToggleUserMenu,
45 ToggleProjectMenu,
46 SwitchBranch
47 ]
48);
49
50pub fn init(cx: &mut AppContext) {
51 cx.observe_new_views(|workspace: &mut Workspace, cx| {
52 let item = cx.new_view(|cx| TitleBar::new("title-bar", workspace, cx));
53 workspace.set_titlebar_item(item.into(), cx)
54 })
55 .detach();
56}
57
58pub struct TitleBar {
59 platform_style: PlatformStyle,
60 content: Stateful<Div>,
61 children: SmallVec<[AnyElement; 2]>,
62 project: Model<Project>,
63 user_store: Model<UserStore>,
64 client: Arc<Client>,
65 workspace: WeakView<Workspace>,
66 should_move: bool,
67 _subscriptions: Vec<Subscription>,
68}
69
70impl Render for TitleBar {
71 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
72 let close_action = Box::new(workspace::CloseWindow);
73 let height = Self::height(cx);
74 let supported_controls = cx.window_controls();
75 let decorations = cx.window_decorations();
76
77 h_flex()
78 .id("titlebar")
79 .w_full()
80 .h(height)
81 .map(|this| {
82 if cx.is_fullscreen() {
83 this.pl_2()
84 } else if self.platform_style == PlatformStyle::Mac {
85 this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING))
86 } else {
87 this.pl_2()
88 }
89 })
90 .map(|el| match decorations {
91 Decorations::Server => el,
92 Decorations::Client { tiling, .. } => el
93 .when(!(tiling.top || tiling.right), |el| {
94 el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
95 })
96 .when(!(tiling.top || tiling.left), |el| {
97 el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
98 })
99 // this border is to avoid a transparent gap in the rounded corners
100 .mt(px(-1.))
101 .border(px(1.))
102 .border_color(cx.theme().colors().title_bar_background),
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 the title bar behavior is handled by the platform implementation.
114 .when(self.platform_style != PlatformStyle::Windows, |this| {
115 this.on_click(|event, cx| {
116 if event.up.click_count == 2 {
117 cx.zoom_window();
118 }
119 })
120 })
121 .child(
122 h_flex()
123 .gap_1()
124 .children(match self.platform_style {
125 PlatformStyle::Mac => None,
126 PlatformStyle::Linux | PlatformStyle::Windows => {
127 Some(ApplicationMenu::new())
128 }
129 })
130 .children(self.render_project_host(cx))
131 .child(self.render_project_name(cx))
132 .children(self.render_project_branch(cx))
133 .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()),
134 )
135 .child(self.render_collaborator_list(cx))
136 .child(
137 h_flex()
138 .gap_1()
139 .pr_1()
140 .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
141 .children(self.render_call_controls(cx))
142 .map(|el| {
143 let status = self.client.status();
144 let status = &*status.borrow();
145 if matches!(status, client::Status::Connected { .. }) {
146 el.child(self.render_user_menu_button(cx))
147 } else {
148 el.children(self.render_connection_status(status, cx))
149 .child(self.render_sign_in_button(cx))
150 .child(self.render_user_menu_button(cx))
151 }
152 }),
153 ),
154 )
155 .when(!cx.is_fullscreen(), |title_bar| match self.platform_style {
156 PlatformStyle::Mac => title_bar,
157 PlatformStyle::Linux => {
158 if matches!(decorations, Decorations::Client { .. }) {
159 title_bar
160 .child(platform_linux::LinuxWindowControls::new(close_action))
161 .when(supported_controls.window_menu, |titlebar| {
162 titlebar.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
163 cx.show_window_menu(ev.position)
164 })
165 })
166 .on_mouse_move(cx.listener(move |this, _ev, cx| {
167 if this.should_move {
168 this.should_move = false;
169 cx.start_window_move();
170 }
171 }))
172 .on_mouse_down_out(cx.listener(move |this, _ev, _cx| {
173 this.should_move = false;
174 }))
175 .on_mouse_down(
176 gpui::MouseButton::Left,
177 cx.listener(move |this, _ev, _cx| {
178 this.should_move = true;
179 }),
180 )
181 } else {
182 title_bar
183 }
184 }
185 PlatformStyle::Windows => {
186 title_bar.child(platform_windows::WindowsWindowControls::new(height))
187 }
188 })
189 }
190}
191
192impl TitleBar {
193 pub fn new(
194 id: impl Into<ElementId>,
195 workspace: &Workspace,
196 cx: &mut ViewContext<Self>,
197 ) -> Self {
198 let project = workspace.project().clone();
199 let user_store = workspace.app_state().user_store.clone();
200 let client = workspace.app_state().client.clone();
201 let active_call = ActiveCall::global(cx);
202 let mut subscriptions = Vec::new();
203 subscriptions.push(
204 cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
205 cx.notify()
206 }),
207 );
208 subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
209 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
210 subscriptions.push(cx.observe_window_activation(Self::window_activation_changed));
211 subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
212
213 Self {
214 platform_style: PlatformStyle::platform(),
215 content: div().id(id.into()),
216 children: SmallVec::new(),
217 workspace: workspace.weak_handle(),
218 should_move: false,
219 project,
220 user_store,
221 client,
222 _subscriptions: subscriptions,
223 }
224 }
225
226 #[cfg(not(target_os = "windows"))]
227 pub fn height(cx: &mut WindowContext) -> Pixels {
228 (1.75 * cx.rem_size()).max(px(34.))
229 }
230
231 #[cfg(target_os = "windows")]
232 pub fn height(_cx: &mut WindowContext) -> Pixels {
233 // todo(windows) instead of hard coded size report the actual size to the Windows platform API
234 px(32.)
235 }
236
237 /// Sets the platform style.
238 pub fn platform_style(mut self, style: PlatformStyle) -> Self {
239 self.platform_style = style;
240 self
241 }
242
243 pub fn render_project_host(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
244 if let Some(dev_server) =
245 self.project
246 .read(cx)
247 .dev_server_project_id()
248 .and_then(|dev_server_project_id| {
249 dev_server_projects::Store::global(cx)
250 .read(cx)
251 .dev_server_for_project(dev_server_project_id)
252 })
253 {
254 return Some(
255 ButtonLike::new("dev_server_trigger")
256 .child(Indicator::dot().color(
257 if dev_server.status == DevServerStatus::Online {
258 Color::Created
259 } else {
260 Color::Disabled
261 },
262 ))
263 .child(
264 Label::new(dev_server.name.clone())
265 .size(LabelSize::Small)
266 .line_height_style(LineHeightStyle::UiLabel),
267 )
268 .tooltip(move |cx| Tooltip::text("Project is hosted on a dev server", cx))
269 .on_click(cx.listener(|this, _, cx| {
270 if let Some(workspace) = this.workspace.upgrade() {
271 recent_projects::DevServerProjects::open(workspace, cx)
272 }
273 }))
274 .into_any_element(),
275 );
276 }
277
278 if self.project.read(cx).is_disconnected() {
279 return Some(
280 Button::new("disconnected", "Disconnected")
281 .disabled(true)
282 .color(Color::Disabled)
283 .style(ButtonStyle::Subtle)
284 .label_size(LabelSize::Small)
285 .into_any_element(),
286 );
287 }
288
289 let host = self.project.read(cx).host()?;
290 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
291 let participant_index = self
292 .user_store
293 .read(cx)
294 .participant_indices()
295 .get(&host_user.id)?;
296 Some(
297 Button::new("project_owner_trigger", host_user.github_login.clone())
298 .color(Color::Player(participant_index.0))
299 .style(ButtonStyle::Subtle)
300 .label_size(LabelSize::Small)
301 .tooltip(move |cx| {
302 Tooltip::text(
303 format!(
304 "{} is sharing this project. Click to follow.",
305 host_user.github_login.clone()
306 ),
307 cx,
308 )
309 })
310 .on_click({
311 let host_peer_id = host.peer_id;
312 cx.listener(move |this, _, cx| {
313 this.workspace
314 .update(cx, |workspace, cx| {
315 workspace.follow(host_peer_id, cx);
316 })
317 .log_err();
318 })
319 })
320 .into_any_element(),
321 )
322 }
323
324 pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
325 let name = {
326 let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
327 let worktree = worktree.read(cx);
328 worktree.root_name()
329 });
330
331 names.next()
332 };
333 let is_project_selected = name.is_some();
334 let name = if let Some(name) = name {
335 util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
336 } else {
337 "Open recent project".to_string()
338 };
339
340 let workspace = self.workspace.clone();
341 Button::new("project_name_trigger", name)
342 .when(!is_project_selected, |b| b.color(Color::Muted))
343 .style(ButtonStyle::Subtle)
344 .label_size(LabelSize::Small)
345 .tooltip(move |cx| {
346 Tooltip::for_action(
347 "Recent Projects",
348 &recent_projects::OpenRecent {
349 create_new_window: false,
350 },
351 cx,
352 )
353 })
354 .on_click(cx.listener(move |_, _, cx| {
355 if let Some(workspace) = workspace.upgrade() {
356 workspace.update(cx, |workspace, cx| {
357 RecentProjects::open(workspace, false, cx);
358 })
359 }
360 }))
361 }
362
363 pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
364 let entry = {
365 let mut names_and_branches =
366 self.project.read(cx).visible_worktrees(cx).map(|worktree| {
367 let worktree = worktree.read(cx);
368 worktree.root_git_entry()
369 });
370
371 names_and_branches.next().flatten()
372 };
373 let workspace = self.workspace.upgrade()?;
374 let branch_name = entry
375 .as_ref()
376 .and_then(RepositoryEntry::branch)
377 .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
378 Some(
379 Button::new("project_branch_trigger", branch_name)
380 .color(Color::Muted)
381 .style(ButtonStyle::Subtle)
382 .label_size(LabelSize::Small)
383 .tooltip(move |cx| {
384 Tooltip::with_meta(
385 "Recent Branches",
386 Some(&ToggleVcsMenu),
387 "Local branches only",
388 cx,
389 )
390 })
391 .on_click(move |_, cx| {
392 let _ = workspace.update(cx, |this, cx| {
393 BranchList::open(this, &Default::default(), cx)
394 });
395 }),
396 )
397 }
398
399 fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
400 if cx.is_window_active() {
401 ActiveCall::global(cx)
402 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
403 .detach_and_log_err(cx);
404 } else if cx.active_window().is_none() {
405 ActiveCall::global(cx)
406 .update(cx, |call, cx| call.set_location(None, cx))
407 .detach_and_log_err(cx);
408 }
409 self.workspace
410 .update(cx, |workspace, cx| {
411 workspace.update_active_view_for_followers(cx);
412 })
413 .ok();
414 }
415
416 fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
417 cx.notify();
418 }
419
420 fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
421 let active_call = ActiveCall::global(cx);
422 let project = self.project.clone();
423 active_call
424 .update(cx, |call, cx| call.share_project(project, cx))
425 .detach_and_log_err(cx);
426 }
427
428 fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
429 let active_call = ActiveCall::global(cx);
430 let project = self.project.clone();
431 active_call
432 .update(cx, |call, cx| call.unshare_project(project, cx))
433 .log_err();
434 }
435
436 fn render_connection_status(
437 &self,
438 status: &client::Status,
439 cx: &mut ViewContext<Self>,
440 ) -> Option<AnyElement> {
441 match status {
442 client::Status::ConnectionError
443 | client::Status::ConnectionLost
444 | client::Status::Reauthenticating { .. }
445 | client::Status::Reconnecting { .. }
446 | client::Status::ReconnectionError { .. } => Some(
447 div()
448 .id("disconnected")
449 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
450 .tooltip(|cx| Tooltip::text("Disconnected", cx))
451 .into_any_element(),
452 ),
453 client::Status::UpgradeRequired => {
454 let auto_updater = auto_update::AutoUpdater::get(cx);
455 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
456 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
457 Some(AutoUpdateStatus::Installing)
458 | Some(AutoUpdateStatus::Downloading)
459 | Some(AutoUpdateStatus::Checking) => "Updating...",
460 Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
461 "Please update Zed to Collaborate"
462 }
463 };
464
465 Some(
466 Button::new("connection-status", label)
467 .label_size(LabelSize::Small)
468 .on_click(|_, cx| {
469 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
470 if auto_updater.read(cx).status().is_updated() {
471 workspace::reload(&Default::default(), cx);
472 return;
473 }
474 }
475 auto_update::check(&Default::default(), cx);
476 })
477 .into_any_element(),
478 )
479 }
480 _ => None,
481 }
482 }
483
484 pub fn render_sign_in_button(&mut self, _: &mut ViewContext<Self>) -> Button {
485 let client = self.client.clone();
486 Button::new("sign_in", "Sign in")
487 .label_size(LabelSize::Small)
488 .on_click(move |_, cx| {
489 let client = client.clone();
490 cx.spawn(move |mut cx| async move {
491 client
492 .authenticate_and_connect(true, &cx)
493 .await
494 .notify_async_err(&mut cx);
495 })
496 .detach();
497 })
498 }
499
500 pub fn render_user_menu_button(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
501 if let Some(user) = self.user_store.read(cx).current_user() {
502 PopoverMenu::new("user-menu")
503 .menu(|cx| {
504 ContextMenu::build(cx, |menu, _| {
505 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
506 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
507 .action("Themes…", theme_selector::Toggle::default().boxed_clone())
508 .action("Extensions", extensions_ui::Extensions.boxed_clone())
509 .separator()
510 .action("Sign Out", client::SignOut.boxed_clone())
511 })
512 .into()
513 })
514 .trigger(
515 ButtonLike::new("user-menu")
516 .child(
517 h_flex()
518 .gap_0p5()
519 .child(Avatar::new(user.avatar_uri.clone()))
520 .child(
521 Icon::new(IconName::ChevronDown)
522 .size(IconSize::Small)
523 .color(Color::Muted),
524 ),
525 )
526 .style(ButtonStyle::Subtle)
527 .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
528 )
529 .anchor(gpui::AnchorCorner::TopRight)
530 } else {
531 PopoverMenu::new("user-menu")
532 .menu(|cx| {
533 ContextMenu::build(cx, |menu, _| {
534 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
535 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
536 .action("Themes…", theme_selector::Toggle::default().boxed_clone())
537 .action("Extensions", extensions_ui::Extensions.boxed_clone())
538 })
539 .into()
540 })
541 .trigger(
542 ButtonLike::new("user-menu")
543 .child(
544 h_flex().gap_0p5().child(
545 Icon::new(IconName::ChevronDown)
546 .size(IconSize::Small)
547 .color(Color::Muted),
548 ),
549 )
550 .style(ButtonStyle::Subtle)
551 .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
552 )
553 }
554 }
555}
556
557impl InteractiveElement for TitleBar {
558 fn interactivity(&mut self) -> &mut Interactivity {
559 self.content.interactivity()
560 }
561}
562
563impl StatefulInteractiveElement for TitleBar {}
564
565impl ParentElement for TitleBar {
566 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
567 self.children.extend(elements)
568 }
569}