title_bar: Improve design for call controls (#58691)

For the longest time, we've had a labeled "Share" button on the title
bar whenever you're in a call. However, it was extremely easy to click
that button thinking you'd be sharing _your screen_ instead of your
project. At the end of the day, there wasn't any specific cue that'd
allow you to know what you'd be sharing. This is why people just assumed
you'd share the screen, which is arguably way more common than sharing
the project. We've seen users bump into this confusion countless times,
and this also even happens to us (the whole Zed staff) weekly whenever
we're in meetings.

So, this PR changes that button to be an icon button instead, using a
folder icon to better communicate it means sharing the project. I also
took advantage of the opportunity to change the approach we use for
adding these buttons, using `when` instead of `children.push`; I think
the former is just easier to understand, allowing to more easily picture
the UI in your (my) head. Lastly, also improved what we display in the
tooltip for the project share button, by showing the names of all
projects you'll be sharing, as well as the tooltip that displayed call
diagnostics.

| Before | After |
|--------|--------|
| <img width="778" height="360" alt="Screenshot 2026-06-05 at 3  48
2@2x"
src="https://github.com/user-attachments/assets/363593fb-79f8-42e9-83b9-345bc731846f"
/> | <img width="778" height="360" alt="Screenshot 2026-06-05 at 3  48
3@2x"
src="https://github.com/user-attachments/assets/32afc412-a1fb-45a4-a19e-74e4923c0c8b"
/> |

Release Notes:

- Improved call controls design, making it clearer what button shares
_the project_ vs. _the screen_.
This commit is contained in:
Danilo Leal authored and GitHub committed 2026-06-05 19:30:47 +00:00
1 parent 918e68017f
commit f816f346e4
5 files changed
+294 -224

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.99994 11.3581V7.54178" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.90948 8.98791L7.99999 7.07844L6.09053 8.98791" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.1715 13.387C13.5144 13.387 13.8432 13.2535 14.0858 13.0157C14.3281 12.7781 14.4644 12.4557 14.4644 12.1195V5.78184C14.4644 5.44567 14.3281 5.12328 14.0858 4.88557C13.8432 4.64787 13.5144 4.51432 13.1715 4.51432H8.06464C7.84841 4.5164 7.63512 4.46528 7.44426 4.36563C7.2534 4.26598 7.09108 4.121 6.97217 3.94394L6.44855 3.18342C6.33083 3.00817 6.17058 2.86431 5.98215 2.76476C5.79373 2.6652 5.58304 2.61308 5.36901 2.61304H2.82852C2.48562 2.61304 2.15677 2.74658 1.91433 2.98428C1.67186 3.22199 1.53564 3.54439 1.53564 3.88056V12.1195C1.53564 12.4557 1.67186 12.7781 1.91433 13.0157C2.15677 13.2535 2.48562 13.387 2.82852 13.387H13.1715Z" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.99994 11.3581V7.54178" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.90948 8.98791L7.99999 7.07844L6.09053 8.98791" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.1715 13.387C13.5144 13.387 13.8432 13.2535 14.0858 13.0157C14.3281 12.7781 14.4644 12.4557 14.4644 12.1195V5.78184C14.4644 5.44567 14.3281 5.12328 14.0858 4.88557C13.8432 4.64787 13.5144 4.51432 13.1715 4.51432H8.06464C7.84841 4.5164 7.63512 4.46528 7.44426 4.36563C7.2534 4.26598 7.09108 4.121 6.97217 3.94394L6.44855 3.18342C6.33083 3.00817 6.17058 2.86431 5.98215 2.76476C5.79373 2.6652 5.58304 2.61308 5.36901 2.61304H2.82852C2.48562 2.61304 2.15677 2.74658 1.91433 2.98428C1.67186 3.22199 1.53564 3.54439 1.53564 3.88056V12.1195C1.53564 12.4557 1.67186 12.7781 1.91433 13.0157C2.15677 13.2535 2.48562 13.387 2.82852 13.387H13.1715Z" fill="#D9D9D9" fill-opacity="0.15" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+2
View File
@@ -140,6 +140,8 @@ pub enum IconName {
FolderOpen,
FolderOpenAdd,
FolderSearch,
FolderShare,
FolderShared,
Font,
FontSize,
FontWeight,
+281 -223
View File
@@ -5,7 +5,7 @@ use call::{ActiveCall, Room};
use channel::ChannelStore;
use client::{User, proto::PeerId};
use gpui::{
AnyElement, Hsla, IntoElement, MouseButton, Path, ScreenCaptureSource, Styled, TaskExt,
AnyElement, Empty, Hsla, IntoElement, MouseButton, Path, ScreenCaptureSource, Styled, TaskExt,
WeakEntity, canvas, point,
};
use gpui::{App, Task, Window};
@@ -18,7 +18,8 @@ use settings::{Settings as _, SettingsLocation};
use theme::ActiveTheme;
use ui::{
Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Divider, DividerColor,
Facepile, PopoverMenu, SplitButton, SplitButtonStyle, TintColor, Tooltip, prelude::*,
Facepile, KeyBinding, PopoverMenu, SplitButton, SplitButtonStyle, TintColor, Tooltip,
prelude::*,
};
use util::rel_path::RelPath;
use workspace::{ParticipantLocation, notifications::DetachAndPromptErr};
@@ -336,9 +337,9 @@ impl TitleBar {
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<AnyElement> {
) -> AnyElement {
let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else {
return Vec::new();
return Empty.into_any_element();
};
let is_connecting_to_project = self
@@ -372,8 +373,6 @@ impl TitleBar {
.channel_id()
.and_then(|channel_id| channel_store.read(cx).channel_for_id(channel_id).cloned());
let mut children = Vec::new();
let effective_quality = stats.effective_quality.unwrap_or(ConnectionQuality::Lost);
let (signal_icon, signal_color, quality_label) = match effective_quality {
ConnectionQuality::Excellent => {
@@ -386,246 +385,305 @@ impl TitleBar {
let quality_label: SharedString = quality_label.into();
children.push(
h_flex()
.gap_1()
.child(
IconButton::new("leave-call", IconName::Exit)
.style(ButtonStyle::Subtle)
.tooltip(Tooltip::text("Leave Call"))
.icon_size(IconSize::Small)
.on_click(move |_, _window, cx| {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.detach_and_log_err(cx);
}),
)
.child(Divider::vertical().color(DividerColor::Border))
.into_any_element(),
);
children.push(
IconButton::new("call-quality", signal_icon)
.icon_size(IconSize::Small)
.when_some(signal_color, |button, color| button.icon_color(color))
.tooltip(move |_window, cx| {
let quality_label = quality_label.clone();
let latency = format_stat(stats.latency_ms, |v| format!("{:.0}ms", v));
let jitter = format_stat(stats.jitter_ms, |v| format!("{:.0}ms", v));
let packet_loss = format_stat(stats.packet_loss_pct, |v| format!("{:.1}%", v));
let input_lag =
format_stat(stats.input_lag.map(|d| d.as_secs_f64() * 1000.0), |v| {
format!("{:.1}ms", v)
});
Tooltip::with_meta(
format!("Connection: {quality_label}"),
Some(&ShowCallStats),
format!(
"Latency: {latency} · Jitter: {jitter} · Loss: {packet_loss} · Input lag: {input_lag}",
),
cx,
h_flex()
.gap_1()
.pr_1p5()
.child(
h_flex()
.gap_1()
.child(
IconButton::new("leave-call", IconName::Exit)
.tooltip(Tooltip::text("Leave Call"))
.icon_size(IconSize::Small)
.on_click(move |_, _window, cx| {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.detach_and_log_err(cx);
}),
)
})
.on_click(move |_, window, cx| {
window.dispatch_action(Box::new(ShowCallStats), cx);
})
.into_any_element(),
);
.child(Divider::vertical().color(DividerColor::Border)),
)
.child(
IconButton::new("call-quality", signal_icon)
.icon_size(IconSize::Small)
.when_some(signal_color, |button, color| button.icon_color(color))
.tooltip(Tooltip::element(move |window, cx| {
let quality_label = quality_label.clone();
let latency = format_stat(stats.latency_ms, |v| format!("{:.0}ms", v));
let jitter = format_stat(stats.jitter_ms, |v| format!("{:.0}ms", v));
let packet_loss =
format_stat(stats.packet_loss_pct, |v| format!("{:.1}%", v));
let input_lag =
format_stat(stats.input_lag.map(|d| d.as_secs_f64() * 1000.0), |v| {
format!("{:.1}ms", v)
});
if is_local && can_share_projects && !is_connecting_to_project {
let is_sharing_disabled = channel.is_some_and(|channel| match channel.visibility {
proto::ChannelVisibility::Public => project.visible_worktrees(cx).any(|worktree| {
let worktree_id = worktree.read(cx).id();
let key_binding = KeyBinding::for_action(&ShowCallStats, cx);
let has_key_binding = key_binding.has_binding(window);
let settings_location = Some(SettingsLocation {
worktree_id,
path: RelPath::empty(),
});
let stat_row = |label: &'static str, value: String| {
h_flex()
.justify_between()
.gap_4()
.child(Label::new(label).size(LabelSize::Small).color(Color::Muted))
.child(Label::new(value).size(LabelSize::Small))
};
WorktreeSettings::get(settings_location, cx).prevent_sharing_in_public_channels
}),
proto::ChannelVisibility::Members => false,
});
children.push(
Button::new(
"toggle_sharing",
if is_shared { "Unshare" } else { "Share" },
v_flex()
.child(
h_flex()
.gap_4()
.justify_between()
.child(Label::new(format!("Connection: {quality_label}")))
.when(has_key_binding, |this| this.child(key_binding)),
)
.child(
v_flex()
.gap_0p5()
.child(stat_row("Latency", latency))
.child(stat_row("Jitter", jitter))
.child(stat_row("Packet loss", packet_loss))
.child(stat_row("Input lag", input_lag)),
)
.into_any_element()
}))
.on_click(move |_, window, cx| {
window.dispatch_action(Box::new(ShowCallStats), cx);
}),
)
.when(can_use_microphone, |this| {
this.child(
IconButton::new(
"mute-microphone",
if is_muted {
IconName::MicMute
} else {
IconName::Mic
},
)
.tooltip(move |_window, cx| {
if is_muted {
if is_deafened {
Tooltip::with_meta(
"Unmute Microphone",
None,
"Audio will be unmuted",
cx,
)
} else {
Tooltip::simple("Unmute Microphone", cx)
}
} else {
Tooltip::simple("Mute Microphone", cx)
}
})
.icon_size(IconSize::Small)
.toggle_state(is_muted)
.selected_style(ButtonStyle::Tinted(TintColor::Error))
.on_click(move |_, _window, cx| toggle_mute(cx)),
)
.tooltip(Tooltip::text(if is_shared {
"Stop sharing project with call participants"
} else {
"Share project with call participants"
}))
.style(ButtonStyle::Subtle)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.toggle_state(is_shared)
.label_size(LabelSize::Small)
.when(is_sharing_disabled, |parent| {
parent.disabled(true).tooltip(Tooltip::text(
"This project may not be shared in a public channel.",
))
})
.on_click(cx.listener(move |this, _, window, cx| {
if is_shared {
this.unshare_project(window, cx);
} else {
this.share_project(cx);
}
}))
.into_any_element(),
);
}
if can_use_microphone {
children.push(
})
.child(
IconButton::new(
"mute-microphone",
if is_muted {
IconName::MicMute
"mute-sound",
if is_deafened {
IconName::AudioOff
} else {
IconName::Mic
IconName::AudioOn
},
)
.selected_style(ButtonStyle::Tinted(TintColor::Error))
.icon_size(IconSize::Small)
.toggle_state(is_deafened)
.tooltip(move |_window, cx| {
if is_muted {
if is_deafened {
Tooltip::with_meta(
"Unmute Microphone",
None,
"Audio will be unmuted",
cx,
)
if is_deafened {
let label = "Unmute Audio";
if !muted_by_user {
Tooltip::with_meta(label, None, "Microphone will be unmuted", cx)
} else {
Tooltip::simple("Unmute Microphone", cx)
Tooltip::simple(label, cx)
}
} else {
Tooltip::simple("Mute Microphone", cx)
let label = "Mute Audio";
if !muted_by_user {
Tooltip::with_meta(label, None, "Microphone will be muted", cx)
} else {
Tooltip::simple(label, cx)
}
}
})
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.toggle_state(is_muted)
.selected_style(ButtonStyle::Tinted(TintColor::Error))
.on_click(move |_, _window, cx| toggle_mute(cx))
.into_any_element(),
);
}
.on_click(move |_, _, cx| toggle_deafen(cx)),
)
.when(
is_local && can_share_projects && !is_connecting_to_project,
|this| {
let is_sharing_disabled =
channel.is_some_and(|channel| match channel.visibility {
proto::ChannelVisibility::Public => {
project.visible_worktrees(cx).any(|worktree| {
let worktree_id = worktree.read(cx).id();
children.push(
IconButton::new(
"mute-sound",
if is_deafened {
IconName::AudioOff
} else {
IconName::AudioOn
let settings_location = Some(SettingsLocation {
worktree_id,
path: RelPath::empty(),
});
WorktreeSettings::get(settings_location, cx)
.prevent_sharing_in_public_channels
})
}
proto::ChannelVisibility::Members => false,
});
let icon = if is_shared {
IconName::FolderShared
} else {
IconName::FolderShare
};
let folder_names = project
.visible_worktrees(cx)
.filter_map(|worktree| {
worktree
.read(cx)
.root_name()
.file_name()
.map(|name| name.to_string())
})
.collect::<Vec<_>>();
let folder_list = folder_names.join(", ");
let unshare_meta: SharedString = if folder_list.is_empty() {
"Stop sharing project with call participants".into()
} else {
format!("Stop sharing {folder_list} with call participants").into()
};
let share_meta: SharedString = if folder_list.is_empty() {
"Share active project with call participants".into()
} else {
format!("Share {folder_list} with call participants").into()
};
this.child(
IconButton::new("toggle_sharing", icon)
.icon_size(IconSize::Small)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.toggle_state(is_shared)
.map(|this| {
if is_shared {
this.tooltip(move |_, cx| {
Tooltip::with_meta(
"Unshare Project",
None,
unshare_meta.clone(),
cx,
)
})
.on_click(cx.listener(
move |this, _, window, cx| {
this.unshare_project(window, cx);
},
))
} else if is_sharing_disabled {
this.disabled(true).tooltip(Tooltip::text(
"This project may not be shared in a public channel.",
))
} else {
this.tooltip(move |_, cx| {
Tooltip::with_meta(
"Share Project",
None,
share_meta.clone(),
cx,
)
})
.on_click(cx.listener(
move |this, _, _, cx| {
this.share_project(cx);
},
))
}
}),
)
},
)
.style(ButtonStyle::Subtle)
.selected_style(ButtonStyle::Tinted(TintColor::Error))
.icon_size(IconSize::Small)
.toggle_state(is_deafened)
.tooltip(move |_window, cx| {
if is_deafened {
let label = "Unmute Audio";
.when(can_use_microphone && screen_sharing_supported, |parent| {
#[cfg(target_os = "linux")]
let is_wayland = gpui::guess_compositor() == "Wayland";
#[cfg(not(target_os = "linux"))]
let is_wayland = false;
if !muted_by_user {
Tooltip::with_meta(label, None, "Microphone will be unmuted", cx)
let trigger = IconButton::new("screen-share", IconName::Screen)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.toggle_state(is_screen_sharing)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.tooltip(Tooltip::text(if is_screen_sharing {
"Stop Sharing Screen"
} else {
Tooltip::simple(label, cx)
}
"Share Screen"
}))
.on_click(move |_, window, cx| {
let should_share = ActiveCall::global(cx)
.read(cx)
.room()
.is_some_and(|room| !room.read(cx).is_sharing_screen());
#[cfg(target_os = "linux")]
{
if is_wayland
&& let Some(room) = ActiveCall::global(cx).read(cx).room().cloned()
{
let task = room.update(cx, |room, cx| {
if should_share {
room.share_screen_wayland(cx)
} else {
room.unshare_screen(true, cx)
.map(|()| Task::ready(Ok(())))
.unwrap_or_else(|e| Task::ready(Err(e)))
}
});
task.detach_and_prompt_err(
"Sharing Screen Failed",
window,
cx,
|e, _, _| Some(format!("{e:?}")),
);
}
}
if !is_wayland {
window
.spawn(cx, async move |cx| {
let screen = if should_share {
cx.update(|_, cx| pick_default_screen(cx))?.await
} else {
Ok(None)
};
cx.update(|window, cx| {
toggle_screen_sharing(screen, window, cx)
})?;
Result::<_, anyhow::Error>::Ok(())
})
.detach();
}
});
if is_wayland {
parent.child(trigger)
} else {
let label = "Mute Audio";
if !muted_by_user {
Tooltip::with_meta(label, None, "Microphone will be muted", cx)
} else {
Tooltip::simple(label, cx)
}
parent.child(
SplitButton::new(
trigger.render(window, cx),
self.render_screen_list().into_any_element(),
)
.style(SplitButtonStyle::Transparent),
)
}
})
.on_click(move |_, _, cx| toggle_deafen(cx))
.into_any_element(),
);
if can_use_microphone && screen_sharing_supported {
#[cfg(target_os = "linux")]
let is_wayland = gpui::guess_compositor() == "Wayland";
#[cfg(not(target_os = "linux"))]
let is_wayland = false;
let trigger = IconButton::new("screen-share", IconName::Screen)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.toggle_state(is_screen_sharing)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.tooltip(Tooltip::text(if is_screen_sharing {
"Stop Sharing Screen"
} else {
"Share Screen"
}))
.on_click(move |_, window, cx| {
let should_share = ActiveCall::global(cx)
.read(cx)
.room()
.is_some_and(|room| !room.read(cx).is_sharing_screen());
#[cfg(target_os = "linux")]
{
if is_wayland
&& let Some(room) = ActiveCall::global(cx).read(cx).room().cloned()
{
let task = room.update(cx, |room, cx| {
if should_share {
room.share_screen_wayland(cx)
} else {
room.unshare_screen(true, cx)
.map(|()| Task::ready(Ok(())))
.unwrap_or_else(|e| Task::ready(Err(e)))
}
});
task.detach_and_prompt_err(
"Sharing Screen Failed",
window,
cx,
|e, _, _| Some(format!("{e:?}")),
);
}
}
if !is_wayland {
window
.spawn(cx, async move |cx| {
let screen = if should_share {
cx.update(|_, cx| pick_default_screen(cx))?.await
} else {
Ok(None)
};
cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?;
Result::<_, anyhow::Error>::Ok(())
})
.detach();
}
});
if is_wayland {
children.push(trigger.into_any_element());
} else {
children.push(
SplitButton::new(
trigger.render(window, cx),
self.render_screen_list().into_any_element(),
)
.style(SplitButtonStyle::Transparent)
.into_any_element(),
);
}
}
children.push(div().pr_2().into_any_element());
children
.into_any_element()
}
fn render_screen_list(&self) -> impl IntoElement {
+1 -1
View File
@@ -362,7 +362,7 @@ impl Render for TitleBar {
})
.gap_1()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.children(self.render_call_controls(window, cx))
.child(self.render_call_controls(window, cx))
.children(self.render_connection_status(status, cx))
.child(self.update_version.clone())
.when(