logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled

Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 02:22:17 +03:30
co-authored by Claude Fable 5
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
[package]
name = "title_bar"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/title_bar.rs"
doctest = false
[features]
default = []
test-support = [
"call/test-support",
"client/test-support",
"gpui/test-support",
"project/test-support",
"remote/test-support",
"util/test-support",
"workspace/test-support",
"recent_projects/test-support",
]
[dependencies]
agent_settings.workspace = true
anyhow.workspace = true
auto_update.workspace = true
fs.workspace = true
platform_title_bar.workspace = true
call.workspace = true
channel.workspace = true
chrono.workspace = true
client.workspace = true
cloud_api_types.workspace = true
db.workspace = true
git_ui.workspace = true
gpui = { workspace = true, features = ["screen-capture"] }
icons.workspace = true
livekit_client.workspace = true
notifications.workspace = true
project.workspace = true
recent_projects.workspace = true
remote.workspace = true
remote_connection.workspace = true
rpc.workspace = true
semver.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
smallvec.workspace = true
telemetry.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
arrayvec = "0.7.6"
[target.'cfg(windows)'.dependencies]
windows.workspace = true
[dev-dependencies]
call = { workspace = true, features = ["test-support"] }
client = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
notifications = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
release_channel.workspace = true
remote = { workspace = true, features = ["test-support"] }
rpc = { workspace = true, features = ["test-support"] }
semver.workspace = true
settings = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
+1
View File
@@ -0,0 +1 @@
../../LICENSE-GPL
+28
View File
@@ -0,0 +1,28 @@
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
fn main() {
println!("cargo::rustc-check-cfg=cfg(macos_sdk_26)");
#[cfg(target_os = "macos")]
{
use std::process::Command;
let output = Command::new("xcrun")
.args(["--sdk", "macosx", "--show-sdk-version"])
.output()
.unwrap();
let sdk_version = String::from_utf8(output.stdout).unwrap();
let major_version: Option<u32> = sdk_version
.trim()
.split('.')
.next()
.and_then(|v| v.parse().ok());
if let Some(major) = major_version
&& major >= 26
{
println!("cargo:rustc-cfg=macos_sdk_26");
}
}
}
+327
View File
@@ -0,0 +1,327 @@
use gpui::{Action, Entity, OwnedMenu, OwnedMenuItem, actions};
use settings::Settings;
use schemars::JsonSchema;
use serde::Deserialize;
use smallvec::SmallVec;
use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use crate::title_bar_settings::TitleBarSettings;
actions!(
app_menu,
[
/// Activates the menu on the right in the client-side application menu.
///
/// Does not apply to platform menu bars (e.g. on macOS).
ActivateMenuRight,
/// Activates the menu on the left in the client-side application menu.
///
/// Does not apply to platform menu bars (e.g. on macOS).
ActivateMenuLeft
]
);
/// Opens the named menu in the client-side application menu.
///
/// Does not apply to platform menu bars (e.g. on macOS).
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Default, Action)]
#[action(namespace = app_menu)]
pub struct OpenApplicationMenu(String);
#[cfg(not(target_os = "macos"))]
pub enum ActivateDirection {
Left,
Right,
}
#[derive(Clone)]
struct MenuEntry {
menu: OwnedMenu,
handle: PopoverMenuHandle<ContextMenu>,
}
pub struct ApplicationMenu {
entries: SmallVec<[MenuEntry; 8]>,
pending_menu_open: Option<String>,
}
impl ApplicationMenu {
pub fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
let menus = cx.get_menus().unwrap_or_default();
Self {
entries: menus
.into_iter()
.map(|menu| MenuEntry {
menu,
handle: PopoverMenuHandle::default(),
})
.collect(),
pending_menu_open: None,
}
}
fn sanitize_menu_items(items: Vec<OwnedMenuItem>) -> Vec<OwnedMenuItem> {
let mut cleaned = Vec::new();
let mut last_was_separator = false;
for item in items {
match item {
OwnedMenuItem::Separator => {
if !last_was_separator {
cleaned.push(item);
last_was_separator = true;
}
}
OwnedMenuItem::Submenu(submenu) => {
// Skip empty submenus
if !submenu.items.is_empty() {
cleaned.push(OwnedMenuItem::Submenu(submenu));
last_was_separator = false;
}
}
item => {
cleaned.push(item);
last_was_separator = false;
}
}
}
// Remove trailing separator
if let Some(OwnedMenuItem::Separator) = cleaned.last() {
cleaned.pop();
}
cleaned
}
fn build_menu_from_items(
entry: MenuEntry,
window: &mut Window,
cx: &mut App,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |menu, window, cx| {
// Grab current focus handle so menu can shown items in context with the focused element
let menu = menu.when_some(window.focused(cx), |menu, focused| menu.context(focused));
let sanitized_items = Self::sanitize_menu_items(entry.menu.items);
sanitized_items
.into_iter()
.fold(menu, |menu, item| match item {
OwnedMenuItem::Separator => menu.separator(),
OwnedMenuItem::Action {
name,
action,
checked,
disabled,
..
} => menu.action_checked_with_disabled(name, action, checked, disabled),
OwnedMenuItem::Submenu(submenu) => {
submenu
.items
.into_iter()
.fold(menu, |menu, item| match item {
OwnedMenuItem::Separator => menu.separator(),
OwnedMenuItem::Action {
name,
action,
checked,
disabled,
..
} => menu
.action_checked_with_disabled(name, action, checked, disabled),
OwnedMenuItem::Submenu(_) => menu,
OwnedMenuItem::SystemMenu(_) => {
// A system menu doesn't make sense in this context, so ignore it
menu
}
})
}
OwnedMenuItem::SystemMenu(_) => {
// A system menu doesn't make sense in this context, so ignore it
menu
}
})
})
}
fn render_application_menu(&self, entry: &MenuEntry) -> impl IntoElement {
let handle = entry.handle.clone();
let menu_name = entry.menu.name.clone();
let entry = entry.clone();
// Application menu must have same ids as first menu item in standard menu
div()
.id(format!("{}-menu-item", menu_name))
.occlude()
.child(
PopoverMenu::new(format!("{}-menu-popover", menu_name))
.menu(move |window, cx| {
Self::build_menu_from_items(entry.clone(), window, cx).into()
})
.trigger_with_tooltip(
IconButton::new(
SharedString::from(format!("{}-menu-trigger", menu_name)),
ui::IconName::Menu,
)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small),
Tooltip::text("Open Application Menu"),
)
.with_handle(handle),
)
}
fn render_standard_menu(&self, entry: &MenuEntry) -> impl IntoElement {
let current_handle = entry.handle.clone();
let menu_name = entry.menu.name.clone();
let entry = entry.clone();
let all_handles: Vec<_> = self
.entries
.iter()
.map(|entry| entry.handle.clone())
.collect();
div()
.id(format!("{}-menu-item", menu_name))
.occlude()
.child(
PopoverMenu::new(format!("{}-menu-popover", menu_name))
.menu(move |window, cx| {
Self::build_menu_from_items(entry.clone(), window, cx).into()
})
.trigger(
Button::new(
SharedString::from(format!("{}-menu-trigger", menu_name)),
menu_name,
)
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small),
)
.with_handle(current_handle.clone()),
)
.on_hover(move |hover_enter, window, cx| {
if *hover_enter && !current_handle.is_deployed() {
all_handles.iter().for_each(|h| h.hide(cx));
// We need to defer this so that this menu handle can take focus from the previous menu
let handle = current_handle.clone();
window.defer(cx, move |window, cx| handle.show(window, cx));
}
})
}
#[cfg(not(target_os = "macos"))]
pub fn open_menu(
&mut self,
action: &OpenApplicationMenu,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
self.pending_menu_open = Some(action.0.clone());
}
#[cfg(not(target_os = "macos"))]
pub fn navigate_menus_in_direction(
&mut self,
direction: ActivateDirection,
window: &mut Window,
cx: &mut Context<Self>,
) {
let current_index = self
.entries
.iter()
.position(|entry| entry.handle.is_deployed());
let Some(current_index) = current_index else {
return;
};
let next_index = match direction {
ActivateDirection::Left => {
if current_index == 0 {
self.entries.len() - 1
} else {
current_index - 1
}
}
ActivateDirection::Right => {
if current_index == self.entries.len() - 1 {
0
} else {
current_index + 1
}
}
};
self.entries[current_index].handle.hide(cx);
// We need to defer this so that this menu handle can take focus from the previous menu
let next_handle = self.entries[next_index].handle.clone();
cx.defer_in(window, move |_, window, cx| next_handle.show(window, cx));
}
pub fn all_menus_shown(&self, cx: &mut Context<Self>) -> bool {
show_menus(cx)
|| self.entries.iter().any(|entry| entry.handle.is_deployed())
|| self.pending_menu_open.is_some()
}
}
pub(crate) fn show_menus(cx: &mut App) -> bool {
TitleBarSettings::get_global(cx).show_menus
&& (cfg!(not(target_os = "macos")) || option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some())
}
impl Render for ApplicationMenu {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let all_menus_shown = self.all_menus_shown(cx);
if let Some(pending_menu_open) = self.pending_menu_open.take()
&& let Some(entry) = self
.entries
.iter()
.find(|entry| entry.menu.name == pending_menu_open && !entry.handle.is_deployed())
{
let handle_to_show = entry.handle.clone();
let handles_to_hide: Vec<_> = self
.entries
.iter()
.filter(|e| e.menu.name != pending_menu_open && e.handle.is_deployed())
.map(|e| e.handle.clone())
.collect();
if handles_to_hide.is_empty() {
// We need to wait for the next frame to show all menus first,
// before we can handle show/hide operations
window.on_next_frame(move |window, cx| {
handles_to_hide.iter().for_each(|handle| handle.hide(cx));
window.defer(cx, move |window, cx| handle_to_show.show(window, cx));
});
} else {
// Since menus are already shown, we can directly handle show/hide operations
handles_to_hide.iter().for_each(|handle| handle.hide(cx));
cx.defer_in(window, move |_, window, cx| handle_to_show.show(window, cx));
}
}
div()
.key_context("ApplicationMenu")
.flex()
.flex_row()
.gap_x_1()
.when(!all_menus_shown && !self.entries.is_empty(), |this| {
this.child(self.render_application_menu(&self.entries[0]))
})
.when(all_menus_shown, |this| {
this.children(
self.entries
.iter()
.map(|entry| self.render_standard_menu(entry)),
)
})
}
}
+722
View File
@@ -0,0 +1,722 @@
use std::rc::Rc;
use std::sync::Arc;
use call::{ActiveCall, Room};
use channel::ChannelStore;
use client::{User, proto::PeerId};
use gpui::{
AnyElement, Hsla, IntoElement, MouseButton, Path, ScreenCaptureSource, Styled, TaskExt,
WeakEntity, canvas, point,
};
use gpui::{App, Task, Window};
use icons::IconName;
use livekit_client::ConnectionQuality;
use project::WorktreeSettings;
use remote_connection::RemoteConnectionModal;
use rpc::proto::{self};
use settings::{Settings as _, SettingsLocation};
use theme::ActiveTheme;
use ui::{
Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Divider, DividerColor,
Facepile, PopoverMenu, SplitButton, SplitButtonStyle, TintColor, Tooltip, prelude::*,
};
use util::rel_path::RelPath;
use workspace::{ParticipantLocation, notifications::DetachAndPromptErr};
use zed_actions::ShowCallStats;
use crate::TitleBar;
fn format_stat(value: Option<f64>, format: impl Fn(f64) -> String) -> String {
match value {
Some(v) => format(v),
None => "—".to_string(),
}
}
pub fn toggle_screen_sharing(
screen: anyhow::Result<Option<Rc<dyn ScreenCaptureSource>>>,
window: &mut Window,
cx: &mut App,
) {
let call = ActiveCall::global(cx).read(cx);
let toggle_screen_sharing = match screen {
Ok(screen) => {
let Some(room) = call.room().cloned() else {
return;
};
room.update(cx, |room, cx| {
let clicked_on_currently_shared_screen =
room.shared_screen_id().is_some_and(|screen_id| {
Some(screen_id)
== screen
.as_deref()
.and_then(|s| s.metadata().ok().map(|meta| meta.id))
});
let should_unshare_current_screen = room.is_sharing_screen();
let unshared_current_screen = should_unshare_current_screen.then(|| {
telemetry::event!(
"Screen Share Disabled",
room_id = room.id(),
channel_id = room.channel_id(),
);
room.unshare_screen(clicked_on_currently_shared_screen || screen.is_none(), cx)
});
if let Some(screen) = screen {
if !should_unshare_current_screen {
telemetry::event!(
"Screen Share Enabled",
room_id = room.id(),
channel_id = room.channel_id(),
);
}
cx.spawn(async move |room, cx| {
unshared_current_screen.transpose()?;
if !clicked_on_currently_shared_screen {
room.update(cx, |room, cx| room.share_screen(screen, cx))?
.await
} else {
Ok(())
}
})
} else {
Task::ready(Ok(()))
}
})
}
Err(e) => Task::ready(Err(e)),
};
toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", window, cx, |e, _, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e)));
}
pub fn toggle_mute(cx: &mut App) {
let call = ActiveCall::global(cx).read(cx);
if let Some(room) = call.room().cloned() {
room.update(cx, |room, cx| {
let operation = if room.is_muted() {
"Microphone Enabled"
} else {
"Microphone Disabled"
};
telemetry::event!(
operation,
room_id = room.id(),
channel_id = room.channel_id(),
);
room.toggle_mute(cx)
});
}
}
pub fn toggle_deafen(cx: &mut App) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
room.update(cx, |room, cx| room.toggle_deafen(cx));
}
}
fn render_color_ribbon(color: Hsla) -> impl Element {
canvas(
move |_, _, _| {},
move |bounds, _, window, _| {
let height = bounds.size.height;
let horizontal_offset = height;
let vertical_offset = height / 2.0;
let mut path = Path::new(bounds.bottom_left());
path.curve_to(
bounds.origin + point(horizontal_offset, vertical_offset),
bounds.origin + point(px(0.0), vertical_offset),
);
path.line_to(bounds.top_right() + point(-horizontal_offset, vertical_offset));
path.curve_to(
bounds.bottom_right(),
bounds.top_right() + point(px(0.0), vertical_offset),
);
path.line_to(bounds.bottom_left());
window.paint_path(path, color);
},
)
.h_1()
.w_full()
}
impl TitleBar {
pub(crate) fn render_collaborator_list(
&self,
_: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let room = ActiveCall::global(cx).read(cx).room().cloned();
let current_user = self.user_store.read(cx).current_user();
let client = self.client.clone();
let project_id = self.project.read(cx).remote_id();
let workspace = self.workspace.upgrade();
h_flex()
.id("collaborator-list")
.w_full()
.gap_1()
.overflow_x_scroll()
.when_some(
current_user.zip(client.peer_id()).zip(room),
|this, ((current_user, peer_id), room)| {
let player_colors = cx.theme().players();
let room = room.read(cx);
let mut remote_participants =
room.remote_participants().values().collect::<Vec<_>>();
remote_participants.sort_by_key(|p| p.participant_index.0);
let current_user_face_pile = self.render_collaborator(
&current_user,
peer_id,
true,
room.is_speaking(),
room.is_muted(),
None,
room,
project_id,
&current_user,
cx,
);
this.children(current_user_face_pile.map(|face_pile| {
v_flex()
.on_mouse_down(MouseButton::Left, |_, window, _| {
window.prevent_default()
})
.child(face_pile)
.child(render_color_ribbon(player_colors.local().cursor))
}))
.children(remote_participants.iter().filter_map(|collaborator| {
let player_color =
player_colors.color_for_participant(collaborator.participant_index.0);
let is_following = workspace
.as_ref()?
.read(cx)
.is_being_followed(collaborator.peer_id);
let is_present = project_id.is_some_and(|project_id| {
collaborator.location
== ParticipantLocation::SharedProject { project_id }
});
let facepile = self.render_collaborator(
&collaborator.user,
collaborator.peer_id,
is_present,
collaborator.speaking,
collaborator.muted,
is_following.then_some(player_color.selection),
room,
project_id,
&current_user,
cx,
)?;
Some(
v_flex()
.id(("collaborator", collaborator.user.legacy_id))
.child(facepile)
.child(render_color_ribbon(player_color.cursor))
.cursor_pointer()
.on_mouse_down(MouseButton::Left, |_, window, _| {
window.prevent_default()
})
.on_click({
let peer_id = collaborator.peer_id;
cx.listener(move |this, _, window, cx| {
cx.stop_propagation();
this.workspace
.update(cx, |workspace, cx| {
if is_following {
workspace.unfollow(peer_id, window, cx);
} else {
workspace.follow(peer_id, window, cx);
}
})
.ok();
})
})
.occlude()
.tooltip({
let login = collaborator.user.github_login.clone();
Tooltip::text(format!("Follow {login}"))
}),
)
}))
},
)
}
fn render_collaborator(
&self,
user: &Arc<User>,
peer_id: PeerId,
is_present: bool,
is_speaking: bool,
is_muted: bool,
leader_selection_color: Option<Hsla>,
room: &Room,
project_id: Option<u64>,
current_user: &Arc<User>,
cx: &App,
) -> Option<Div> {
if room.role_for_user(user.legacy_id) == Some(proto::ChannelRole::Guest) {
return None;
}
const FACEPILE_LIMIT: usize = 3;
let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
let extra_count = followers.len().saturating_sub(FACEPILE_LIMIT);
Some(
div()
.m_0p5()
.p_0p5()
// When the collaborator is not followed, still draw this wrapper div, but leave
// it transparent, so that it does not shift the layout when following.
.when_some(leader_selection_color, |div, color| {
div.rounded_sm().bg(color)
})
.child(
Facepile::empty()
.child(
Avatar::new(user.avatar_uri.clone())
.grayscale(!is_present)
.border_color(if is_speaking {
cx.theme().status().info
} else {
// We draw the border in a transparent color rather to avoid
// the layout shift that would come with adding/removing the border.
gpui::transparent_black()
})
.when(is_muted, |avatar| {
avatar.indicator(
AvatarAudioStatusIndicator::new(ui::AudioStatus::Muted)
.tooltip({
let github_login = user.github_login.clone();
Tooltip::text(format!("{} is muted", github_login))
}),
)
}),
)
.children(followers.iter().take(FACEPILE_LIMIT).filter_map(
|follower_peer_id| {
let follower = room
.remote_participants()
.values()
.find_map(|p| {
(p.peer_id == *follower_peer_id).then_some(&p.user)
})
.or_else(|| {
(self.client.peer_id() == Some(*follower_peer_id))
.then_some(current_user)
})?
.clone();
Some(div().mt(-px(4.)).child(
Avatar::new(follower.avatar_uri.clone()).size(rems(0.75)),
))
},
))
.children(if extra_count > 0 {
Some(
Label::new(format!("+{extra_count}"))
.ml_1()
.into_any_element(),
)
} else {
None
}),
),
)
}
pub(crate) fn render_call_controls(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<AnyElement> {
let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else {
return Vec::new();
};
let is_connecting_to_project = self
.workspace
.update(cx, |workspace, cx| {
workspace
.active_modal::<RemoteConnectionModal>(cx)
.is_some()
})
.unwrap_or(false);
let room = room.read(cx);
let project = self.project.read(cx);
let is_local = project.is_local() || project.is_via_remote_server();
let is_shared = is_local && project.is_shared();
let is_muted = room.is_muted();
let muted_by_user = room.muted_by_user();
let is_deafened = room.is_deafened().unwrap_or(false);
let is_screen_sharing = room.is_sharing_screen();
let can_use_microphone = room.can_use_microphone();
let can_share_projects = room.can_share_projects();
let screen_sharing_supported = cx.is_screen_capture_supported();
let stats = room
.diagnostics()
.map(|d| d.read(cx).stats().clone())
.unwrap_or_default();
let channel_store = ChannelStore::global(cx);
let channel = room
.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 => {
(IconName::SignalHigh, Some(Color::Success), "Excellent")
}
ConnectionQuality::Good => (IconName::SignalHigh, None, "Good"),
ConnectionQuality::Poor => (IconName::SignalMedium, Some(Color::Warning), "Poor"),
ConnectionQuality::Lost => (IconName::SignalLow, Some(Color::Error), "Lost"),
};
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,
)
})
.on_click(move |_, window, cx| {
window.dispatch_action(Box::new(ShowCallStats), cx);
})
.into_any_element(),
);
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 settings_location = Some(SettingsLocation {
worktree_id,
path: RelPath::empty(),
});
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" },
)
.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(
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)
}
})
.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(),
);
}
children.push(
IconButton::new(
"mute-sound",
if is_deafened {
IconName::AudioOff
} else {
IconName::AudioOn
},
)
.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";
if !muted_by_user {
Tooltip::with_meta(label, None, "Microphone will be unmuted", cx)
} else {
Tooltip::simple(label, cx)
}
} else {
let label = "Mute Audio";
if !muted_by_user {
Tooltip::with_meta(label, None, "Microphone will be muted", cx)
} else {
Tooltip::simple(label, cx)
}
}
})
.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
}
fn render_screen_list(&self) -> impl IntoElement {
PopoverMenu::new("screen-share-screen-list")
.with_handle(self.screen_share_popover_handle.clone())
.trigger(
ui::ButtonLike::new_rounded_right("screen-share-screen-list-trigger")
.child(
h_flex()
.mx_neg_0p5()
.h_full()
.justify_center()
.child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
)
.toggle_state(self.screen_share_popover_handle.is_deployed()),
)
.menu(|window, cx| {
let screens = cx.screen_capture_sources();
Some(ContextMenu::build(window, cx, |context_menu, _, cx| {
cx.spawn(async move |this: WeakEntity<ContextMenu>, cx| {
let screens = screens.await??;
this.update(cx, |this, cx| {
let active_screenshare_id = ActiveCall::global(cx)
.read(cx)
.room()
.and_then(|room| room.read(cx).shared_screen_id());
for screen in screens {
let Ok(meta) = screen.metadata() else {
continue;
};
let label = meta
.label
.clone()
.unwrap_or_else(|| SharedString::from("Unknown screen"));
let resolution = SharedString::from(format!(
"{} × {}",
meta.resolution.width.0, meta.resolution.height.0
));
this.push_item(ContextMenuItem::CustomEntry {
entry_render: Box::new(move |_, _| {
h_flex()
.gap_2()
.child(
Icon::new(IconName::Screen)
.size(IconSize::XSmall)
.map(|this| {
if active_screenshare_id == Some(meta.id) {
this.color(Color::Accent)
} else {
this.color(Color::Muted)
}
}),
)
.child(Label::new(label.clone()))
.child(
Label::new(resolution.clone())
.color(Color::Muted)
.size(LabelSize::Small),
)
.into_any()
}),
selectable: true,
documentation_aside: None,
handler: Rc::new(move |_, window, cx| {
toggle_screen_sharing(Ok(Some(screen.clone())), window, cx);
}),
});
}
})
})
.detach_and_log_err(cx);
context_menu
}))
})
}
}
/// Picks the screen to share when clicking on the main screen sharing button.
fn pick_default_screen(cx: &App) -> Task<anyhow::Result<Option<Rc<dyn ScreenCaptureSource>>>> {
let source = cx.screen_capture_sources();
cx.spawn(async move |_| {
let available_sources = source.await??;
Ok(available_sources
.iter()
.find(|it| {
it.as_ref()
.metadata()
.is_ok_and(|meta| meta.is_main.unwrap_or_default())
})
.or_else(|| available_sources.first())
.cloned())
})
}
+177
View File
@@ -0,0 +1,177 @@
// This module provides infrastructure for showing onboarding banners in the title bar.
// It's currently not in use but is kept for future feature announcements.
#![allow(dead_code)]
use gpui::{Action, Entity, Global, Render, SharedString, TaskExt};
use ui::{ButtonLike, Tooltip, prelude::*};
use util::ResultExt;
/// Prompts the user to try newly released Zed's features
pub struct OnboardingBanner {
dismissed: bool,
source: String,
details: BannerDetails,
visible_when: Option<Box<dyn Fn(&mut App) -> bool>>,
}
#[derive(Clone)]
struct BannerGlobal {
entity: Entity<OnboardingBanner>,
}
impl Global for BannerGlobal {}
pub struct BannerDetails {
pub action: Box<dyn Action>,
pub icon_name: IconName,
pub label: SharedString,
pub subtitle: Option<SharedString>,
}
impl OnboardingBanner {
pub fn new(
source: &str,
icon_name: IconName,
label: impl Into<SharedString>,
subtitle: Option<SharedString>,
action: Box<dyn Action>,
cx: &mut Context<Self>,
) -> Self {
cx.set_global(BannerGlobal {
entity: cx.entity(),
});
Self {
source: source.to_string(),
details: BannerDetails {
action,
icon_name,
label: label.into(),
subtitle: subtitle.or(Some(SharedString::from("Introducing:"))),
},
visible_when: None,
dismissed: get_dismissed(source, cx),
}
}
pub fn visible_when(mut self, predicate: impl Fn(&mut App) -> bool + 'static) -> Self {
self.visible_when = Some(Box::new(predicate));
self
}
fn should_show(&self, cx: &mut App) -> bool {
!self.dismissed && self.visible_when.as_ref().map_or(true, |f| f(cx))
}
fn dismiss(&mut self, cx: &mut Context<Self>) {
persist_dismissed(&self.source, cx);
self.dismissed = true;
cx.notify();
}
}
fn dismissed_at_key(source: &str) -> String {
if source == "Git Onboarding" {
"zed_git_banner_dismissed_at".to_string()
} else {
format!(
"{}_banner_dismissed_at",
source.to_lowercase().trim().replace(" ", "_")
)
}
}
fn get_dismissed(source: &str, cx: &App) -> bool {
let dismissed_at = dismissed_at_key(source);
db::kvp::KeyValueStore::global(cx)
.read_kvp(&dismissed_at)
.log_err()
.is_some_and(|dismissed| dismissed.is_some())
}
fn persist_dismissed(source: &str, cx: &mut App) {
let dismissed_at = dismissed_at_key(source);
let kvp = db::kvp::KeyValueStore::global(cx);
cx.spawn(async move |_| {
let time = chrono::Utc::now().to_rfc3339();
kvp.write_kvp(dismissed_at, time).await
})
.detach_and_log_err(cx);
}
pub fn restore_banner(cx: &mut App) {
if let Some(banner_global) = cx.try_global::<BannerGlobal>() {
let entity = banner_global.entity.clone();
cx.defer(move |cx| {
entity.update(cx, |this, cx| {
this.dismissed = false;
cx.notify();
});
});
let source = &cx.global::<BannerGlobal>().entity.read(cx).source;
let dismissed_at = dismissed_at_key(source);
let kvp = db::kvp::KeyValueStore::global(cx);
cx.spawn(async move |_| kvp.delete_kvp(dismissed_at).await)
.detach_and_log_err(cx);
}
}
impl Render for OnboardingBanner {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if !self.should_show(cx) {
return div();
}
let border_color = cx.theme().colors().editor_foreground.opacity(0.3);
let banner = h_flex()
.rounded_sm()
.border_1()
.border_color(border_color)
.occlude()
.child(
ButtonLike::new("try-a-feature")
.child(
h_flex()
.h_full()
.gap_1()
.child(Icon::new(self.details.icon_name).size(IconSize::XSmall))
.child(
h_flex()
.gap_0p5()
.when_some(self.details.subtitle.as_ref(), |this, subtitle| {
this.child(
Label::new(subtitle)
.size(LabelSize::Small)
.color(Color::Muted),
)
})
.child(Label::new(&self.details.label).size(LabelSize::Small)),
),
)
.on_click(cx.listener(|this, _, window, cx| {
telemetry::event!("Banner Clicked", source = this.source);
this.dismiss(cx);
window.dispatch_action(this.details.action.boxed_clone(), cx)
})),
)
.child(
div().border_l_1().border_color(border_color).child(
IconButton::new("close", IconName::Close)
.icon_size(IconSize::Indicator)
.on_click(cx.listener(|this, _, _window, cx| {
telemetry::event!("Banner Dismissed", source = this.source);
this.dismiss(cx)
}))
.tooltip(|_window, cx| {
Tooltip::with_meta(
"Close Announcement Banner",
None,
"It won't show again for this feature",
cx,
)
}),
),
);
div().pr_2().child(banner)
}
}
+44
View File
@@ -0,0 +1,44 @@
use cloud_api_types::Plan;
use ui::{Chip, prelude::*};
/// A [`Chip`] that displays a [`Plan`].
#[derive(IntoElement)]
pub struct PlanChip {
plan: Plan,
}
impl PlanChip {
pub fn new(plan: Plan) -> Self {
Self { plan }
}
}
impl RenderOnce for PlanChip {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let free_chip_bg = cx
.theme()
.colors()
.editor_background
.opacity(0.5)
.blend(cx.theme().colors().text_accent.opacity(0.05));
let pro_chip_bg = cx
.theme()
.colors()
.editor_background
.opacity(0.5)
.blend(cx.theme().colors().text_accent.opacity(0.2));
let (plan_name, label_color, bg_color) = match self.plan {
Plan::ZedFree => ("Free", Color::Default, free_chip_bg),
Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg),
Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg),
Plan::ZedBusiness => ("Business", Color::Accent, pro_chip_bg),
Plan::ZedStudent => ("Student", Color::Accent, pro_chip_bg),
};
Chip::new(plan_name.to_string())
.bg_color(bg_color)
.label_color(label_color)
}
}
+3
View File
@@ -0,0 +1,3 @@
mod application_menu;
pub use application_menu::*;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
use gpui::WindowButtonLayout;
use settings::{RegisterSetting, Settings, SettingsContent};
#[derive(Copy, Clone, Debug, RegisterSetting)]
pub struct TitleBarSettings {
pub show_branch_status_icon: bool,
pub show_onboarding_banner: bool,
pub show_user_picture: bool,
pub show_branch_name: bool,
pub show_project_items: bool,
pub show_sign_in: bool,
pub show_user_menu: bool,
pub show_menus: bool,
pub button_layout: Option<WindowButtonLayout>,
}
impl Settings for TitleBarSettings {
fn from_settings(s: &SettingsContent) -> Self {
let content = s.title_bar.clone().unwrap();
TitleBarSettings {
show_branch_status_icon: content.show_branch_status_icon.unwrap(),
show_onboarding_banner: content.show_onboarding_banner.unwrap(),
show_user_picture: content.show_user_picture.unwrap(),
show_branch_name: content.show_branch_name.unwrap(),
show_project_items: content.show_project_items.unwrap(),
show_sign_in: content.show_sign_in.unwrap(),
show_user_menu: content.show_user_menu.unwrap(),
show_menus: content.show_menus.unwrap(),
button_layout: content.button_layout.unwrap_or_default().into_layout(),
}
}
}
+145
View File
@@ -0,0 +1,145 @@
use std::sync::Arc;
use anyhow::anyhow;
use auto_update::{AutoUpdateStatus, AutoUpdater, UpdateCheckType, VersionCheckType};
use gpui::{Empty, Render};
use semver::Version;
use ui::{UpdateButton, prelude::*};
pub struct UpdateVersion {
status: AutoUpdateStatus,
update_check_type: UpdateCheckType,
dismissed: bool,
}
impl UpdateVersion {
pub fn new(cx: &mut Context<Self>) -> Self {
if let Some(auto_updater) = AutoUpdater::get(cx) {
cx.observe(&auto_updater, |this, auto_update, cx| {
this.status = auto_update.read(cx).status();
this.update_check_type = auto_update.read(cx).update_check_type();
if this.status.is_updated() {
this.dismissed = false;
}
})
.detach();
Self {
status: auto_updater.read(cx).status(),
update_check_type: UpdateCheckType::Automatic,
dismissed: false,
}
} else {
Self {
status: AutoUpdateStatus::Idle,
update_check_type: UpdateCheckType::Automatic,
dismissed: false,
}
}
}
pub fn update_simulation(&mut self, cx: &mut Context<Self>) {
let next_state = match self.status {
AutoUpdateStatus::Idle => AutoUpdateStatus::Checking,
AutoUpdateStatus::Checking => AutoUpdateStatus::Downloading {
version: VersionCheckType::Semantic(Version::new(1, 99, 0)),
},
AutoUpdateStatus::Downloading { .. } => AutoUpdateStatus::Installing {
version: VersionCheckType::Semantic(Version::new(1, 99, 0)),
},
AutoUpdateStatus::Installing { .. } => AutoUpdateStatus::Updated {
version: VersionCheckType::Semantic(Version::new(1, 99, 0)),
},
AutoUpdateStatus::Updated { .. } => AutoUpdateStatus::Errored {
error: Arc::new(anyhow!("Network timeout")),
},
AutoUpdateStatus::Errored { .. } => AutoUpdateStatus::Idle,
};
self.status = next_state;
self.update_check_type = UpdateCheckType::Manual;
self.dismissed = false;
cx.notify()
}
pub fn show_update_in_menu_bar(&self) -> bool {
self.dismissed && self.status.is_updated()
}
fn version_tooltip_message(version: &VersionCheckType) -> String {
format!("Version: {}", {
match version {
VersionCheckType::Sha(sha) => format!("{}…", sha.short()),
VersionCheckType::Semantic(semantic_version) => semantic_version.to_string(),
}
})
}
}
impl Render for UpdateVersion {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.dismissed {
return Empty.into_any_element();
}
match &self.status {
AutoUpdateStatus::Checking if self.update_check_type.is_manual() => {
UpdateButton::checking().into_any_element()
}
AutoUpdateStatus::Downloading { version } => {
let tooltip = Self::version_tooltip_message(&version);
UpdateButton::downloading(tooltip).into_any_element()
}
AutoUpdateStatus::Installing { version } => {
let tooltip = Self::version_tooltip_message(&version);
UpdateButton::installing(tooltip).into_any_element()
}
AutoUpdateStatus::Updated { version } => {
let tooltip = Self::version_tooltip_message(&version);
UpdateButton::updated(tooltip)
.on_click(|_, _, cx| {
workspace::reload(cx);
})
.on_dismiss(cx.listener(|this, _, _window, cx| {
this.dismissed = true;
cx.notify()
}))
.into_any_element()
}
AutoUpdateStatus::Errored { error } => {
let error_str = error.to_string();
UpdateButton::errored(error_str)
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(workspace::OpenLog), cx);
})
.on_dismiss(cx.listener(|this, _, _window, cx| {
this.dismissed = true;
cx.notify()
}))
.into_any_element()
}
AutoUpdateStatus::Idle | AutoUpdateStatus::Checking { .. } => Empty.into_any_element(),
}
}
}
#[cfg(test)]
mod tests {
use auto_update::VersionCheckType;
use release_channel::AppCommitSha;
use semver::Version;
use super::*;
#[test]
fn test_version_tooltip_message() {
let message = UpdateVersion::version_tooltip_message(&VersionCheckType::Semantic(
Version::new(1, 0, 0),
));
assert_eq!(message, "Version: 1.0.0");
let message = UpdateVersion::version_tooltip_message(&VersionCheckType::Sha(
AppCommitSha::new("14d9a4189f058d8736339b06ff2340101eaea5af".to_string()),
));
assert_eq!(message, "Version: 14d9a41…");
}
}