Detailed changes
@@ -1580,6 +1580,28 @@ dependencies = [
"rustc-hash",
]
+[[package]]
+name = "color"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "fs",
+ "indexmap 1.9.3",
+ "itertools 0.11.0",
+ "palette",
+ "parking_lot 0.11.2",
+ "refineable",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings",
+ "story",
+ "toml 0.5.11",
+ "util",
+ "uuid 1.4.1",
+]
+
[[package]]
name = "color_quant"
version = "1.1.0"
@@ -4977,6 +4999,7 @@ dependencies = [
"approx",
"fast-srgb8",
"palette_derive",
+ "phf",
]
[[package]]
@@ -5165,6 +5188,48 @@ dependencies = [
"indexmap 2.0.0",
]
+[[package]]
+name = "phf"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
+dependencies = [
+ "phf_shared",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.37",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
+dependencies = [
+ "siphasher 0.3.11",
+]
+
[[package]]
name = "picker"
version = "0.1.0"
@@ -7074,6 +7139,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
[[package]]
name = "slab"
version = "0.4.9"
@@ -7644,7 +7715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff"
dependencies = [
"float-cmp",
- "siphasher",
+ "siphasher 0.2.3",
]
[[package]]
@@ -7775,6 +7846,7 @@ dependencies = [
"schemars",
"serde",
"serde_derive",
+ "serde_json",
"settings",
"shellexpand",
"smallvec",
@@ -7854,6 +7926,7 @@ name = "theme"
version = "0.1.0"
dependencies = [
"anyhow",
+ "color",
"fs",
"gpui",
"indexmap 1.9.3",
@@ -8857,7 +8930,7 @@ dependencies = [
"roxmltree",
"rustybuzz",
"simplecss",
- "siphasher",
+ "siphasher 0.2.3",
"svgtypes",
"ttf-parser 0.12.3",
"unicode-bidi",
@@ -9649,6 +9722,7 @@ dependencies = [
"client",
"collab_ui",
"collections",
+ "color",
"command_palette",
"copilot",
"copilot_ui",
@@ -402,7 +402,7 @@
"cmd-r": "workspace::ToggleRightDock",
"cmd-j": "workspace::ToggleBottomDock",
"alt-cmd-y": "workspace::CloseAllDocks",
- "cmd-shift-f": "workspace::DeploySearch",
+ "cmd-shift-f": "pane::DeploySearch",
"cmd-k cmd-t": "theme_selector::Toggle",
"cmd-k cmd-s": "zed::OpenKeymap",
"cmd-t": "project_symbols::Toggle",
@@ -99,7 +99,7 @@
"ctrl-i": "pane::GoForward",
"ctrl-]": "editor::GoToDefinition",
"escape": ["vim::SwitchMode", "Normal"],
- "ctrl+[": ["vim::SwitchMode", "Normal"],
+ "ctrl-[": ["vim::SwitchMode", "Normal"],
"v": "vim::ToggleVisual",
"shift-v": "vim::ToggleVisualLine",
"ctrl-v": "vim::ToggleVisualBlock",
@@ -288,7 +288,7 @@
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": {
"escape": "editor::Cancel",
- "ctrl+[": "editor::Cancel"
+ "ctrl-[": "editor::Cancel"
}
},
{
@@ -441,7 +441,7 @@
"r": ["vim::PushOperator", "Replace"],
"ctrl-c": ["vim::SwitchMode", "Normal"],
"escape": ["vim::SwitchMode", "Normal"],
- "ctrl+[": ["vim::SwitchMode", "Normal"],
+ "ctrl-[": ["vim::SwitchMode", "Normal"],
">": "editor::Indent",
"<": "editor::Outdent",
"i": [
@@ -481,7 +481,7 @@
"tab": "vim::Tab",
"enter": "vim::Enter",
"escape": ["vim::SwitchMode", "Normal"],
- "ctrl+[": ["vim::SwitchMode", "Normal"]
+ "ctrl-[": ["vim::SwitchMode", "Normal"]
}
},
{
@@ -143,10 +143,10 @@ const MAX_QUEUE_LEN: usize = 5;
const MAX_QUEUE_LEN: usize = 50;
#[cfg(debug_assertions)]
-const FLUSH_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
+const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
#[cfg(not(debug_assertions))]
-const FLUSH_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(60 * 5);
+const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
impl Telemetry {
pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
@@ -457,6 +457,15 @@ impl Telemetry {
return;
}
+ if state.flush_events_task.is_none() {
+ let this = self.clone();
+ let executor = self.executor.clone();
+ state.flush_events_task = Some(self.executor.spawn(async move {
+ executor.timer(FLUSH_INTERVAL).await;
+ this.flush_events();
+ }));
+ }
+
let signed_in = state.metrics_id.is_some();
state.events_queue.push(EventWrapper { signed_in, event });
@@ -464,13 +473,6 @@ impl Telemetry {
if state.events_queue.len() >= MAX_QUEUE_LEN {
drop(state);
self.flush_events();
- } else {
- let this = self.clone();
- let executor = self.executor.clone();
- state.flush_events_task = Some(self.executor.spawn(async move {
- executor.timer(FLUSH_DEBOUNCE_INTERVAL).await;
- this.flush_events();
- }));
}
}
}
@@ -1,4 +1,4 @@
-use crate::{collab_panel, is_channels_feature_enabled, ChatPanelSettings};
+use crate::{collab_panel, ChatPanelSettings};
use anyhow::Result;
use call::{room, ActiveCall};
use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
@@ -630,9 +630,6 @@ impl Panel for ChatPanel {
self.active = active;
if active {
self.acknowledge_last_message(cx);
- if !is_channels_feature_enabled(cx) {
- cx.emit(PanelEvent::Close);
- }
}
}
@@ -641,10 +638,6 @@ impl Panel for ChatPanel {
}
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
- if !is_channels_feature_enabled(cx) {
- return None;
- }
-
Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
}
@@ -12,7 +12,6 @@ use client::{Client, Contact, User, UserStore};
use contact_finder::ContactFinder;
use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorElement, EditorStyle};
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, canvas, div, fill, list, overlay, point, prelude::*, px, AnyElement, AppContext,
@@ -31,8 +30,8 @@ use smallvec::SmallVec;
use std::{mem, sync::Arc};
use theme::{ActiveTheme, ThemeSettings};
use ui::{
- prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconName, IconSize, Label,
- ListHeader, ListItem, Tooltip,
+ prelude::*, Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Icon, IconButton,
+ IconName, IconSize, Label, ListHeader, ListItem, Tooltip,
};
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
@@ -265,10 +264,6 @@ impl CollabPanel {
}));
this.subscriptions
.push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
- this.subscriptions
- .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
- this.update_entries(true, cx)
- }));
this.subscriptions.push(cx.subscribe(
&this.channel_store,
|this, _channel_store, e, cx| match e {
@@ -504,115 +499,118 @@ impl CollabPanel {
let mut request_entries = Vec::new();
- if cx.has_flag::<ChannelsAlpha>() {
- self.entries.push(ListEntry::Header(Section::Channels));
+ self.entries.push(ListEntry::Header(Section::Channels));
- if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(channel_store.ordered_channels().enumerate().map(
- |(ix, (_, channel))| StringMatchCandidate {
+ if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
+ self.match_candidates.clear();
+ self.match_candidates
+ .extend(
+ channel_store
+ .ordered_channels()
+ .enumerate()
+ .map(|(ix, (_, channel))| StringMatchCandidate {
id: ix,
string: channel.name.clone().into(),
char_bag: channel.name.chars().collect(),
- },
- ));
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- if let Some(state) = &self.channel_editing_state {
- if matches!(state, ChannelEditingState::Create { location: None, .. }) {
- self.entries.push(ListEntry::ChannelEditor { depth: 0 });
- }
+ }),
+ );
+ let matches = executor.block(match_strings(
+ &self.match_candidates,
+ &query,
+ true,
+ usize::MAX,
+ &Default::default(),
+ executor.clone(),
+ ));
+ if let Some(state) = &self.channel_editing_state {
+ if matches!(state, ChannelEditingState::Create { location: None, .. }) {
+ self.entries.push(ListEntry::ChannelEditor { depth: 0 });
}
- let mut collapse_depth = None;
- for mat in matches {
- let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
- let depth = channel.parent_path.len();
-
- if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+ }
+ let mut collapse_depth = None;
+ for mat in matches {
+ let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
+ let depth = channel.parent_path.len();
+
+ if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+ collapse_depth = Some(depth);
+ } else if let Some(collapsed_depth) = collapse_depth {
+ if depth > collapsed_depth {
+ continue;
+ }
+ if self.is_channel_collapsed(channel.id) {
collapse_depth = Some(depth);
- } else if let Some(collapsed_depth) = collapse_depth {
- if depth > collapsed_depth {
- continue;
- }
- if self.is_channel_collapsed(channel.id) {
- collapse_depth = Some(depth);
- } else {
- collapse_depth = None;
- }
+ } else {
+ collapse_depth = None;
}
+ }
- let has_children = channel_store
- .channel_at_index(mat.candidate_id + 1)
- .map_or(false, |next_channel| {
- next_channel.parent_path.ends_with(&[channel.id])
- });
+ let has_children = channel_store
+ .channel_at_index(mat.candidate_id + 1)
+ .map_or(false, |next_channel| {
+ next_channel.parent_path.ends_with(&[channel.id])
+ });
- match &self.channel_editing_state {
- Some(ChannelEditingState::Create {
- location: parent_id,
- ..
- }) if *parent_id == Some(channel.id) => {
- self.entries.push(ListEntry::Channel {
- channel: channel.clone(),
- depth,
- has_children: false,
- });
- self.entries
- .push(ListEntry::ChannelEditor { depth: depth + 1 });
- }
- Some(ChannelEditingState::Rename {
- location: parent_id,
- ..
- }) if parent_id == &channel.id => {
- self.entries.push(ListEntry::ChannelEditor { depth });
- }
- _ => {
- self.entries.push(ListEntry::Channel {
- channel: channel.clone(),
- depth,
- has_children,
- });
- }
+ match &self.channel_editing_state {
+ Some(ChannelEditingState::Create {
+ location: parent_id,
+ ..
+ }) if *parent_id == Some(channel.id) => {
+ self.entries.push(ListEntry::Channel {
+ channel: channel.clone(),
+ depth,
+ has_children: false,
+ });
+ self.entries
+ .push(ListEntry::ChannelEditor { depth: depth + 1 });
+ }
+ Some(ChannelEditingState::Rename {
+ location: parent_id,
+ ..
+ }) if parent_id == &channel.id => {
+ self.entries.push(ListEntry::ChannelEditor { depth });
+ }
+ _ => {
+ self.entries.push(ListEntry::Channel {
+ channel: channel.clone(),
+ depth,
+ has_children,
+ });
}
}
}
+ }
- let channel_invites = channel_store.channel_invitations();
- if !channel_invites.is_empty() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
- StringMatchCandidate {
- id: ix,
- string: channel.name.clone().into(),
- char_bag: channel.name.chars().collect(),
- }
- }));
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- request_entries.extend(matches.iter().map(|mat| {
- ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
+ let channel_invites = channel_store.channel_invitations();
+ if !channel_invites.is_empty() {
+ self.match_candidates.clear();
+ self.match_candidates
+ .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
+ StringMatchCandidate {
+ id: ix,
+ string: channel.name.clone().into(),
+ char_bag: channel.name.chars().collect(),
+ }
}));
+ let matches = executor.block(match_strings(
+ &self.match_candidates,
+ &query,
+ true,
+ usize::MAX,
+ &Default::default(),
+ executor.clone(),
+ ));
+ request_entries.extend(
+ matches
+ .iter()
+ .map(|mat| ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())),
+ );
- if !request_entries.is_empty() {
- self.entries
- .push(ListEntry::Header(Section::ChannelInvites));
- if !self.collapsed_sections.contains(&Section::ChannelInvites) {
- self.entries.append(&mut request_entries);
- }
+ if !request_entries.is_empty() {
+ self.entries
+ .push(ListEntry::Header(Section::ChannelInvites));
+ if !self.collapsed_sections.contains(&Section::ChannelInvites) {
+ self.entries.append(&mut request_entries);
}
}
}
@@ -2000,43 +1998,49 @@ impl CollabPanel {
let busy = contact.busy || calling;
let user_id = contact.user.id;
let github_login = SharedString::from(contact.user.github_login.clone());
- let item =
- ListItem::new(github_login.clone())
- .indent_level(1)
- .indent_step_size(px(20.))
- .selected(is_selected)
- .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
- .child(
- h_flex()
- .w_full()
- .justify_between()
- .child(Label::new(github_login.clone()))
- .when(calling, |el| {
- el.child(Label::new("Calling").color(Color::Muted))
- })
- .when(!calling, |el| {
- el.child(
- IconButton::new("remove_contact", IconName::Close)
- .icon_color(Color::Muted)
- .visible_on_hover("")
- .tooltip(|cx| Tooltip::text("Remove Contact", cx))
- .on_click(cx.listener({
- let github_login = github_login.clone();
- move |this, _, cx| {
- this.remove_contact(user_id, &github_login, cx);
- }
- })),
- )
- }),
- )
- .start_slot(
- // todo handle contacts with no avatar
- Avatar::new(contact.user.avatar_uri.clone())
- .availability_indicator(if online { Some(!busy) } else { None }),
- )
- .when(online && !busy, |el| {
- el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
- });
+ let item = ListItem::new(github_login.clone())
+ .indent_level(1)
+ .indent_step_size(px(20.))
+ .selected(is_selected)
+ .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
+ .child(
+ h_flex()
+ .w_full()
+ .justify_between()
+ .child(Label::new(github_login.clone()))
+ .when(calling, |el| {
+ el.child(Label::new("Calling").color(Color::Muted))
+ })
+ .when(!calling, |el| {
+ el.child(
+ IconButton::new("remove_contact", IconName::Close)
+ .icon_color(Color::Muted)
+ .visible_on_hover("")
+ .tooltip(|cx| Tooltip::text("Remove Contact", cx))
+ .on_click(cx.listener({
+ let github_login = github_login.clone();
+ move |this, _, cx| {
+ this.remove_contact(user_id, &github_login, cx);
+ }
+ })),
+ )
+ }),
+ )
+ .start_slot(
+ // todo handle contacts with no avatar
+ Avatar::new(contact.user.avatar_uri.clone())
+ .indicator::<AvatarAvailabilityIndicator>(if online {
+ Some(AvatarAvailabilityIndicator::new(match busy {
+ true => ui::Availability::Busy,
+ false => ui::Availability::Free,
+ }))
+ } else {
+ None
+ }),
+ )
+ .when(online && !busy, |el| {
+ el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
+ });
div()
.id(github_login.clone())
@@ -2310,7 +2314,7 @@ impl CollabPanel {
.child(
IconButton::new("channel_chat", IconName::MessageBubbles)
.style(ButtonStyle::Filled)
- .size(ButtonSize::Compact)
+ .shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(if has_messages_notification {
Color::Default
@@ -2328,7 +2332,7 @@ impl CollabPanel {
.child(
IconButton::new("channel_notes", IconName::File)
.style(ButtonStyle::Filled)
- .size(ButtonSize::Compact)
+ .shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(if has_notes_notification {
Color::Default
@@ -348,6 +348,10 @@ impl PickerDelegate for ChannelModalDelegate {
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
+ if Some(selected_user.id) == self.user_store.read(cx).current_user().map(|user| user.id)
+ {
+ return;
+ }
match self.mode {
Mode::ManageMembers => {
self.show_context_menu(selected_user, role.unwrap_or(ChannelRole::Member), cx)
@@ -383,6 +387,7 @@ impl PickerDelegate for ChannelModalDelegate {
) -> Option<Self::ListItem> {
let (user, role) = self.user_at_index(ix)?;
let request_status = self.member_status(user.id, cx);
+ let is_me = self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
Some(
ListItem::new(ix)
@@ -406,7 +411,10 @@ impl PickerDelegate for ChannelModalDelegate {
Some(ChannelRole::Guest) => Some(Label::new("Guest")),
_ => None,
})
- .child(IconButton::new("ellipsis", IconName::Ellipsis))
+ .when(!is_me, |el| {
+ el.child(IconButton::new("ellipsis", IconName::Ellipsis))
+ })
+ .when(is_me, |el| el.child(Label::new("You").color(Color::Muted)))
.children(
if let (Some((menu, _)), true) = (&self.context_menu, selected) {
Some(
@@ -1,9 +1,9 @@
use crate::face_pile::FacePile;
use auto_update::AutoUpdateStatus;
use call::{ActiveCall, ParticipantLocation, Room};
-use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
+use client::{proto::PeerId, Client, User, UserStore};
use gpui::{
- actions, canvas, div, point, px, rems, Action, AnyElement, AppContext, Element, Hsla,
+ actions, canvas, div, point, px, Action, AnyElement, AppContext, Element, Hsla,
InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
WindowBounds,
@@ -12,14 +12,14 @@ use project::{Project, RepositoryEntry};
use recent_projects::RecentProjects;
use rpc::proto;
use std::sync::Arc;
-use theme::{ActiveTheme, PlayerColors};
+use theme::ActiveTheme;
use ui::{
- h_flex, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
- IconButton, IconName, TintColor, Tooltip,
+ h_flex, popover_menu, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike,
+ ButtonStyle, ContextMenu, Icon, IconButton, IconName, TintColor, Tooltip,
};
use util::ResultExt;
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
-use workspace::{notifications::NotifyResultExt, Workspace};
+use workspace::{notifications::NotifyResultExt, titlebar_height, Workspace};
const MAX_PROJECT_NAME_LENGTH: usize = 40;
const MAX_BRANCH_NAME_LENGTH: usize = 40;
@@ -62,10 +62,7 @@ impl Render for CollabTitlebarItem {
.id("titlebar")
.justify_between()
.w_full()
- .h(rems(1.75))
- // Set a non-scaling min-height here to ensure the titlebar is
- // always at least the height of the traffic lights.
- .min_h(px(32.))
+ .h(titlebar_height(cx))
.map(|this| {
if matches!(cx.window_bounds(), WindowBounds::Fullscreen) {
this.pl_2()
@@ -97,7 +94,7 @@ impl Render for CollabTitlebarItem {
room.remote_participants().values().collect::<Vec<_>>();
remote_participants.sort_by_key(|p| p.participant_index.0);
- this.children(self.render_collaborator(
+ let current_user_face_pile = self.render_collaborator(
¤t_user,
peer_id,
true,
@@ -107,7 +104,13 @@ impl Render for CollabTitlebarItem {
project_id,
¤t_user,
cx,
- ))
+ );
+
+ this.children(current_user_face_pile.map(|face_pile| {
+ v_flex()
+ .child(face_pile)
+ .child(render_color_ribbon(player_colors.local().cursor))
+ }))
.children(
remote_participants.iter().filter_map(|collaborator| {
let is_present = project_id.map_or(false, |project_id| {
@@ -132,8 +135,11 @@ impl Render for CollabTitlebarItem {
.id(("collaborator", collaborator.user.id))
.child(face_pile)
.child(render_color_ribbon(
- collaborator.participant_index,
- player_colors,
+ player_colors
+ .color_for_participant(
+ collaborator.participant_index.0,
+ )
+ .cursor,
))
.cursor_pointer()
.on_click({
@@ -312,8 +318,7 @@ impl Render for CollabTitlebarItem {
}
}
-fn render_color_ribbon(participant_index: ParticipantIndex, colors: &PlayerColors) -> gpui::Canvas {
- let color = colors.color_for_participant(participant_index.0).cursor;
+fn render_color_ribbon(color: Hsla) -> gpui::Canvas {
canvas(move |bounds, cx| {
let height = bounds.size.height;
let horizontal_offset = height;
@@ -472,7 +477,9 @@ impl CollabTitlebarItem {
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);
let pile = FacePile::default()
.child(
@@ -480,24 +487,48 @@ impl CollabTitlebarItem {
.grayscale(!is_present)
.border_color(if is_speaking {
cx.theme().status().info_border
- } else if is_muted {
- cx.theme().status().error_border
} else {
- Hsla::default()
+ // 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();
+ move |cx| Tooltip::text(format!("{} is muted", github_login), cx)
+ }),
+ )
+ }),
+ )
+ .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(Avatar::new(follower.avatar_uri.clone()))
}),
)
- .children(followers.iter().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(Avatar::new(follower.avatar_uri.clone()))
- }));
+ .children(if extra_count > 0 {
+ Some(
+ div()
+ .ml_1()
+ .child(Label::new(format!("+{extra_count}")))
+ .into_any_element(),
+ )
+ } else {
+ None
+ });
Some(pile)
}
@@ -12,7 +12,6 @@ use std::{rc::Rc, sync::Arc};
use call::{report_call_event_for_room, ActiveCall};
pub use collab_panel::CollabPanel;
pub use collab_titlebar_item::CollabTitlebarItem;
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
use gpui::{
actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
WindowKind, WindowOptions,
@@ -121,7 +120,3 @@ fn notification_window_options(
display_id: Some(screen.id()),
}
}
-
-fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
- cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
-}
@@ -0,0 +1,32 @@
+[package]
+name = "color"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[features]
+default = []
+stories = ["dep:itertools", "dep:story"]
+
+[lib]
+path = "src/color.rs"
+doctest = true
+
+[dependencies]
+# TODO: Clean up dependencies
+anyhow.workspace = true
+fs = { path = "../fs" }
+indexmap = "1.6.2"
+parking_lot.workspace = true
+refineable.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+settings = { path = "../settings" }
+story = { path = "../story", optional = true }
+toml.workspace = true
+uuid.workspace = true
+util = { path = "../util" }
+itertools = { version = "0.11.0", optional = true }
+palette = "0.7.3"
@@ -0,0 +1,234 @@
+//! # Color
+//!
+//! The `color` crate provides a set utilities for working with colors. It is a wrapper around the [`palette`](https://docs.rs/palette) crate with some additional functionality.
+//!
+//! It is used to create a manipulate colors when building themes.
+//!
+//! === In development note ===
+//!
+//! This crate is meant to sit between gpui and the theme/ui for all the color related stuff.
+//!
+//! It could be folded into gpui, ui or theme potentially but for now we'll continue
+//! to develop it in isolation.
+//!
+//! Once we have a good idea of the needs of the theme system and color in gpui in general I see 3 paths:
+//! 1. Use `palette` (or another color library) directly in gpui and everywhere else, rather than rolling our own color system.
+//! 2. Keep this crate as a thin wrapper around `palette` and use it everywhere except gpui, and convert to gpui's color system when needed.
+//! 3. Build the needed functionality into gpui and keep using it's color system everywhere.
+//!
+//! I'm leaning towards 2 in the short term and 1 in the long term, but we'll need to discuss it more.
+//!
+//! === End development note ===
+use palette::{
+ blend::Blend, convert::FromColorUnclamped, encoding, rgb::Rgb, Clamp, Mix, Srgb, WithAlpha,
+};
+
+/// The types of blend modes supported
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum BlendMode {
+ /// Multiplies the colors, resulting in a darker color. This mode is useful for creating shadows.
+ Multiply,
+ /// Lightens the color by adding the source and destination colors. It results in a lighter color.
+ Screen,
+ /// Combines Multiply and Screen blend modes. Parts of the image that are lighter than 50% gray are lightened, and parts that are darker are darkened.
+ Overlay,
+ /// Selects the darker of the base or blend color as the resulting color. Useful for darkening images without affecting the overall contrast.
+ Darken,
+ /// Selects the lighter of the base or blend color as the resulting color. Useful for lightening images without affecting the overall contrast.
+ Lighten,
+ /// Brightens the base color to reflect the blend color. The result is a lightened image.
+ Dodge,
+ /// Darkens the base color to reflect the blend color. The result is a darkened image.
+ Burn,
+ /// Similar to Overlay, but with a stronger effect. Hard Light can either multiply or screen colors, depending on the blend color.
+ HardLight,
+ /// A softer version of Hard Light. Soft Light either darkens or lightens colors, depending on the blend color.
+ SoftLight,
+ /// Subtracts the darker of the two constituent colors from the lighter color. Difference mode is useful for creating more vivid colors.
+ Difference,
+ /// Similar to Difference, but with a lower contrast. Exclusion mode produces an effect similar to Difference but with less intensity.
+ Exclusion,
+}
+
+/// Converts a hexadecimal color string to a `palette::Hsla` color.
+///
+/// This function supports the following hex formats:
+/// `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`.
+pub fn hex_to_hsla(s: &str) -> Result<RGBAColor, String> {
+ let hex = s.trim_start_matches('#');
+
+ // Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA
+ let hex = match hex.len() {
+ 3 => hex
+ .chars()
+ .map(|c| c.to_string().repeat(2))
+ .collect::<String>(),
+ 4 => {
+ let (rgb, alpha) = hex.split_at(3);
+ let rgb = rgb
+ .chars()
+ .map(|c| c.to_string().repeat(2))
+ .collect::<String>();
+ let alpha = alpha.chars().next().unwrap().to_string().repeat(2);
+ format!("{}{}", rgb, alpha)
+ }
+ 6 => format!("{}ff", hex), // Add alpha if missing
+ 8 => hex.to_string(), // Already in full format
+ _ => return Err("Invalid hexadecimal string length".to_string()),
+ };
+
+ let hex_val =
+ u32::from_str_radix(&hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
+
+ let r = ((hex_val >> 24) & 0xFF) as f32 / 255.0;
+ let g = ((hex_val >> 16) & 0xFF) as f32 / 255.0;
+ let b = ((hex_val >> 8) & 0xFF) as f32 / 255.0;
+ let a = (hex_val & 0xFF) as f32 / 255.0;
+
+ let color = RGBAColor { r, g, b, a };
+
+ Ok(color)
+}
+
+// These derives implement to and from palette's color types.
+#[derive(FromColorUnclamped, WithAlpha, Debug, Clone)]
+#[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")]
+pub struct RGBAColor {
+ r: f32,
+ g: f32,
+ b: f32,
+ // Let Palette know this is our alpha channel.
+ #[palette(alpha)]
+ a: f32,
+}
+
+impl FromColorUnclamped<RGBAColor> for RGBAColor {
+ fn from_color_unclamped(color: RGBAColor) -> RGBAColor {
+ color
+ }
+}
+
+impl<S> FromColorUnclamped<Rgb<S, f32>> for RGBAColor
+where
+ Srgb: FromColorUnclamped<Rgb<S, f32>>,
+{
+ fn from_color_unclamped(color: Rgb<S, f32>) -> RGBAColor {
+ let srgb = Srgb::from_color_unclamped(color);
+ RGBAColor {
+ r: srgb.red,
+ g: srgb.green,
+ b: srgb.blue,
+ a: 1.0,
+ }
+ }
+}
+
+impl<S> FromColorUnclamped<RGBAColor> for Rgb<S, f32>
+where
+ Rgb<S, f32>: FromColorUnclamped<Srgb>,
+{
+ fn from_color_unclamped(color: RGBAColor) -> Self {
+ let srgb = Srgb::new(color.r, color.g, color.b);
+ Self::from_color_unclamped(srgb)
+ }
+}
+
+impl Clamp for RGBAColor {
+ fn clamp(self) -> Self {
+ RGBAColor {
+ r: self.r.min(1.0).max(0.0),
+ g: self.g.min(1.0).max(0.0),
+ b: self.b.min(1.0).max(0.0),
+ a: self.a.min(1.0).max(0.0),
+ }
+ }
+}
+
+impl RGBAColor {
+ /// Creates a new color from the given RGBA values.
+ ///
+ /// This color can be used to convert to any [`palette::Color`] type.
+ pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
+ RGBAColor { r, g, b, a }
+ }
+
+ /// Returns a set of states for this color.
+ pub fn states(self, is_light: bool) -> ColorStates {
+ states_for_color(self, is_light)
+ }
+
+ /// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`.
+ pub fn mixed(&self, other: RGBAColor, mix_ratio: f32) -> Self {
+ let srgb_self = Srgb::new(self.r, self.g, self.b);
+ let srgb_other = Srgb::new(other.r, other.g, other.b);
+
+ // Directly mix the colors as sRGB values
+ let mixed = srgb_self.mix(srgb_other, mix_ratio);
+ RGBAColor::from_color_unclamped(mixed)
+ }
+
+ pub fn blend(&self, other: RGBAColor, blend_mode: BlendMode) -> Self {
+ let srgb_self = Srgb::new(self.r, self.g, self.b);
+ let srgb_other = Srgb::new(other.r, other.g, other.b);
+
+ let blended = match blend_mode {
+ // replace hsl methods with the respective sRGB methods
+ BlendMode::Multiply => srgb_self.multiply(srgb_other),
+ _ => unimplemented!(),
+ };
+
+ Self {
+ r: blended.red,
+ g: blended.green,
+ b: blended.blue,
+ a: self.a,
+ }
+ }
+}
+
+/// A set of colors for different states of an element.
+#[derive(Debug, Clone)]
+pub struct ColorStates {
+ /// The default color.
+ pub default: RGBAColor,
+ /// The color when the mouse is hovering over the element.
+ pub hover: RGBAColor,
+ /// The color when the mouse button is held down on the element.
+ pub active: RGBAColor,
+ /// The color when the element is focused with the keyboard.
+ pub focused: RGBAColor,
+ /// The color when the element is disabled.
+ pub disabled: RGBAColor,
+}
+
+/// Returns a set of colors for different states of an element.
+///
+/// todo!("This should take a theme and use appropriate colors from it")
+pub fn states_for_color(color: RGBAColor, is_light: bool) -> ColorStates {
+ let adjustment_factor = if is_light { 0.1 } else { -0.1 };
+ let hover_adjustment = 1.0 - adjustment_factor;
+ let active_adjustment = 1.0 - 2.0 * adjustment_factor;
+ let focused_adjustment = 1.0 - 3.0 * adjustment_factor;
+ let disabled_adjustment = 1.0 - 4.0 * adjustment_factor;
+
+ let make_adjustment = |color: RGBAColor, adjustment: f32| -> RGBAColor {
+ // Adjust lightness for each state
+ // Note: Adjustment logic may differ; simplify as needed for sRGB
+ RGBAColor::new(
+ color.r * adjustment,
+ color.g * adjustment,
+ color.b * adjustment,
+ color.a,
+ )
+ };
+
+ let color = color.clamp();
+
+ ColorStates {
+ default: color.clone(),
+ hover: make_adjustment(color.clone(), hover_adjustment),
+ active: make_adjustment(color.clone(), active_adjustment),
+ focused: make_adjustment(color.clone(), focused_adjustment),
+ disabled: make_adjustment(color.clone(), disabled_adjustment),
+ }
+}
@@ -2288,17 +2288,18 @@ impl EditorElement {
.map(|p| SharedString::from(p.to_string_lossy().to_string() + "/"));
}
- div()
+ v_flex()
.id(("path header container", block_id))
.size_full()
- .p_1p5()
+ .justify_center()
+ .p(gpui::px(6.))
.child(
h_flex()
.id("path header block")
- .py_1p5()
- .pl_3()
- .pr_2()
- .rounded_lg()
+ .size_full()
+ .pl(gpui::px(12.))
+ .pr(gpui::px(8.))
+ .rounded_md()
.shadow_md()
.border()
.border_color(cx.theme().colors().border)
@@ -2861,6 +2862,7 @@ impl Element for EditorElement {
cx.with_text_style(
Some(gpui::TextStyleRefinement {
font_size: Some(self.style.text.font_size),
+ line_height: Some(self.style.text.line_height),
..Default::default()
}),
|cx| {
@@ -339,6 +339,7 @@ fn show_hover(
this.hover_state.info_popover = hover_popover;
cx.notify();
+ cx.refresh();
})?;
Ok::<_, anyhow::Error>(())
@@ -16,12 +16,6 @@ pub trait FeatureFlag {
const NAME: &'static str;
}
-pub enum ChannelsAlpha {}
-
-impl FeatureFlag for ChannelsAlpha {
- const NAME: &'static str = "channels_alpha";
-}
-
pub trait FeatureFlagViewExt<V: 'static> {
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
where
@@ -192,7 +192,7 @@ pub trait PlatformDispatcher: Send + Sync {
pub trait PlatformTextSystem: Send + Sync {
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()>;
- fn all_font_families(&self) -> Vec<String>;
+ fn all_font_names(&self) -> Vec<String>;
fn font_id(&self, descriptor: &Font) -> Result<FontId>;
fn font_metrics(&self, font_id: FontId) -> FontMetrics;
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
@@ -82,7 +82,7 @@ impl MetalRenderer {
];
let unit_vertices = device.new_buffer_with_data(
unit_vertices.as_ptr() as *const c_void,
- (unit_vertices.len() * mem::size_of::<u64>()) as u64,
+ mem::size_of_val(&unit_vertices) as u64,
MTLResourceOptions::StorageModeManaged,
);
let instances = device.new_buffer(
@@ -340,7 +340,8 @@ impl MetalRenderer {
for (texture_id, vertices) in vertices_by_texture_id {
align_offset(offset);
- let next_offset = *offset + vertices.len() * mem::size_of::<PathVertex<ScaledPixels>>();
+ let vertices_bytes_len = mem::size_of_val(vertices.as_slice());
+ let next_offset = *offset + vertices_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return None;
}
@@ -373,7 +374,6 @@ impl MetalRenderer {
&texture_size as *const Size<DevicePixels> as *const _,
);
- let vertices_bytes_len = mem::size_of::<PathVertex<ScaledPixels>>() * vertices.len();
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
unsafe {
ptr::copy_nonoverlapping(
@@ -430,7 +430,7 @@ impl MetalRenderer {
&viewport_size as *const Size<DevicePixels> as *const _,
);
- let shadow_bytes_len = std::mem::size_of_val(shadows);
+ let shadow_bytes_len = mem::size_of_val(shadows);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let next_offset = *offset + shadow_bytes_len;
@@ -491,7 +491,7 @@ impl MetalRenderer {
&viewport_size as *const Size<DevicePixels> as *const _,
);
- let quad_bytes_len = std::mem::size_of_val(quads);
+ let quad_bytes_len = mem::size_of_val(quads);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let next_offset = *offset + quad_bytes_len;
@@ -591,7 +591,7 @@ impl MetalRenderer {
command_encoder
.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
- let sprite_bytes_len = mem::size_of::<MonochromeSprite>() * sprites.len();
+ let sprite_bytes_len = mem::size_of_val(sprites.as_slice());
let next_offset = *offset + sprite_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
@@ -656,21 +656,22 @@ impl MetalRenderer {
&viewport_size as *const Size<DevicePixels> as *const _,
);
- let quad_bytes_len = std::mem::size_of_val(underlines);
+ let underline_bytes_len = mem::size_of_val(underlines);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+
+ let next_offset = *offset + underline_bytes_len;
+ if next_offset > INSTANCE_BUFFER_SIZE {
+ return false;
+ }
+
unsafe {
ptr::copy_nonoverlapping(
underlines.as_ptr() as *const u8,
buffer_contents,
- quad_bytes_len,
+ underline_bytes_len,
);
}
- let next_offset = *offset + quad_bytes_len;
- if next_offset > INSTANCE_BUFFER_SIZE {
- return false;
- }
-
command_encoder.draw_primitives_instanced(
metal::MTLPrimitiveType::Triangle,
0,
@@ -727,7 +728,7 @@ impl MetalRenderer {
);
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
- let sprite_bytes_len = std::mem::size_of_val(sprites);
+ let sprite_bytes_len = mem::size_of_val(sprites);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let next_offset = *offset + sprite_bytes_len;
@@ -799,7 +800,7 @@ impl MetalRenderer {
);
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
- let sprite_bytes_len = std::mem::size_of_val(sprites);
+ let sprite_bytes_len = mem::size_of_val(sprites);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let next_offset = *offset + sprite_bytes_len;
@@ -5,7 +5,7 @@ use crate::{
};
use anyhow::anyhow;
use cocoa::appkit::{CGFloat, CGPoint};
-use collections::HashMap;
+use collections::{BTreeSet, HashMap};
use core_foundation::{
array::CFIndex,
attributed_string::{CFAttributedStringRef, CFMutableAttributedString},
@@ -78,12 +78,16 @@ impl PlatformTextSystem for MacTextSystem {
self.0.write().add_fonts(fonts)
}
- fn all_font_families(&self) -> Vec<String> {
- self.0
- .read()
- .system_source
- .all_families()
- .expect("core text should never return an error")
+ fn all_font_names(&self) -> Vec<String> {
+ let collection = core_text::font_collection::create_for_all_families();
+ let Some(descriptors) = collection.get_descriptors() else {
+ return vec![];
+ };
+ let mut names = BTreeSet::new();
+ for descriptor in descriptors.into_iter() {
+ names.insert(descriptor.display_name());
+ }
+ names.into_iter().collect()
}
fn font_id(&self, font: &Font) -> Result<FontId> {
@@ -299,8 +299,8 @@ impl<'a> Iterator for BatchIterator<'a> {
let first = orders_and_kinds[0];
let second = orders_and_kinds[1];
- let (batch_kind, max_order) = if first.0.is_some() {
- (first.1, second.0.unwrap_or(u32::MAX))
+ let (batch_kind, max_order_and_kind) = if first.0.is_some() {
+ (first.1, (second.0.unwrap_or(u32::MAX), second.1))
} else {
return None;
};
@@ -312,7 +312,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.shadows_iter.next();
while self
.shadows_iter
- .next_if(|shadow| shadow.order < max_order)
+ .next_if(|shadow| (shadow.order, batch_kind) < max_order_and_kind)
.is_some()
{
shadows_end += 1;
@@ -328,7 +328,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.quads_iter.next();
while self
.quads_iter
- .next_if(|quad| quad.order < max_order)
+ .next_if(|quad| (quad.order, batch_kind) < max_order_and_kind)
.is_some()
{
quads_end += 1;
@@ -342,7 +342,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.paths_iter.next();
while self
.paths_iter
- .next_if(|path| path.order < max_order)
+ .next_if(|path| (path.order, batch_kind) < max_order_and_kind)
.is_some()
{
paths_end += 1;
@@ -356,7 +356,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.underlines_iter.next();
while self
.underlines_iter
- .next_if(|underline| underline.order < max_order)
+ .next_if(|underline| (underline.order, batch_kind) < max_order_and_kind)
.is_some()
{
underlines_end += 1;
@@ -374,7 +374,8 @@ impl<'a> Iterator for BatchIterator<'a> {
while self
.monochrome_sprites_iter
.next_if(|sprite| {
- sprite.order < max_order && sprite.tile.texture_id == texture_id
+ (sprite.order, batch_kind) < max_order_and_kind
+ && sprite.tile.texture_id == texture_id
})
.is_some()
{
@@ -394,7 +395,8 @@ impl<'a> Iterator for BatchIterator<'a> {
while self
.polychrome_sprites_iter
.next_if(|sprite| {
- sprite.order < max_order && sprite.tile.texture_id == texture_id
+ (sprite.order, batch_kind) < max_order_and_kind
+ && sprite.tile.texture_id == texture_id
})
.is_some()
{
@@ -412,7 +414,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.surfaces_iter.next();
while self
.surfaces_iter
- .next_if(|surface| surface.order < max_order)
+ .next_if(|surface| (surface.order, batch_kind) < max_order_and_kind)
.is_some()
{
surfaces_end += 1;
@@ -386,7 +386,7 @@ impl Style {
let background_color = self.background.as_ref().and_then(Fill::color);
if background_color.map_or(false, |color| !color.is_transparent()) {
- cx.with_z_index(1, |cx| {
+ cx.with_z_index(0, |cx| {
let mut border_color = background_color.unwrap_or_default();
border_color.a = 0.;
cx.paint_quad(quad(
@@ -399,12 +399,12 @@ impl Style {
});
}
- cx.with_z_index(2, |cx| {
+ cx.with_z_index(0, |cx| {
continuation(cx);
});
if self.is_border_visible() {
- cx.with_z_index(3, |cx| {
+ cx.with_z_index(0, |cx| {
let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size);
let border_widths = self.border_widths.to_pixels(rem_size);
let max_border_width = border_widths.max();
@@ -65,8 +65,8 @@ impl TextSystem {
}
}
- pub fn all_font_families(&self) -> Vec<String> {
- let mut families = self.platform_text_system.all_font_families();
+ pub fn all_font_names(&self) -> Vec<String> {
+ let mut families = self.platform_text_system.all_font_names();
families.append(
&mut self
.fallback_font_stack
@@ -101,7 +101,6 @@ impl TextSystem {
if let Ok(font_id) = self.font_id(font) {
return font_id;
}
-
for fallback in &self.fallback_font_stack {
if let Ok(font_id) = self.font_id(fallback) {
return font_id;
@@ -315,6 +315,7 @@ pub(crate) struct Frame {
pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds<Pixels>)>,
pub(crate) z_index_stack: StackingOrder,
pub(crate) next_stacking_order_id: u32,
+ next_root_z_index: u8,
content_mask_stack: Vec<ContentMask<Pixels>>,
element_offset_stack: Vec<Point<Pixels>>,
requested_input_handler: Option<RequestedInputHandler>,
@@ -337,6 +338,7 @@ impl Frame {
depth_map: Vec::new(),
z_index_stack: StackingOrder::default(),
next_stacking_order_id: 0,
+ next_root_z_index: 0,
content_mask_stack: Vec::new(),
element_offset_stack: Vec::new(),
requested_input_handler: None,
@@ -354,6 +356,7 @@ impl Frame {
self.dispatch_tree.clear();
self.depth_map.clear();
self.next_stacking_order_id = 0;
+ self.next_root_z_index = 0;
self.reused_views.clear();
self.scene.clear();
self.requested_input_handler.take();
@@ -2450,8 +2453,13 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
};
let new_stacking_order_id =
post_inc(&mut self.window_mut().next_frame.next_stacking_order_id);
+ let new_root_z_index = post_inc(&mut self.window_mut().next_frame.next_root_z_index);
let old_stacking_order = mem::take(&mut self.window_mut().next_frame.z_index_stack);
self.window_mut().next_frame.z_index_stack.id = new_stacking_order_id;
+ self.window_mut()
+ .next_frame
+ .z_index_stack
+ .push(new_root_z_index);
self.window_mut().next_frame.content_mask_stack.push(mask);
let result = f(self);
self.window_mut().next_frame.content_mask_stack.pop();
@@ -85,6 +85,18 @@ fn generate_methods() -> Vec<TokenStream2> {
}
for (prefix, fields, prefix_doc_string) in border_prefixes() {
+ methods.push(generate_custom_value_setter(
+ // The plain method names (e.g., `border`, `border_t`, `border_r`, etc.) are special-cased
+ // versions of the 1px variants. This better matches Tailwind, but breaks our existing
+ // convention of the suffix-less variant of the method being the one that accepts a custom value
+ //
+ // To work around this, we're assigning a `_width` suffix here.
+ &format!("{prefix}_width"),
+ quote! { AbsoluteLength },
+ &fields,
+ prefix_doc_string,
+ ));
+
for (suffix, width_tokens, suffix_doc_string) in border_suffixes() {
methods.push(generate_predefined_setter(
prefix,
@@ -141,7 +153,7 @@ fn generate_predefined_setter(
}
fn generate_custom_value_setter(
- prefix: &'static str,
+ prefix: &str,
length_type: TokenStream2,
fields: &[TokenStream2],
doc_string: &str,
@@ -54,14 +54,10 @@ actions!(
[SearchInNew, ToggleFocus, NextField, ToggleFilters]
);
-#[derive(Default)]
-struct ActiveSearches(HashMap<WeakModel<Project>, WeakView<ProjectSearchView>>);
-
#[derive(Default)]
struct ActiveSettings(HashMap<WeakModel<Project>, ProjectSearchSettings>);
pub fn init(cx: &mut AppContext) {
- cx.set_global(ActiveSearches::default());
cx.set_global(ActiveSettings::default());
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace
@@ -282,6 +278,8 @@ impl EventEmitter<ViewEvent> for ProjectSearchView {}
impl Render for ProjectSearchView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ const PLEASE_AUTHENTICATE: &str = "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables. If you authenticated using the Assistant Panel, please restart Zed to Authenticate.";
+
if self.has_matches() {
div()
.flex_1()
@@ -303,40 +301,39 @@ impl Render for ProjectSearchView {
let mut show_minor_text = true;
let semantic_status = self.semantic_state.as_ref().and_then(|semantic| {
let status = semantic.index_status;
- match status {
- SemanticIndexStatus::NotAuthenticated => {
- major_text = Label::new("Not Authenticated");
- show_minor_text = false;
- Some(
- "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables. If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string())
- }
- SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
- SemanticIndexStatus::Indexing {
- remaining_files,
- rate_limit_expiry,
- } => {
- if remaining_files == 0 {
- Some("Indexing...".to_string())
- } else {
- if let Some(rate_limit_expiry) = rate_limit_expiry {
- let remaining_seconds =
- rate_limit_expiry.duration_since(Instant::now());
- if remaining_seconds > Duration::from_secs(0) {
- Some(format!(
- "Remaining files to index (rate limit resets in {}s): {}",
- remaining_seconds.as_secs(),
- remaining_files
- ))
- } else {
- Some(format!("Remaining files to index: {}", remaining_files))
- }
- } else {
- Some(format!("Remaining files to index: {}", remaining_files))
- }
- }
- }
- SemanticIndexStatus::NotIndexed => None,
+ match status {
+ SemanticIndexStatus::NotAuthenticated => {
+ major_text = Label::new("Not Authenticated");
+ show_minor_text = false;
+ Some(PLEASE_AUTHENTICATE.to_string())
+ }
+ SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
+ SemanticIndexStatus::Indexing {
+ remaining_files,
+ rate_limit_expiry,
+ } => {
+ if remaining_files == 0 {
+ Some("Indexing...".to_string())
+ } else {
+ if let Some(rate_limit_expiry) = rate_limit_expiry {
+ let remaining_seconds =
+ rate_limit_expiry.duration_since(Instant::now());
+ if remaining_seconds > Duration::from_secs(0) {
+ Some(format!(
+ "Remaining files to index (rate limit resets in {}s): {}",
+ remaining_seconds.as_secs(),
+ remaining_files
+ ))
+ } else {
+ Some(format!("Remaining files to index: {}", remaining_files))
}
+ } else {
+ Some(format!("Remaining files to index: {}", remaining_files))
+ }
+ }
+ }
+ SemanticIndexStatus::NotIndexed => None,
+ }
});
let major_text = div().justify_center().max_w_96().child(major_text);
@@ -947,25 +944,19 @@ impl ProjectSearchView {
});
}
- // Re-activate the most recently activated search or the most recent if it has been closed.
+ // Re-activate the most recently activated search in this pane or the most recent if it has been closed.
// If no search exists in the workspace, create a new one.
fn deploy_search(
workspace: &mut Workspace,
_: &workspace::DeploySearch,
cx: &mut ViewContext<Workspace>,
) {
- let active_search = cx
- .global::<ActiveSearches>()
- .0
- .get(&workspace.project().downgrade());
- let existing = active_search
- .and_then(|active_search| {
- workspace
- .items_of_type::<ProjectSearchView>(cx)
- .filter(|search| &search.downgrade() == active_search)
- .last()
- })
- .or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
+ let existing = workspace
+ .active_pane()
+ .read(cx)
+ .items()
+ .find_map(|item| item.downcast::<ProjectSearchView>());
+
Self::existing_or_new_search(workspace, existing, cx)
}
@@ -983,11 +974,6 @@ impl ProjectSearchView {
existing: Option<View<ProjectSearchView>>,
cx: &mut ViewContext<Workspace>,
) {
- // Clean up entries for dropped projects
- cx.update_global(|state: &mut ActiveSearches, _cx| {
- state.0.retain(|project, _| project.is_upgradable())
- });
-
let query = workspace.active_item(cx).and_then(|item| {
let editor = item.act_as::<Editor>(cx)?;
let query = editor.query_suggestion(cx);
@@ -1019,6 +1005,7 @@ impl ProjectSearchView {
workspace.add_item(Box::new(view.clone()), cx);
view
};
+
search.update(cx, |search, cx| {
if let Some(query) = query {
search.set_query(&query, cx);
@@ -3117,6 +3104,7 @@ pub mod tests {
async fn test_deploy_search_with_multiple_panes(cx: &mut TestAppContext) {
init_test(cx);
+ // Setup 2 panes, both with a file open and one with a project search.
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/dir",
@@ -3175,6 +3163,8 @@ pub mod tests {
}
})
.unwrap();
+
+ // Add a project search item to the second pane
window
.update(cx, {
let search_bar = search_bar.clone();
@@ -3194,6 +3184,8 @@ pub mod tests {
cx.run_until_parked();
assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2);
assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1);
+
+ // Focus the first pane
window
.update(cx, |workspace, cx| {
assert_eq!(workspace.active_pane(), &second_pane);
@@ -3212,20 +3204,47 @@ pub mod tests {
assert_eq!(second_pane.read(cx).items_len(), 2);
})
.unwrap();
+
+ // Deploy a new search
cx.dispatch_action(window.into(), DeploySearch);
- // We should have same # of items in workspace, the only difference being that
- // the search we've deployed previously should now be focused.
+ // Both panes should now have a project search in them
window
.update(cx, |workspace, cx| {
- assert_eq!(workspace.active_pane(), &second_pane);
- second_pane.update(cx, |this, _| {
+ assert_eq!(workspace.active_pane(), &first_pane);
+ first_pane.update(cx, |this, _| {
assert_eq!(this.active_item_index(), 1);
assert_eq!(this.items_len(), 2);
});
- first_pane.update(cx, |this, cx| {
+ second_pane.update(cx, |this, cx| {
assert!(!cx.focus_handle().contains_focused(cx));
- assert_eq!(this.items_len(), 1);
+ assert_eq!(this.items_len(), 2);
+ });
+ })
+ .unwrap();
+
+ // Focus the second pane's non-search item
+ window
+ .update(cx, |_workspace, cx| {
+ second_pane.update(cx, |pane, cx| pane.activate_next_item(true, cx));
+ })
+ .unwrap();
+
+ // Deploy a new search
+ cx.dispatch_action(window.into(), DeploySearch);
+
+ // The project search view should now be focused in the second pane
+ // And the number of items should be unchanged.
+ window
+ .update(cx, |_workspace, cx| {
+ second_pane.update(cx, |pane, _cx| {
+ assert!(pane
+ .active_item()
+ .unwrap()
+ .downcast::<ProjectSearchView>()
+ .is_some());
+
+ assert_eq!(pane.items_len(), 2);
});
})
.unwrap();
@@ -3235,7 +3254,7 @@ pub mod tests {
cx.update(|cx| {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
- cx.set_global(ActiveSearches::default());
+
SemanticIndexSettings::register(cx);
theme::init(theme::LoadThemes::JustBase, cx);
@@ -33,6 +33,7 @@ thiserror.workspace = true
lazy_static.workspace = true
serde.workspace = true
serde_derive.workspace = true
+serde_json.workspace = true
[dev-dependencies]
rand.workspace = true
@@ -1,6 +1,12 @@
use gpui::{px, AbsoluteLength, AppContext, FontFeatures, Pixels};
-use schemars::JsonSchema;
+use schemars::{
+ gen::SchemaGenerator,
+ schema::{InstanceType, RootSchema, Schema, SchemaObject},
+ JsonSchema,
+};
use serde_derive::{Deserialize, Serialize};
+use serde_json::Value;
+use settings::SettingsJsonSchemaParams;
use std::{collections::HashMap, path::PathBuf};
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -153,6 +159,39 @@ impl settings::Settings for TerminalSettings {
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
+ fn json_schema(
+ generator: &mut SchemaGenerator,
+ _: &SettingsJsonSchemaParams,
+ cx: &AppContext,
+ ) -> RootSchema {
+ let mut root_schema = generator.root_schema_for::<Self::FileContent>();
+ let available_fonts = cx
+ .text_system()
+ .all_font_names()
+ .into_iter()
+ .map(Value::String)
+ .collect();
+ let fonts_schema = SchemaObject {
+ instance_type: Some(InstanceType::String.into()),
+ enum_values: Some(available_fonts),
+ ..Default::default()
+ };
+ root_schema
+ .definitions
+ .extend([("FontFamilies".into(), fonts_schema.into())]);
+ root_schema
+ .schema
+ .object
+ .as_mut()
+ .unwrap()
+ .properties
+ .extend([(
+ "font_family".to_owned(),
+ Schema::new_ref("#/definitions/FontFamilies".into()),
+ )]);
+
+ root_schema
+ }
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
@@ -250,8 +250,8 @@ impl TerminalElement {
//Layout current cell text
{
- let cell_text = cell.c.to_string();
if !is_blank(&cell) {
+ let cell_text = cell.c.to_string();
let cell_style =
TerminalElement::cell_style(&cell, fg, theme, text_style, hyperlink);
@@ -586,24 +586,6 @@ impl TerminalElement {
}
}
- fn register_key_listeners(&self, cx: &mut WindowContext) {
- cx.on_key_event({
- let this = self.terminal.clone();
- move |event: &ModifiersChangedEvent, phase, cx| {
- if phase != DispatchPhase::Bubble {
- return;
- }
-
- let handled =
- this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
-
- if handled {
- cx.refresh();
- }
- }
- });
- }
-
fn register_mouse_listeners(
&mut self,
origin: Point<Pixels>,
@@ -771,53 +753,68 @@ impl Element for TerminalElement {
self.register_mouse_listeners(origin, layout.mode, bounds, cx);
- let mut interactivity = mem::take(&mut self.interactivity);
- interactivity.paint(bounds, bounds.size, state, cx, |_, _, cx| {
- cx.handle_input(&self.focus, terminal_input_handler);
+ self.interactivity
+ .paint(bounds, bounds.size, state, cx, |_, _, cx| {
+ cx.handle_input(&self.focus, terminal_input_handler);
- self.register_key_listeners(cx);
+ cx.on_key_event({
+ let this = self.terminal.clone();
+ move |event: &ModifiersChangedEvent, phase, cx| {
+ if phase != DispatchPhase::Bubble {
+ return;
+ }
- for rect in &layout.rects {
- rect.paint(origin, &layout, cx);
- }
+ let handled =
+ this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
- cx.with_z_index(1, |cx| {
- for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter()
- {
- if let Some((start_y, highlighted_range_lines)) =
- to_highlighted_range_lines(relative_highlighted_range, &layout, origin)
- {
- let hr = HighlightedRange {
- start_y, //Need to change this
- line_height: layout.dimensions.line_height,
- lines: highlighted_range_lines,
- color: color.clone(),
- //Copied from editor. TODO: move to theme or something
- corner_radius: 0.15 * layout.dimensions.line_height,
- };
- hr.paint(bounds, cx);
+ if handled {
+ cx.refresh();
+ }
}
- }
- });
+ });
- cx.with_z_index(2, |cx| {
- for cell in &layout.cells {
- cell.paint(origin, &layout, bounds, cx);
+ for rect in &layout.rects {
+ rect.paint(origin, &layout, cx);
}
- });
- if self.cursor_visible {
- cx.with_z_index(3, |cx| {
- if let Some(cursor) = &layout.cursor {
- cursor.paint(origin, cx);
+ cx.with_z_index(1, |cx| {
+ for (relative_highlighted_range, color) in
+ layout.relative_highlighted_ranges.iter()
+ {
+ if let Some((start_y, highlighted_range_lines)) =
+ to_highlighted_range_lines(relative_highlighted_range, &layout, origin)
+ {
+ let hr = HighlightedRange {
+ start_y, //Need to change this
+ line_height: layout.dimensions.line_height,
+ lines: highlighted_range_lines,
+ color: color.clone(),
+ //Copied from editor. TODO: move to theme or something
+ corner_radius: 0.15 * layout.dimensions.line_height,
+ };
+ hr.paint(bounds, cx);
+ }
}
});
- }
- if let Some(mut element) = layout.hyperlink_tooltip.take() {
- element.draw(origin, bounds.size.map(AvailableSpace::Definite), cx)
- }
- });
+ cx.with_z_index(2, |cx| {
+ for cell in &layout.cells {
+ cell.paint(origin, &layout, bounds, cx);
+ }
+ });
+
+ if self.cursor_visible {
+ cx.with_z_index(3, |cx| {
+ if let Some(cursor) = &layout.cursor {
+ cursor.paint(origin, cx);
+ }
+ });
+ }
+
+ if let Some(mut element) = layout.hyperlink_tooltip.take() {
+ element.draw(origin, bounds.size.map(AvailableSpace::Definite), cx)
+ }
+ });
}
}
@@ -34,6 +34,7 @@ story = { path = "../story", optional = true }
toml.workspace = true
uuid.workspace = true
util = { path = "../util" }
+color = {path = "../color"}
itertools = { version = "0.11.0", optional = true }
[dev-dependencies]
@@ -205,7 +205,7 @@ impl settings::Settings for ThemeSettings {
let available_fonts = cx
.text_system()
- .all_font_families()
+ .all_font_names()
.into_iter()
.map(Value::String)
.collect();
@@ -234,6 +234,10 @@ impl settings::Settings for ThemeSettings {
"buffer_font_family".to_owned(),
Schema::new_ref("#/definitions/FontFamilies".into()),
),
+ (
+ "ui_font_family".to_owned(),
+ Schema::new_ref("#/definitions/FontFamilies".into()),
+ ),
]);
root_schema
@@ -1,135 +1,7 @@
-use crate::prelude::*;
-use gpui::{img, Hsla, ImageSource, Img, IntoElement, Styled};
+mod avatar;
+mod avatar_audio_status_indicator;
+mod avatar_availability_indicator;
-/// The shape of an [`Avatar`].
-#[derive(Debug, Default, PartialEq, Clone)]
-pub enum AvatarShape {
- /// The avatar is shown in a circle.
- #[default]
- Circle,
- /// The avatar is shown in a rectangle with rounded corners.
- RoundedRectangle,
-}
-
-/// An element that renders a user avatar with customizable appearance options.
-///
-/// # Examples
-///
-/// ```
-/// use ui::{Avatar, AvatarShape};
-///
-/// Avatar::new("path/to/image.png")
-/// .shape(AvatarShape::Circle)
-/// .grayscale(true)
-/// .border_color(gpui::red());
-/// ```
-#[derive(IntoElement)]
-pub struct Avatar {
- image: Img,
- size: Option<Pixels>,
- border_color: Option<Hsla>,
- is_available: Option<bool>,
-}
-
-impl RenderOnce for Avatar {
- fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
- if self.image.style().corner_radii.top_left.is_none() {
- self = self.shape(AvatarShape::Circle);
- }
-
- let size = self.size.unwrap_or_else(|| cx.rem_size());
-
- div()
- .size(size + px(2.))
- .map(|mut div| {
- div.style().corner_radii = self.image.style().corner_radii.clone();
- div
- })
- .when_some(self.border_color, |this, color| {
- this.border().border_color(color)
- })
- .child(
- self.image
- .size(size)
- .bg(cx.theme().colors().ghost_element_background),
- )
- .children(self.is_available.map(|is_free| {
- // HACK: non-integer sizes result in oval indicators.
- let indicator_size = (size * 0.4).round();
-
- div()
- .absolute()
- .z_index(1)
- .bg(if is_free {
- cx.theme().status().created
- } else {
- cx.theme().status().deleted
- })
- .size(indicator_size)
- .rounded(indicator_size)
- .bottom_0()
- .right_0()
- }))
- }
-}
-
-impl Avatar {
- pub fn new(src: impl Into<ImageSource>) -> Self {
- Avatar {
- image: img(src),
- is_available: None,
- border_color: None,
- size: None,
- }
- }
-
- /// Sets the shape of the avatar image.
- ///
- /// This method allows the shape of the avatar to be specified using a [`Shape`].
- /// It modifies the corner radius of the image to match the specified shape.
- ///
- /// # Examples
- ///
- /// ```
- /// use ui::{Avatar, AvatarShape};
- ///
- /// Avatar::new("path/to/image.png").shape(AvatarShape::Circle);
- /// ```
- pub fn shape(mut self, shape: AvatarShape) -> Self {
- self.image = match shape {
- AvatarShape::Circle => self.image.rounded_full(),
- AvatarShape::RoundedRectangle => self.image.rounded_md(),
- };
- self
- }
-
- /// Applies a grayscale filter to the avatar image.
- ///
- /// # Examples
- ///
- /// ```
- /// use ui::{Avatar, AvatarShape};
- ///
- /// let avatar = Avatar::new("path/to/image.png").grayscale(true);
- /// ```
- pub fn grayscale(mut self, grayscale: bool) -> Self {
- self.image = self.image.grayscale(grayscale);
- self
- }
-
- pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
- self.border_color = Some(color.into());
- self
- }
-
- pub fn availability_indicator(mut self, is_available: impl Into<Option<bool>>) -> Self {
- self.is_available = is_available.into();
- self
- }
-
- /// Size overrides the avatar size. By default they are 1rem.
- pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
- self.size = size.into();
- self
- }
-}
+pub use avatar::*;
+pub use avatar_audio_status_indicator::*;
+pub use avatar_availability_indicator::*;
@@ -0,0 +1,130 @@
+use crate::prelude::*;
+
+use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
+
+/// The shape of an [`Avatar`].
+#[derive(Debug, Default, PartialEq, Clone)]
+pub enum AvatarShape {
+ /// The avatar is shown in a circle.
+ #[default]
+ Circle,
+ /// The avatar is shown in a rectangle with rounded corners.
+ RoundedRectangle,
+}
+
+/// An element that renders a user avatar with customizable appearance options.
+///
+/// # Examples
+///
+/// ```
+/// use ui::{Avatar, AvatarShape};
+///
+/// Avatar::new("path/to/image.png")
+/// .shape(AvatarShape::Circle)
+/// .grayscale(true)
+/// .border_color(gpui::red());
+/// ```
+#[derive(IntoElement)]
+pub struct Avatar {
+ image: Img,
+ size: Option<Pixels>,
+ border_color: Option<Hsla>,
+ indicator: Option<AnyElement>,
+}
+
+impl Avatar {
+ pub fn new(src: impl Into<ImageSource>) -> Self {
+ Avatar {
+ image: img(src),
+ size: None,
+ border_color: None,
+ indicator: None,
+ }
+ }
+
+ /// Sets the shape of the avatar image.
+ ///
+ /// This method allows the shape of the avatar to be specified using a [`Shape`].
+ /// It modifies the corner radius of the image to match the specified shape.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use ui::{Avatar, AvatarShape};
+ ///
+ /// Avatar::new("path/to/image.png").shape(AvatarShape::Circle);
+ /// ```
+ pub fn shape(mut self, shape: AvatarShape) -> Self {
+ self.image = match shape {
+ AvatarShape::Circle => self.image.rounded_full(),
+ AvatarShape::RoundedRectangle => self.image.rounded_md(),
+ };
+ self
+ }
+
+ /// Applies a grayscale filter to the avatar image.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use ui::{Avatar, AvatarShape};
+ ///
+ /// let avatar = Avatar::new("path/to/image.png").grayscale(true);
+ /// ```
+ pub fn grayscale(mut self, grayscale: bool) -> Self {
+ self.image = self.image.grayscale(grayscale);
+ self
+ }
+
+ pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
+ self.border_color = Some(color.into());
+ self
+ }
+
+ /// Size overrides the avatar size. By default they are 1rem.
+ pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
+ self.size = size.into();
+ self
+ }
+
+ pub fn indicator<E: IntoElement>(mut self, indicator: impl Into<Option<E>>) -> Self {
+ self.indicator = indicator.into().map(IntoElement::into_any_element);
+ self
+ }
+}
+
+impl RenderOnce for Avatar {
+ fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
+ if self.image.style().corner_radii.top_left.is_none() {
+ self = self.shape(AvatarShape::Circle);
+ }
+
+ let border_width = if self.border_color.is_some() {
+ px(2.)
+ } else {
+ px(0.)
+ };
+
+ let image_size = self.size.unwrap_or_else(|| cx.rem_size());
+ let container_size = image_size + border_width * 2.;
+
+ div()
+ .size(container_size)
+ .map(|mut div| {
+ div.style().corner_radii = self.image.style().corner_radii.clone();
+ div
+ })
+ .when_some(self.border_color, |this, color| {
+ this.border_width(border_width).border_color(color)
+ })
+ .child(
+ self.image
+ .size(image_size)
+ .bg(cx.theme().colors().ghost_element_background),
+ )
+ .children(
+ self.indicator
+ .map(|indicator| div().z_index(1).child(indicator)),
+ )
+ }
+}
@@ -0,0 +1,65 @@
+use gpui::AnyView;
+
+use crate::prelude::*;
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum AudioStatus {
+ Muted,
+ Deafened,
+}
+
+#[derive(IntoElement)]
+pub struct AvatarAudioStatusIndicator {
+ audio_status: AudioStatus,
+ tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
+}
+
+impl AvatarAudioStatusIndicator {
+ pub fn new(audio_status: AudioStatus) -> Self {
+ Self {
+ audio_status,
+ tooltip: None,
+ }
+ }
+
+ pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+ self.tooltip = Some(Box::new(tooltip));
+ self
+ }
+}
+
+impl RenderOnce for AvatarAudioStatusIndicator {
+ fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+ let icon_size = IconSize::Indicator;
+
+ let width_in_px = icon_size.rems() * cx.rem_size();
+ let padding_x = px(4.);
+
+ div()
+ .absolute()
+ .bottom(rems(-3. / 16.))
+ .right(rems(-6. / 16.))
+ .w(width_in_px + padding_x)
+ .h(icon_size.rems())
+ .child(
+ h_flex()
+ .id("muted-indicator")
+ .justify_center()
+ .px(padding_x)
+ .py(px(2.))
+ .bg(cx.theme().status().error_background)
+ .rounded_md()
+ .child(
+ Icon::new(match self.audio_status {
+ AudioStatus::Muted => IconName::MicMute,
+ AudioStatus::Deafened => IconName::AudioOff,
+ })
+ .size(icon_size)
+ .color(Color::Error),
+ )
+ .when_some(self.tooltip, |this, tooltip| {
+ this.tooltip(move |cx| tooltip(cx))
+ }),
+ )
+ }
+}
@@ -0,0 +1,48 @@
+use crate::prelude::*;
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum Availability {
+ Free,
+ Busy,
+}
+
+#[derive(IntoElement)]
+pub struct AvatarAvailabilityIndicator {
+ availability: Availability,
+ avatar_size: Option<Pixels>,
+}
+
+impl AvatarAvailabilityIndicator {
+ pub fn new(availability: Availability) -> Self {
+ Self {
+ availability,
+ avatar_size: None,
+ }
+ }
+
+ /// Sets the size of the [`Avatar`] this indicator appears on.
+ pub fn avatar_size(mut self, size: impl Into<Option<Pixels>>) -> Self {
+ self.avatar_size = size.into();
+ self
+ }
+}
+
+impl RenderOnce for AvatarAvailabilityIndicator {
+ fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+ let avatar_size = self.avatar_size.unwrap_or_else(|| cx.rem_size());
+
+ // HACK: non-integer sizes result in oval indicators.
+ let indicator_size = (avatar_size * 0.4).round();
+
+ div()
+ .absolute()
+ .bottom_0()
+ .right_0()
+ .size(indicator_size)
+ .rounded(indicator_size)
+ .bg(match self.availability {
+ Availability::Free => cx.theme().status().created,
+ Availability::Busy => cx.theme().status().deleted,
+ })
+ }
+}
@@ -127,16 +127,25 @@ impl VisibleOnHover for IconButton {
}
impl RenderOnce for IconButton {
- fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+ fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let is_disabled = self.base.disabled;
let is_selected = self.base.selected;
let selected_style = self.base.selected_style;
self.base
.map(|this| match self.shape {
- IconButtonShape::Square => this
- .width(self.icon_size.rems().into())
- .height(self.icon_size.rems().into()),
+ IconButtonShape::Square => {
+ let icon_size = self.icon_size.rems() * cx.rem_size();
+ let padding = match self.icon_size {
+ IconSize::Indicator => px(0.),
+ IconSize::XSmall => px(0.),
+ IconSize::Small => px(2.),
+ IconSize::Medium => px(2.),
+ };
+
+ this.width((icon_size + padding * 2.).into())
+ .height((icon_size + padding * 2.).into())
+ }
IconButtonShape::Wide => this,
})
.child(
@@ -51,6 +51,7 @@ impl ContextMenu {
let _on_blur_subscription = cx.on_blur(&focus_handle, |this: &mut ContextMenu, cx| {
this.cancel(&menu::Cancel, cx)
});
+ cx.refresh();
f(
Self {
items: Default::default(),
@@ -5,6 +5,7 @@ use crate::prelude::*;
#[derive(Default, PartialEq, Copy, Clone)]
pub enum IconSize {
+ Indicator,
XSmall,
Small,
#[default]
@@ -14,6 +15,7 @@ pub enum IconSize {
impl IconSize {
pub fn rems(self) -> Rems {
match self {
+ IconSize::Indicator => rems(10. / 16.),
IconSize::XSmall => rems(12. / 16.),
IconSize::Small => rems(14. / 16.),
IconSize::Medium => rems(16. / 16.),
@@ -1,29 +1,63 @@
use gpui::Render;
-use story::Story;
+use story::{StoryContainer, StoryItem, StorySection};
-use crate::prelude::*;
-use crate::Avatar;
+use crate::{prelude::*, AudioStatus, Availability, AvatarAvailabilityIndicator};
+use crate::{Avatar, AvatarAudioStatusIndicator};
pub struct AvatarStory;
impl Render for AvatarStory {
- fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- Story::container()
- .child(Story::title_for::<Avatar>())
- .child(Story::label("Default"))
- .child(Avatar::new(
- "https://avatars.githubusercontent.com/u/1714999?v=4",
- ))
- .child(Avatar::new(
- "https://avatars.githubusercontent.com/u/326587?v=4",
- ))
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ StoryContainer::new("Avatar", "crates/ui/src/components/stories/avatar.rs")
.child(
- Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
- .availability_indicator(true),
+ StorySection::new()
+ .child(StoryItem::new(
+ "Default",
+ Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4"),
+ ))
+ .child(StoryItem::new(
+ "Default",
+ Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4"),
+ )),
)
.child(
- Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
- .availability_indicator(false),
+ StorySection::new()
+ .child(StoryItem::new(
+ "With free availability indicator",
+ Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+ .indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
+ ))
+ .child(StoryItem::new(
+ "With busy availability indicator",
+ Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+ .indicator(AvatarAvailabilityIndicator::new(Availability::Busy)),
+ )),
+ )
+ .child(
+ StorySection::new()
+ .child(StoryItem::new(
+ "With info border",
+ Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+ .border_color(cx.theme().status().info_border),
+ ))
+ .child(StoryItem::new(
+ "With error border",
+ Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+ .border_color(cx.theme().status().error_border),
+ )),
+ )
+ .child(
+ StorySection::new()
+ .child(StoryItem::new(
+ "With muted audio indicator",
+ Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+ .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)),
+ ))
+ .child(StoryItem::new(
+ "With deafened audio indicator",
+ Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+ .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)),
+ )),
)
}
}
@@ -1,7 +1,7 @@
use gpui::Render;
use story::{StoryContainer, StoryItem, StorySection};
-use crate::{prelude::*, Tooltip};
+use crate::{prelude::*, IconButtonShape, Tooltip};
use crate::{IconButton, IconName};
pub struct IconButtonStory;
@@ -115,7 +115,34 @@ impl Render for IconButtonStory {
"Icon Button",
"crates/ui2/src/components/stories/icon_button.rs",
)
- .children(vec![StorySection::new().children(buttons)])
+ .child(StorySection::new().children(buttons))
+ .child(
+ StorySection::new().child(StoryItem::new(
+ "Square",
+ h_flex()
+ .gap_2()
+ .child(
+ IconButton::new("square-medium", IconName::Close)
+ .shape(IconButtonShape::Square)
+ .icon_size(IconSize::Medium),
+ )
+ .child(
+ IconButton::new("square-small", IconName::Close)
+ .shape(IconButtonShape::Square)
+ .icon_size(IconSize::Small),
+ )
+ .child(
+ IconButton::new("square-xsmall", IconName::Close)
+ .shape(IconButtonShape::Square)
+ .icon_size(IconSize::XSmall),
+ )
+ .child(
+ IconButton::new("square-indicator", IconName::Close)
+ .shape(IconButtonShape::Square)
+ .icon_size(IconSize::Indicator),
+ ),
+ )),
+ )
.into_element()
}
}
@@ -71,6 +71,30 @@ async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
assert_eq!(cx.mode(), Mode::Normal);
}
+#[gpui::test]
+async fn test_cancel_selection(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+
+ cx.set_state(
+ indoc! {"The quick brown fox juหmps over the lazy dog"},
+ Mode::Normal,
+ );
+ // jumps
+ cx.simulate_keystrokes(["v", "l", "l"]);
+ cx.assert_editor_state("The quick brown fox juยซmpsหยป over the lazy dog");
+
+ cx.simulate_keystrokes(["escape"]);
+ cx.assert_editor_state("The quick brown fox jumpหs over the lazy dog");
+
+ // go back to the same selection state
+ cx.simulate_keystrokes(["v", "h", "h"]);
+ cx.assert_editor_state("The quick brown fox juยซหmpsยป over the lazy dog");
+
+ // Ctrl-[ should behave like Esc
+ cx.simulate_keystrokes(["ctrl-["]);
+ cx.assert_editor_state("The quick brown fox juหmps over the lazy dog");
+}
+
#[gpui::test]
async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
@@ -60,190 +60,197 @@ pub struct WelcomePage {
impl Render for WelcomePage {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
- h_flex().full().track_focus(&self.focus_handle).child(
- v_flex()
- .w_96()
- .gap_4()
- .mx_auto()
- .child(
- svg()
- .path("icons/logo_96.svg")
- .text_color(gpui::white())
- .w(px(96.))
- .h(px(96.))
- .mx_auto(),
- )
- .child(
- h_flex()
- .justify_center()
- .child(Label::new("Code at the speed of thought")),
- )
- .child(
- v_flex()
- .gap_2()
- .child(
- Button::new("choose-theme", "Choose a theme")
- .full_width()
- .on_click(cx.listener(|this, _, cx| {
- this.telemetry
- .report_app_event("welcome page: change theme".to_string());
- this.workspace
- .update(cx, |workspace, cx| {
- theme_selector::toggle(
- workspace,
- &Default::default(),
- cx,
- )
- })
- .ok();
- })),
- )
- .child(
- Button::new("choose-keymap", "Choose a keymap")
- .full_width()
- .on_click(cx.listener(|this, _, cx| {
- this.telemetry.report_app_event(
- "welcome page: change keymap".to_string(),
- );
- this.workspace
- .update(cx, |workspace, cx| {
- base_keymap_picker::toggle(
- workspace,
- &Default::default(),
- cx,
- )
- })
- .ok();
- })),
- )
- .child(
- Button::new("install-cli", "Install the CLI")
- .full_width()
- .on_click(cx.listener(|this, _, cx| {
- this.telemetry
- .report_app_event("welcome page: install cli".to_string());
- cx.app_mut()
- .spawn(
- |cx| async move { install_cli::install_cli(&cx).await },
+ h_flex()
+ .full()
+ .bg(cx.theme().colors().editor_background)
+ .track_focus(&self.focus_handle)
+ .child(
+ v_flex()
+ .w_96()
+ .gap_4()
+ .mx_auto()
+ .child(
+ svg()
+ .path("icons/logo_96.svg")
+ .text_color(gpui::white())
+ .w(px(96.))
+ .h(px(96.))
+ .mx_auto(),
+ )
+ .child(
+ h_flex()
+ .justify_center()
+ .child(Label::new("Code at the speed of thought")),
+ )
+ .child(
+ v_flex()
+ .gap_2()
+ .child(
+ Button::new("choose-theme", "Choose a theme")
+ .full_width()
+ .on_click(cx.listener(|this, _, cx| {
+ this.telemetry.report_app_event(
+ "welcome page: change theme".to_string(),
+ );
+ this.workspace
+ .update(cx, |workspace, cx| {
+ theme_selector::toggle(
+ workspace,
+ &Default::default(),
+ cx,
+ )
+ })
+ .ok();
+ })),
+ )
+ .child(
+ Button::new("choose-keymap", "Choose a keymap")
+ .full_width()
+ .on_click(cx.listener(|this, _, cx| {
+ this.telemetry.report_app_event(
+ "welcome page: change keymap".to_string(),
+ );
+ this.workspace
+ .update(cx, |workspace, cx| {
+ base_keymap_picker::toggle(
+ workspace,
+ &Default::default(),
+ cx,
+ )
+ })
+ .ok();
+ })),
+ )
+ .child(
+ Button::new("install-cli", "Install the CLI")
+ .full_width()
+ .on_click(cx.listener(|this, _, cx| {
+ this.telemetry.report_app_event(
+ "welcome page: install cli".to_string(),
+ );
+ cx.app_mut()
+ .spawn(|cx| async move {
+ install_cli::install_cli(&cx).await
+ })
+ .detach_and_log_err(cx);
+ })),
+ ),
+ )
+ .child(
+ v_flex()
+ .p_3()
+ .gap_2()
+ .bg(cx.theme().colors().elevated_surface_background)
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .rounded_md()
+ .child(
+ h_flex()
+ .gap_2()
+ .child(
+ Checkbox::new(
+ "enable-vim",
+ if VimModeSetting::get_global(cx).0 {
+ ui::Selection::Selected
+ } else {
+ ui::Selection::Unselected
+ },
)
- .detach_and_log_err(cx);
- })),
- ),
- )
- .child(
- v_flex()
- .p_3()
- .gap_2()
- .bg(cx.theme().colors().elevated_surface_background)
- .border_1()
- .border_color(cx.theme().colors().border)
- .rounded_md()
- .child(
- h_flex()
- .gap_2()
- .child(
- Checkbox::new(
- "enable-vim",
- if VimModeSetting::get_global(cx).0 {
- ui::Selection::Selected
- } else {
- ui::Selection::Unselected
- },
+ .on_click(
+ cx.listener(move |this, selection, cx| {
+ this.telemetry.report_app_event(
+ "welcome page: toggle vim".to_string(),
+ );
+ this.update_settings::<VimModeSetting>(
+ selection,
+ cx,
+ |setting, value| *setting = Some(value),
+ );
+ }),
+ ),
)
- .on_click(cx.listener(
- move |this, selection, cx| {
- this.telemetry.report_app_event(
- "welcome page: toggle vim".to_string(),
- );
- this.update_settings::<VimModeSetting>(
- selection,
- cx,
- |setting, value| *setting = Some(value),
- );
- },
- )),
- )
- .child(Label::new("Enable vim mode")),
- )
- .child(
- h_flex()
- .gap_2()
- .child(
- Checkbox::new(
- "enable-telemetry",
- if TelemetrySettings::get_global(cx).metrics {
- ui::Selection::Selected
- } else {
- ui::Selection::Unselected
- },
- )
- .on_click(cx.listener(
- move |this, selection, cx| {
- this.telemetry.report_app_event(
- "welcome page: toggle metric telemetry".to_string(),
- );
- this.update_settings::<TelemetrySettings>(
- selection,
- cx,
- {
- let telemetry = this.telemetry.clone();
+ .child(Label::new("Enable vim mode")),
+ )
+ .child(
+ h_flex()
+ .gap_2()
+ .child(
+ Checkbox::new(
+ "enable-telemetry",
+ if TelemetrySettings::get_global(cx).metrics {
+ ui::Selection::Selected
+ } else {
+ ui::Selection::Unselected
+ },
+ )
+ .on_click(
+ cx.listener(move |this, selection, cx| {
+ this.telemetry.report_app_event(
+ "welcome page: toggle metric telemetry"
+ .to_string(),
+ );
+ this.update_settings::<TelemetrySettings>(
+ selection,
+ cx,
+ {
+ let telemetry = this.telemetry.clone();
- move |settings, value| {
- settings.metrics = Some(value);
+ move |settings, value| {
+ settings.metrics = Some(value);
- telemetry.report_setting_event(
- "metric telemetry",
- value.to_string(),
- );
- }
- },
- );
- },
- )),
- )
- .child(Label::new("Send anonymous usage data")),
- )
- .child(
- h_flex()
- .gap_2()
- .child(
- Checkbox::new(
- "enable-crash",
- if TelemetrySettings::get_global(cx).diagnostics {
- ui::Selection::Selected
- } else {
- ui::Selection::Unselected
- },
+ telemetry.report_setting_event(
+ "metric telemetry",
+ value.to_string(),
+ );
+ }
+ },
+ );
+ }),
+ ),
)
- .on_click(cx.listener(
- move |this, selection, cx| {
- this.telemetry.report_app_event(
- "welcome page: toggle diagnostic telemetry"
- .to_string(),
- );
- this.update_settings::<TelemetrySettings>(
- selection,
- cx,
- {
- let telemetry = this.telemetry.clone();
+ .child(Label::new("Send anonymous usage data")),
+ )
+ .child(
+ h_flex()
+ .gap_2()
+ .child(
+ Checkbox::new(
+ "enable-crash",
+ if TelemetrySettings::get_global(cx).diagnostics {
+ ui::Selection::Selected
+ } else {
+ ui::Selection::Unselected
+ },
+ )
+ .on_click(
+ cx.listener(move |this, selection, cx| {
+ this.telemetry.report_app_event(
+ "welcome page: toggle diagnostic telemetry"
+ .to_string(),
+ );
+ this.update_settings::<TelemetrySettings>(
+ selection,
+ cx,
+ {
+ let telemetry = this.telemetry.clone();
- move |settings, value| {
- settings.diagnostics = Some(value);
+ move |settings, value| {
+ settings.diagnostics = Some(value);
- telemetry.report_setting_event(
- "diagnostic telemetry",
- value.to_string(),
- );
- }
- },
- );
- },
- )),
- )
- .child(Label::new("Send crash reports")),
- ),
- ),
- )
+ telemetry.report_setting_event(
+ "diagnostic telemetry",
+ value.to_string(),
+ );
+ }
+ },
+ );
+ }),
+ ),
+ )
+ .child(Label::new("Send crash reports")),
+ ),
+ ),
+ )
}
}
@@ -27,7 +27,7 @@ impl<V: ModalView> ModalViewHandle for View<V> {
pub struct ActiveModal {
modal: Box<dyn ModalViewHandle>,
- _subscription: Subscription,
+ _subscriptions: [Subscription; 2],
previous_focus_handle: Option<FocusHandle>,
focus_handle: FocusHandle,
}
@@ -61,13 +61,19 @@ impl ModalLayer {
where
V: ModalView,
{
+ let focus_handle = cx.focus_handle();
self.active_modal = Some(ActiveModal {
modal: Box::new(new_modal.clone()),
- _subscription: cx.subscribe(&new_modal, |this, _, _: &DismissEvent, cx| {
- this.hide_modal(cx);
- }),
+ _subscriptions: [
+ cx.subscribe(&new_modal, |this, _, _: &DismissEvent, cx| {
+ this.hide_modal(cx);
+ }),
+ cx.on_focus_out(&focus_handle, |this, cx| {
+ this.hide_modal(cx);
+ }),
+ ],
previous_focus_handle: cx.focused(),
- focus_handle: cx.focus_handle(),
+ focus_handle,
});
cx.focus_view(&new_modal);
cx.notify();
@@ -108,7 +114,7 @@ impl ModalLayer {
}
impl Render for ModalLayer {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
let Some(active_modal) = &self.active_modal else {
return div();
};
@@ -127,13 +133,7 @@ impl Render for ModalLayer {
.flex_col()
.items_center()
.track_focus(&active_modal.focus_handle)
- .child(
- h_flex()
- .on_mouse_down_out(cx.listener(|this, _, cx| {
- this.hide_modal(cx);
- }))
- .child(active_modal.modal.view()),
- ),
+ .child(h_flex().child(active_modal.modal.view())),
)
}
}
@@ -99,6 +99,7 @@ actions!(
CloseItemsToTheLeft,
CloseItemsToTheRight,
GoBack,
+ DeploySearch,
GoForward,
ReopenClosedItem,
SplitLeft,
@@ -3,14 +3,14 @@ use anyhow::{anyhow, Result};
use call::{ActiveCall, ParticipantLocation};
use collections::HashMap;
use gpui::{
- point, size, AnyView, AnyWeakView, Axis, Bounds, Entity as _, IntoElement, Model, Pixels,
+ point, size, AnyView, AnyWeakView, Axis, Bounds, IntoElement, Model, MouseButton, Pixels,
Point, View, ViewContext,
};
use parking_lot::Mutex;
use project::Project;
use serde::Deserialize;
use std::sync::Arc;
-use ui::{prelude::*, Button};
+use ui::prelude::*;
pub const HANDLE_HITBOX_SIZE: f32 = 4.0;
const HORIZONTAL_MIN_SIZE: f32 = 80.;
@@ -183,6 +183,7 @@ impl Member {
let mut leader_border = None;
let mut leader_status_box = None;
+ let mut leader_join_data = None;
if let Some(leader) = &leader {
let mut leader_color = cx
.theme()
@@ -199,44 +200,21 @@ impl Member {
if Some(leader_project_id) == project.read(cx).remote_id() {
None
} else {
- let leader_user = leader.user.clone();
- let leader_user_id = leader.user.id;
- Some(
- Button::new(
- ("leader-status", pane.entity_id()),
- format!(
- "Follow {} to their active project",
- leader_user.github_login,
- ),
- )
- .on_click(cx.listener(
- move |this, _, cx| {
- crate::join_remote_project(
- leader_project_id,
- leader_user_id,
- this.app_state().clone(),
- cx,
- )
- .detach_and_log_err(cx);
- },
- )),
- )
+ leader_join_data = Some((leader_project_id, leader.user.id));
+ Some(Label::new(format!(
+ "Follow {} to their active project",
+ leader.user.github_login,
+ )))
}
}
- ParticipantLocation::UnsharedProject => Some(Button::new(
- ("leader-status", pane.entity_id()),
- format!(
- "{} is viewing an unshared Zed project",
- leader.user.github_login
- ),
- )),
- ParticipantLocation::External => Some(Button::new(
- ("leader-status", pane.entity_id()),
- format!(
- "{} is viewing a window outside of Zed",
- leader.user.github_login
- ),
- )),
+ ParticipantLocation::UnsharedProject => Some(Label::new(format!(
+ "{} is viewing an unshared Zed project",
+ leader.user.github_login
+ ))),
+ ParticipantLocation::External => Some(Label::new(format!(
+ "{} is viewing a window outside of Zed",
+ leader.user.github_login
+ ))),
};
}
@@ -263,8 +241,27 @@ impl Member {
.w_96()
.bottom_3()
.right_3()
+ .elevation_2(cx)
+ .p_1()
.z_index(1)
- .child(status_box),
+ .child(status_box)
+ .when_some(
+ leader_join_data,
+ |this, (leader_project_id, leader_user_id)| {
+ this.cursor_pointer().on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |this, _, cx| {
+ crate::join_remote_project(
+ leader_project_id,
+ leader_user_id,
+ this.app_state().clone(),
+ cx,
+ )
+ .detach_and_log_err(cx);
+ }),
+ )
+ },
+ ),
)
})
.into_any()
@@ -25,7 +25,7 @@ use futures::{
Future, FutureExt, StreamExt,
};
use gpui::{
- actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView,
+ actions, canvas, div, impl_actions, point, px, size, Action, AnyElement, AnyModel, AnyView,
AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow, Bounds, Context,
Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId, ManagedView, Model,
@@ -108,7 +108,6 @@ actions!(
NewCenterTerminal,
ToggleTerminalFocus,
NewSearch,
- DeploySearch,
Feedback,
Restart,
Welcome,
@@ -4303,6 +4302,10 @@ fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
Some(size((width as f64).into(), (height as f64).into()))
}
+pub fn titlebar_height(cx: &mut WindowContext) -> Pixels {
+ (1.75 * cx.rem_size()).max(px(32.))
+}
+
struct DisconnectedOverlay;
impl Element for DisconnectedOverlay {
@@ -4319,7 +4322,7 @@ impl Element for DisconnectedOverlay {
.bg(background)
.absolute()
.left_0()
- .top_0()
+ .top(titlebar_height(cx))
.size_full()
.flex()
.items_center()
@@ -29,6 +29,7 @@ command_palette = { path = "../command_palette" }
# component_test = { path = "../component_test" }
client = { path = "../client" }
# clock = { path = "../clock" }
+color = { path = "../color" }
copilot = { path = "../copilot" }
copilot_ui = { path = "../copilot_ui" }
diagnostics = { path = "../diagnostics" }