From 97047ffaca57bab47b64ea844d5de79be12c01f0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sun, 14 Jan 2024 13:58:12 -0700 Subject: [PATCH 01/37] Enable Channels for everyone --- crates/collab_ui/src/chat_panel.rs | 9 +- crates/collab_ui/src/collab_panel.rs | 196 +++++++++++----------- crates/collab_ui/src/collab_ui.rs | 5 - crates/feature_flags/src/feature_flags.rs | 6 - 4 files changed, 98 insertions(+), 118 deletions(-) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index c919ceac9fd679f734e3a72b2596af72d0191b56..cbcf8ffa0c61014be54aa14e2b1f8b314ba4e897 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -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 { - if !is_channels_feature_enabled(cx) { - return None; - } - Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button) } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index c9e1ae1bb8a9c28afeac1434e53cab55c1fc0041..9ff8e9da16b36b14af0bb144901a3fcd92007ad9 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -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, @@ -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::(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::() { - 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); } } } diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 779fd121f8afbfa59eac556c17e7abba605e8eee..6f4929c4d56ae5d806c62b289ed50e8bbab631d3 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -12,7 +12,6 @@ use std::{rc::Rc, sync::Arc}; use call::{report_call_event_for_room, ActiveCall, Room}; 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, @@ -126,7 +125,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::() -} diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 065d06f96d1b1765828329464ddb149da371d63b..ea16ff3f7291fd838be45fd826e7c7504ba914fd 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -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 { fn observe_flag(&mut self, callback: F) -> Subscription where From 8c9f3a7322ee2be43859e6cc7976e08433382c54 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 15 Jan 2024 17:01:07 -0500 Subject: [PATCH 02/37] init color crate --- Cargo.lock | 76 +++++++++++++++++++++++- crates/color/Cargo.toml | 32 +++++++++++ crates/color/src/color.rs | 118 ++++++++++++++++++++++++++++++++++++++ crates/zed/Cargo.toml | 1 + 4 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 crates/color/Cargo.toml create mode 100644 crates/color/src/color.rs diff --git a/Cargo.lock b/Cargo.lock index 056fab49cd576915fca5443922b0f094591237e1..1b0c6e4bb97ebc9fb8ce9615729fb370a5b18a34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" @@ -4976,6 +4998,7 @@ dependencies = [ "approx", "fast-srgb8", "palette_derive", + "phf", ] [[package]] @@ -5164,6 +5187,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" @@ -7073,6 +7138,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" @@ -7643,7 +7714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff" dependencies = [ "float-cmp", - "siphasher", + "siphasher 0.2.3", ] [[package]] @@ -8857,7 +8928,7 @@ dependencies = [ "roxmltree", "rustybuzz", "simplecss", - "siphasher", + "siphasher 0.2.3", "svgtypes", "ttf-parser 0.12.3", "unicode-bidi", @@ -9649,6 +9720,7 @@ dependencies = [ "client", "collab_ui", "collections", + "color", "command_palette", "copilot", "copilot_ui", diff --git a/crates/color/Cargo.toml b/crates/color/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..c6416f9691b3ca417c1f7426ace5359c199be88b --- /dev/null +++ b/crates/color/Cargo.toml @@ -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" diff --git a/crates/color/src/color.rs b/crates/color/src/color.rs new file mode 100644 index 0000000000000000000000000000000000000000..77818eb7b8f76c23cc9b05e31402ad04a89aeca3 --- /dev/null +++ b/crates/color/src/color.rs @@ -0,0 +1,118 @@ +//! # 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. +//! +//! **Note:** This crate does not depend on `gpui`, so it does not provide any +//! interfaces for converting to `gpui` style colors. + +use palette::{FromColor, Hsl, Hsla, Mix, Srgba, WithAlpha}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum BlendMode { + Multiply, + Screen, + Overlay, + Darken, + Lighten, + Dodge, + Burn, + HardLight, + SoftLight, + Difference, + Exclusion, +} + +/// Creates a new [`palette::Hsl`] color. +pub fn hsl(h: f32, s: f32, l: f32) -> Hsl { + Hsl::new_srgb(h, s, l) +} + +/// 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 { + 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::(), + 4 => { + let (rgb, alpha) = hex.split_at(3); + let rgb = rgb + .chars() + .map(|c| c.to_string().repeat(2)) + .collect::(); + 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 srgba = Srgba::new(r, g, b, a); + let hsl = Hsl::from_color(srgba); + let hsla = Hsla::from(hsl).with_alpha(a); + + Ok(hsla) +} + +/// Mixes two [`palette::Hsl`] colors at the given `mix_ratio`. +pub fn hsl_mix(hsla_1: Hsl, hsla_2: Hsl, mix_ratio: f32) -> Hsl { + hsla_1.mix(hsla_2, mix_ratio).into() +} + +/// Represents a color +/// An interstitial state used to provide a consistent API for colors +/// with additional functionality like color mixing, blending, etc. +/// +/// Does not return [gpui] colors as the `color` crate does not +/// depend on [gpui]. +#[derive(Debug, Copy, Clone)] +pub struct Color { + value: Hsla, +} + +impl Color { + /// Creates a new [`Color`] + pub fn new(hue: f32, saturation: f32, lightness: f32) -> Self { + let hsl = hsl(hue, saturation, lightness); + + Self { value: hsl.into() } + } + + /// Creates a new [`Color`] with an alpha value. + pub fn from_hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self { + Self { + value: Hsla::new(hue, saturation, lightness, alpha), + } + } + + /// Returns the [`palette::Hsla`] value of this color. + pub fn value(&self) -> Hsla { + self.value + } + + /// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`. + pub fn mix(&self, other: Hsl, mix_ratio: f32) -> Self { + let mixed = self.value.mix(other.into(), mix_ratio); + + Self { + value: mixed.into(), + } + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 734c225cb1e610c64dab92112accad9634632fee..1e21648408e4855e74cf4f411bc287345bac2bee 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -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" } From 62232060e6f029f0204cc159f0b3822ef5071a7a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Jan 2024 19:20:35 -0700 Subject: [PATCH 03/37] Close modals when focus leaves This is more similar to zed1's behaviour, and allows us to work around the difficulty in defining `on_mouse_down_out` for modals that have overlays. --- crates/workspace/src/modal_layer.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/workspace/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs index d940f1d16842a712bb8aaef23281c5e8ab06217f..ded0ef1970edb58460f507afbf458d037b0f515f 100644 --- a/crates/workspace/src/modal_layer.rs +++ b/crates/workspace/src/modal_layer.rs @@ -27,7 +27,7 @@ impl ModalViewHandle for View { pub struct ActiveModal { modal: Box, - _subscription: Subscription, + _subscriptions: [Subscription; 2], previous_focus_handle: Option, 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(); @@ -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())), ) } } From 036e637208525b477d6ed5c3bddab4fd49e737a1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Jan 2024 19:31:01 -0700 Subject: [PATCH 04/37] Disallow self-management for admins --- crates/collab_ui/src/collab_panel/channel_modal.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index c207e31bbeaa2b087578860c09ae176827074388..11890bcbe6d3dace458827583c13d331c6670e7b 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -348,6 +348,10 @@ impl PickerDelegate for ChannelModalDelegate { fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { 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 { 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( From 18739477f7b490742714c675cf6afe170abf0f7a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Jan 2024 20:12:20 -0700 Subject: [PATCH 05/37] Clippy --- crates/workspace/src/modal_layer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workspace/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs index ded0ef1970edb58460f507afbf458d037b0f515f..c30ca35a68578cc3c8a77f4bbf3fa702291181fa 100644 --- a/crates/workspace/src/modal_layer.rs +++ b/crates/workspace/src/modal_layer.rs @@ -114,7 +114,7 @@ impl ModalLayer { } impl Render for ModalLayer { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { let Some(active_modal) = &self.active_modal else { return div(); }; From bdb06f183b09a15340c9a83221432d54fc10a41a Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 16 Jan 2024 00:07:06 -0500 Subject: [PATCH 06/37] Add a rudimentary state color builder --- Cargo.lock | 1 + crates/color/src/color.rs | 57 +++++++++++++++++++++++++++++++++------ crates/theme/Cargo.toml | 1 + crates/theme/src/theme.rs | 7 +++++ 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b0c6e4bb97ebc9fb8ce9615729fb370a5b18a34..728fc695ac0891bb20f06a70d6c35ee2dfe65876 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7925,6 +7925,7 @@ name = "theme" version = "0.1.0" dependencies = [ "anyhow", + "color", "fs", "gpui", "indexmap 1.9.3", diff --git a/crates/color/src/color.rs b/crates/color/src/color.rs index 77818eb7b8f76c23cc9b05e31402ad04a89aeca3..8a7f3f17525cd097dfb4072c43b83675f54323c3 100644 --- a/crates/color/src/color.rs +++ b/crates/color/src/color.rs @@ -88,25 +88,28 @@ pub struct Color { } impl Color { - /// Creates a new [`Color`] - pub fn new(hue: f32, saturation: f32, lightness: f32) -> Self { - let hsl = hsl(hue, saturation, lightness); - - Self { value: hsl.into() } - } - /// Creates a new [`Color`] with an alpha value. - pub fn from_hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self { + pub fn new(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self { Self { value: Hsla::new(hue, saturation, lightness, alpha), } } + /// Creates a new [`Color`] with an alpha value of `1.0`. + pub fn hsl(hue: f32, saturation: f32, lightness: f32) -> Self { + Self::new(hue, saturation, lightness, 1.0) + } + /// Returns the [`palette::Hsla`] value of this color. pub fn value(&self) -> Hsla { self.value } + /// 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 mix(&self, other: Hsl, mix_ratio: f32) -> Self { let mixed = self.value.mix(other.into(), mix_ratio); @@ -116,3 +119,41 @@ impl Color { } } } + +/// A set of colors for different states of an element. +#[derive(Debug, Copy, Clone)] +pub struct ColorStates { + /// The default color. + pub default: Color, + /// The color when the mouse is hovering over the element. + pub hover: Color, + /// The color when the mouse button is held down on the element. + pub active: Color, + /// The color when the element is focused with the keyboard. + pub focused: Color, + /// The color when the element is disabled. + pub disabled: Color, +} + +/// Returns a set of colors for different states of an element. +/// +/// todo!("Test and improve this function") +pub fn states_for_color(color: Color, is_light: bool) -> ColorStates { + let hover_lightness = if is_light { 0.9 } else { 0.1 }; + let active_lightness = if is_light { 0.8 } else { 0.2 }; + let focused_lightness = if is_light { 0.7 } else { 0.3 }; + let disabled_lightness = if is_light { 0.6 } else { 0.5 }; + + let hover = color.mix(hsl(0.0, 0.0, hover_lightness), 0.1); + let active = color.mix(hsl(0.0, 0.0, active_lightness), 0.1); + let focused = color.mix(hsl(0.0, 0.0, focused_lightness), 0.1); + let disabled = color.mix(hsl(0.0, 0.0, disabled_lightness), 0.1); + + ColorStates { + default: color, + hover, + active, + focused, + disabled, + } +} diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index 1c30176b25b41130b79ccbc8879167fe34275895..428bcaac10b76501c78a772652f44370d4a74156 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -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] diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f8d90b7bdc823b0b52348fe94908454002616347..c40d7c8ceb2a1d99b1e7b97f7efb0b18b9bea48f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -147,3 +147,10 @@ pub fn color_alpha(color: Hsla, alpha: f32) -> Hsla { color.a = alpha; color } + +pub fn to_gpui_hsla(color: color::Color) -> gpui::Hsla { + let hsla = color.value(); + let hue: f32 = hsla.hue.into(); + + gpui::hsla(hue / 360.0, hsla.saturation, hsla.lightness, hsla.alpha) +} From dde0056845d31e1bcc60feb28e63622502017e0a Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 16 Jan 2024 01:08:17 -0500 Subject: [PATCH 07/37] Use srgb, get mix and blend working --- crates/color/src/color.rs | 160 +++++++++++++++++++++++++------------- crates/theme/src/theme.rs | 7 -- 2 files changed, 107 insertions(+), 60 deletions(-) diff --git a/crates/color/src/color.rs b/crates/color/src/color.rs index 8a7f3f17525cd097dfb4072c43b83675f54323c3..d3d832099af16d85f2afd0e5e6551bca966618c6 100644 --- a/crates/color/src/color.rs +++ b/crates/color/src/color.rs @@ -7,7 +7,9 @@ //! **Note:** This crate does not depend on `gpui`, so it does not provide any //! interfaces for converting to `gpui` style colors. -use palette::{FromColor, Hsl, Hsla, Mix, Srgba, WithAlpha}; +use palette::{ + blend::Blend, convert::FromColorUnclamped, encoding, rgb::Rgb, Clamp, Mix, Srgb, WithAlpha, +}; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum BlendMode { @@ -24,16 +26,11 @@ pub enum BlendMode { Exclusion, } -/// Creates a new [`palette::Hsl`] color. -pub fn hsl(h: f32, s: f32, l: f32) -> Hsl { - Hsl::new_srgb(h, s, l) -} - /// 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 { +pub fn hex_to_hsla(s: &str) -> Result { let hex = s.trim_start_matches('#'); // Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA @@ -64,64 +61,112 @@ pub fn hex_to_hsla(s: &str) -> Result { let b = ((hex_val >> 8) & 0xFF) as f32 / 255.0; let a = (hex_val & 0xFF) as f32 / 255.0; - let srgba = Srgba::new(r, g, b, a); - let hsl = Hsl::from_color(srgba); - let hsla = Hsla::from(hsl).with_alpha(a); + let color = Color { r, g, b, a }; - Ok(hsla) + Ok(color) } -/// Mixes two [`palette::Hsl`] colors at the given `mix_ratio`. -pub fn hsl_mix(hsla_1: Hsl, hsla_2: Hsl, mix_ratio: f32) -> Hsl { - hsla_1.mix(hsla_2, mix_ratio).into() +// This implements conversion to and from all Palette colors. +#[derive(FromColorUnclamped, WithAlpha, Debug, Clone)] +// We have to tell Palette that we will take care of converting to/from sRGB. +#[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")] +pub struct Color { + r: f32, + g: f32, + b: f32, + // Let Palette know this is our alpha channel. + #[palette(alpha)] + a: f32, } -/// Represents a color -/// An interstitial state used to provide a consistent API for colors -/// with additional functionality like color mixing, blending, etc. -/// -/// Does not return [gpui] colors as the `color` crate does not -/// depend on [gpui]. -#[derive(Debug, Copy, Clone)] -pub struct Color { - value: Hsla, +// There's no blanket implementation for Self -> Self, unlike the From trait. +// This is to better allow cases like Self -> Self. +impl FromColorUnclamped for Color { + fn from_color_unclamped(color: Color) -> Color { + color + } } -impl Color { - /// Creates a new [`Color`] with an alpha value. - pub fn new(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self { - Self { - value: Hsla::new(hue, saturation, lightness, alpha), +// Convert from any kind of f32 sRGB. +impl FromColorUnclamped> for Color +where + Srgb: FromColorUnclamped>, +{ + fn from_color_unclamped(color: Rgb) -> Color { + let srgb = Srgb::from_color_unclamped(color); + Color { + r: srgb.red, + g: srgb.green, + b: srgb.blue, + a: 1.0, } } +} + +// Convert into any kind of f32 sRGB. +impl FromColorUnclamped for Rgb +where + Rgb: FromColorUnclamped, +{ + fn from_color_unclamped(color: Color) -> Self { + let srgb = Srgb::new(color.r, color.g, color.b); + Self::from_color_unclamped(srgb) + } +} - /// Creates a new [`Color`] with an alpha value of `1.0`. - pub fn hsl(hue: f32, saturation: f32, lightness: f32) -> Self { - Self::new(hue, saturation, lightness, 1.0) +// Add the required clamping. +impl Clamp for Color { + fn clamp(self) -> Self { + Color { + 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), + } } +} - /// Returns the [`palette::Hsla`] value of this color. - pub fn value(&self) -> Hsla { - self.value +impl Color { + pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self { + Color { 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) + 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 mix(&self, other: Hsl, mix_ratio: f32) -> Self { - let mixed = self.value.mix(other.into(), mix_ratio); + pub fn mixed(&self, other: Color, 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); + Color::from_color_unclamped(mixed) + } + + pub fn blend(&self, other: Color, 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 { - value: mixed.into(), + r: blended.red, + g: blended.green, + b: blended.blue, + a: self.a, } } } /// A set of colors for different states of an element. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] pub struct ColorStates { /// The default color. pub default: Color, @@ -139,21 +184,30 @@ pub struct ColorStates { /// /// todo!("Test and improve this function") pub fn states_for_color(color: Color, is_light: bool) -> ColorStates { - let hover_lightness = if is_light { 0.9 } else { 0.1 }; - let active_lightness = if is_light { 0.8 } else { 0.2 }; - let focused_lightness = if is_light { 0.7 } else { 0.3 }; - let disabled_lightness = if is_light { 0.6 } else { 0.5 }; + 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: Color, adjustment: f32| -> Color { + // Adjust lightness for each state + // Note: Adjustment logic may differ; simplify as needed for sRGB + Color::new( + color.r * adjustment, + color.g * adjustment, + color.b * adjustment, + color.a, + ) + }; - let hover = color.mix(hsl(0.0, 0.0, hover_lightness), 0.1); - let active = color.mix(hsl(0.0, 0.0, active_lightness), 0.1); - let focused = color.mix(hsl(0.0, 0.0, focused_lightness), 0.1); - let disabled = color.mix(hsl(0.0, 0.0, disabled_lightness), 0.1); + let color = color.clamp(); ColorStates { - default: color, - hover, - active, - focused, - disabled, + 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), } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c40d7c8ceb2a1d99b1e7b97f7efb0b18b9bea48f..f8d90b7bdc823b0b52348fe94908454002616347 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -147,10 +147,3 @@ pub fn color_alpha(color: Hsla, alpha: f32) -> Hsla { color.a = alpha; color } - -pub fn to_gpui_hsla(color: color::Color) -> gpui::Hsla { - let hsla = color.value(); - let hue: f32 = hsla.hue.into(); - - gpui::hsla(hue / 360.0, hsla.saturation, hsla.lightness, hsla.alpha) -} From 1cbdf2ba22b064e53372c5eda56a75caaf49718e Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 16 Jan 2024 16:46:24 +0100 Subject: [PATCH 08/37] Fix overlapping block headers when using custom line height This fixes block headers overlapping over text in the buffer when using a custom line height of 1.25. It fixes the issue by making the parent container a v-flex, vertically-justifying the content and moving from relative padding to absolute padding for the header itself. Co-authored-by: antonio Co-authored-by: julia Co-authored-by: marshall --- crates/editor/src/element.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 895df153406934f9d38dca0d25994baf6f69adb6..b82bd55bcf5e898299ca8a3a0e1d88cf37c72367 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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| { From 62f5becf1fff6a3921bef734c92312a6c205bd54 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 16 Jan 2024 16:51:08 +0100 Subject: [PATCH 09/37] Fix rustfmt by pulling out long string into constant --- crates/search/src/project_search.rs | 67 +++++++++++++++-------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 3827b46c67fc29c96c5b361440fb66143c96db1a..0572b6eab051c3c8baaacdd366ab264126f53d0c 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -282,6 +282,8 @@ impl EventEmitter for ProjectSearchView {} impl Render for ProjectSearchView { fn render(&mut self, cx: &mut ViewContext) -> 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 +305,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); From 4f25df6ce263b5ea770f2a51d1534b7dd5dc81bf Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 16 Jan 2024 11:22:14 -0500 Subject: [PATCH 10/37] Prevent div background/content/border from interleaving at same z-index Co-Authored-By: Antonio Scandurra --- crates/gpui/src/style.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 9f46d8dab6fd6214b0f22cf0b4e616300d595e8a..8fdb926b27ebc8e1ab6b4531a5902e0395f53f8b 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -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(); From 60b79ef2ea90f294ee905b24612d3873da96a889 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 16 Jan 2024 11:23:28 -0500 Subject: [PATCH 11/37] Prevent content mask breaks from having the same z-index Co-Authored-By: Antonio Scandurra --- crates/gpui/src/window.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 869d6b18268cc64bc18f9187dd34e8032237d28b..0269ccfb6c8ef55df1696157302f6156e041f744 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -315,6 +315,7 @@ pub(crate) struct Frame { pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds)>, pub(crate) z_index_stack: StackingOrder, pub(crate) next_stacking_order_id: u32, + next_root_z_index: u8, content_mask_stack: Vec>, element_offset_stack: Vec>, requested_input_handler: Option, @@ -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 + BorrowMut { }; 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(); From 52267a5dec084535e47fcfc9abd2a5d3fe03dd93 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 16 Jan 2024 10:06:48 -0800 Subject: [PATCH 12/37] Adjust project search behavior to be isolated to a pane --- crates/search/src/project_search.rs | 76 ++++++++++++++++++----------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8897ae4bcfcd2ec2f14266dac81d97e8cf161985..a059b43f35cb939b504aea2253ea5dd7403dfadb 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -54,14 +54,10 @@ actions!( [SearchInNew, ToggleFocus, NextField, ToggleFilters] ); -#[derive(Default)] -struct ActiveSearches(HashMap, WeakView>); - #[derive(Default)] struct ActiveSettings(HashMap, 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 @@ -943,25 +939,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, ) { - let active_search = cx - .global::() - .0 - .get(&workspace.project().downgrade()); - let existing = active_search - .and_then(|active_search| { - workspace - .items_of_type::(cx) - .filter(|search| &search.downgrade() == active_search) - .last() - }) - .or_else(|| workspace.item_of_type::(cx)); + let existing = workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()); + Self::existing_or_new_search(workspace, existing, cx) } @@ -979,11 +969,6 @@ impl ProjectSearchView { existing: Option>, cx: &mut ViewContext, ) { - // 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::(cx)?; let query = editor.query_suggestion(cx); @@ -1015,6 +1000,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); @@ -3113,6 +3099,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", @@ -3171,6 +3158,8 @@ pub mod tests { } }) .unwrap(); + + // Add a project search item to the second pane window .update(cx, { let search_bar = search_bar.clone(); @@ -3190,6 +3179,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); @@ -3208,20 +3199,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::() + .is_some()); + + assert_eq!(pane.items_len(), 2); }); }) .unwrap(); @@ -3231,7 +3249,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); From 36ff35fcca754d101530c5dd5fd22a09f4d1a734 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 16 Jan 2024 10:30:21 -0800 Subject: [PATCH 13/37] Change name of deploy action to indicate what scope it operates at --- assets/keymaps/default.json | 2 +- crates/workspace/src/pane.rs | 1 + crates/workspace/src/workspace.rs | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 1372f7a4157da475aa8ccbff8aeb940c85d2ea79..f18cc2a111ec432a283f9f08f3fd1acecdddda52 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -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", diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 1b95671398a613a01f41590bdce20e0a846592d0..5ed68580877a7347f1df00e355b811054ba7fdb0 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -99,6 +99,7 @@ actions!( CloseItemsToTheLeft, CloseItemsToTheRight, GoBack, + DeploySearch, GoForward, ReopenClosedItem, SplitLeft, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index efd2c52989edb51cff2559382f0ec62a2ce2702e..4a8e87db1004345f3a4138c435644cd1722ee364 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -108,7 +108,6 @@ actions!( NewCenterTerminal, ToggleTerminalFocus, NewSearch, - DeploySearch, Feedback, Restart, Welcome, From ca4a8b22261a54e8e6e5db3c2e6e7fb5f7dbcb45 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 16 Jan 2024 14:05:05 -0500 Subject: [PATCH 14/37] Rework `Avatar` indicator to be more general-purpose (#4073) This PR reworks the way we add indicators to `Avatar`s to make them more general-purpose. Previously we had logic specific to the availability indicator embedded in the `Avatar` component, which made it unwieldy to repurpose for something else. Now the `indicator` is just a slot that we can put anything into. Release Notes: - N/A --- crates/collab_ui/src/collab_panel.rs | 84 ++++++----- crates/ui/src/components/avatar.rs | 138 +----------------- crates/ui/src/components/avatar/avatar.rs | 122 ++++++++++++++++ .../avatar/avatar_availability_indicator.rs | 48 ++++++ crates/ui/src/components/stories/avatar.rs | 6 +- 5 files changed, 222 insertions(+), 176 deletions(-) create mode 100644 crates/ui/src/components/avatar/avatar.rs create mode 100644 crates/ui/src/components/avatar/avatar_availability_indicator.rs diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 9cb447153c1bc39b9d291a2e17020cae788b43ca..ec94838386029b69c9feeea4f1879ea792e56bc7 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -31,8 +31,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::{ @@ -2000,43 +2000,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::(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()) diff --git a/crates/ui/src/components/avatar.rs b/crates/ui/src/components/avatar.rs index a97adb73b7d88ae0dfb1c60c25eff3942c5ad52d..b200828ce6cd5a4ac190faf65f8417e08844cf43 100644 --- a/crates/ui/src/components/avatar.rs +++ b/crates/ui/src/components/avatar.rs @@ -1,135 +1,5 @@ -use crate::prelude::*; -use gpui::{img, Hsla, ImageSource, Img, IntoElement, Styled}; +mod avatar; +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, - border_color: Option, - is_available: Option, -} - -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) -> 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) -> Self { - self.border_color = Some(color.into()); - self - } - - pub fn availability_indicator(mut self, is_available: impl Into>) -> 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>) -> Self { - self.size = size.into(); - self - } -} +pub use avatar::*; +pub use avatar_availability_indicator::*; diff --git a/crates/ui/src/components/avatar/avatar.rs b/crates/ui/src/components/avatar/avatar.rs new file mode 100644 index 0000000000000000000000000000000000000000..22ba255a60f12742e173f016886ce25b404d2c0f --- /dev/null +++ b/crates/ui/src/components/avatar/avatar.rs @@ -0,0 +1,122 @@ +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, + border_color: Option, + indicator: Option, +} + +impl Avatar { + pub fn new(src: impl Into) -> 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) -> 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>) -> Self { + self.size = size.into(); + self + } + + pub fn indicator(mut self, indicator: impl Into>) -> 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 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.indicator + .map(|indicator| div().z_index(1).child(indicator)), + ) + } +} diff --git a/crates/ui/src/components/avatar/avatar_availability_indicator.rs b/crates/ui/src/components/avatar/avatar_availability_indicator.rs new file mode 100644 index 0000000000000000000000000000000000000000..3a033cd3959c5d63af5960ba839fb37950fbec57 --- /dev/null +++ b/crates/ui/src/components/avatar/avatar_availability_indicator.rs @@ -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, +} + +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>) -> 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, + }) + } +} diff --git a/crates/ui/src/components/stories/avatar.rs b/crates/ui/src/components/stories/avatar.rs index e447486d69ca4a22a10e3d8975546b4ef661cf0d..c7aae8a73cde4098dca874754c40dd994a1da934 100644 --- a/crates/ui/src/components/stories/avatar.rs +++ b/crates/ui/src/components/stories/avatar.rs @@ -1,8 +1,8 @@ use gpui::Render; use story::Story; -use crate::prelude::*; use crate::Avatar; +use crate::{prelude::*, Availability, AvatarAvailabilityIndicator}; pub struct AvatarStory; @@ -19,11 +19,11 @@ impl Render for AvatarStory { )) .child( Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") - .availability_indicator(true), + .indicator(AvatarAvailabilityIndicator::new(Availability::Free)), ) .child( Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") - .availability_indicator(false), + .indicator(AvatarAvailabilityIndicator::new(Availability::Busy)), ) } } From f011953484ae54eed421f6093fce65dd41cd9206 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 16 Jan 2024 20:12:16 +0100 Subject: [PATCH 15/37] Rename all_font_families to all_font_names --- crates/gpui/src/platform.rs | 2 +- crates/gpui/src/platform/mac/text_system.rs | 18 +++++++++++------- crates/gpui/src/text_system.rs | 5 ++--- crates/theme/src/settings.rs | 2 +- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 5a2335919ebe6ff9b2277e02d4f6f55b4dc9a80c..f165cd9c2b5aa71a3a982a5e23faafaca2b8bf3a 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -192,7 +192,7 @@ pub trait PlatformDispatcher: Send + Sync { pub trait PlatformTextSystem: Send + Sync { fn add_fonts(&self, fonts: &[Arc>]) -> Result<()>; - fn all_font_families(&self) -> Vec; + fn all_font_names(&self) -> Vec; fn font_id(&self, descriptor: &Font) -> Result; fn font_metrics(&self, font_id: FontId) -> FontMetrics; fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index 68f4a63326757f8a87217a133aef005d0c8e5c86..06179e126b6b779a56d25b1bf154eef37162a646 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -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 { - self.0 - .read() - .system_source - .all_families() - .expect("core text should never return an error") + fn all_font_names(&self) -> Vec { + 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 { diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 24438d8c819527a3e8dd869f9e1dac179d9dd618..34470aff021297d9b8a12025e5348550cd6111c5 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -65,8 +65,8 @@ impl TextSystem { } } - pub fn all_font_families(&self) -> Vec { - let mut families = self.platform_text_system.all_font_families(); + pub fn all_font_names(&self) -> Vec { + 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; diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index efc62ed59c429b97420fc276d1f2e2eca6c841cc..90c61ce269019e405ca825fb1bd6d30597fd54cf 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -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(); From ff67d9dea033169bef0a92d8c00a7156fc3936c6 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 16 Jan 2024 20:19:58 +0100 Subject: [PATCH 16/37] Add font name completions to ui_font_family and terminal::font_family --- Cargo.lock | 1 + crates/terminal/Cargo.toml | 1 + crates/terminal/src/terminal_settings.rs | 41 +++++++++++++++++++++++- crates/theme/src/settings.rs | 4 +++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 7b1fe5144c5b95c0adc0c02a2c28dd3cec44fa09..42416c2f2458f52a2ccef25e9789e144d73ad1fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7775,6 +7775,7 @@ dependencies = [ "schemars", "serde", "serde_derive", + "serde_json", "settings", "shellexpand", "smallvec", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index ce2a158e2076a852addcef71afc2644cdd903531..4dc253fd5343b8530fed48b6e6f17400dcb82bca 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -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 diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 14cff3b5a690a6bc370579604305ff5a1217a351..b9b79c9c6b6996e674f1af25485343a093b3fe85 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -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::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::(); + 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)] diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 90c61ce269019e405ca825fb1bd6d30597fd54cf..bced187411594b5979245103c6f99c1aedcd7fa5 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -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 From 9903b7ae6ecd02f0ebbfb138ab0d904adb83d7ec Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 16 Jan 2024 15:17:29 -0500 Subject: [PATCH 17/37] Add color ribbon for local player (#4075) This PR adds a color ribbon for the local player in the current call. This fixes the alignment of the local user's avatar so that it lines up with the rest of the collaborators in the call: Screenshot 2024-01-16 at 2 56 04 PM Release Notes: - Added a color ribbon for the local player when in a call. --- crates/collab_ui/src/collab_titlebar_item.rs | 24 +++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 432f8f6cd242b3b7bbeb53464de98980b4079e47..72b16d49185f6928ca9195c6159e7395add273d2 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,7 +1,7 @@ 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, InteractiveElement, IntoElement, Model, ParentElement, Path, Render, @@ -12,7 +12,7 @@ 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, @@ -97,7 +97,7 @@ impl Render for CollabTitlebarItem { room.remote_participants().values().collect::>(); 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 +107,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 +138,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 +321,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; From 2e03c848e31257ddae10dcbe6d5be793d6394966 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 16 Jan 2024 16:18:06 -0500 Subject: [PATCH 18/37] Add dedicated indicator for showing a muted call participant (#4076) This PR improves the muted indicators to make it clearer when a call participant is muted. Previously we used a red border color to denote when a participant was muted. Now we render an indicator with an icon to more clearly indicate the participant's muted status: Screenshot 2024-01-16 at 4 05 15 PM Hovering over the indicator will display a tooltip for further explanation: Screenshot 2024-01-16 at 4 05 25 PM This change also paves the way for denoting the deafened status for call participants. Release Notes: - Improved the mute indicator for call participants. --- crates/collab_ui/src/collab_titlebar_item.rs | 20 +++--- crates/ui/src/components/avatar.rs | 2 + crates/ui/src/components/avatar/avatar.rs | 1 + .../avatar/avatar_audio_status_indicator.rs | 65 +++++++++++++++++++ crates/ui/src/components/icon.rs | 2 + crates/ui/src/components/stories/avatar.rs | 12 +++- 6 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 crates/ui/src/components/avatar/avatar_audio_status_indicator.rs diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 72b16d49185f6928ca9195c6159e7395add273d2..9f11870d983a20013666ad61c5a6cd1bd30bdba3 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -14,8 +14,8 @@ use rpc::proto; use std::sync::Arc; 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}; @@ -486,12 +486,16 @@ impl CollabTitlebarItem { .child( Avatar::new(user.avatar_uri.clone()) .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() + .when(is_speaking, |avatar| { + avatar.border_color(cx.theme().status().info_border) + }) + .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().filter_map(|follower_peer_id| { diff --git a/crates/ui/src/components/avatar.rs b/crates/ui/src/components/avatar.rs index b200828ce6cd5a4ac190faf65f8417e08844cf43..6c2d88916e7fe47f9fbcfe86b7d88ebd43e4b62f 100644 --- a/crates/ui/src/components/avatar.rs +++ b/crates/ui/src/components/avatar.rs @@ -1,5 +1,7 @@ mod avatar; +mod avatar_audio_status_indicator; mod avatar_availability_indicator; pub use avatar::*; +pub use avatar_audio_status_indicator::*; pub use avatar_availability_indicator::*; diff --git a/crates/ui/src/components/avatar/avatar.rs b/crates/ui/src/components/avatar/avatar.rs index 22ba255a60f12742e173f016886ce25b404d2c0f..5154d90bd2e38718d003ad167a4eabb7a5852479 100644 --- a/crates/ui/src/components/avatar/avatar.rs +++ b/crates/ui/src/components/avatar/avatar.rs @@ -1,4 +1,5 @@ use crate::prelude::*; + use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled}; /// The shape of an [`Avatar`]. diff --git a/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs b/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs new file mode 100644 index 0000000000000000000000000000000000000000..55f98d3db3b8a49f827ffac0d13d69c00dfc5d75 --- /dev/null +++ b/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs @@ -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 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(-1. / 16.)) + .right(rems(-4. / 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)) + }), + ) + } +} diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 908e76ef918b56aefff6949e86ea6473a272253d..bdc691dc9a26e178ad65eaaeda85caa7973dd526 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -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.), diff --git a/crates/ui/src/components/stories/avatar.rs b/crates/ui/src/components/stories/avatar.rs index c7aae8a73cde4098dca874754c40dd994a1da934..c3409b1ca81dfcf412111eb832a61b83c1a768bc 100644 --- a/crates/ui/src/components/stories/avatar.rs +++ b/crates/ui/src/components/stories/avatar.rs @@ -1,8 +1,8 @@ use gpui::Render; use story::Story; -use crate::Avatar; -use crate::{prelude::*, Availability, AvatarAvailabilityIndicator}; +use crate::{prelude::*, AudioStatus, Availability, AvatarAvailabilityIndicator}; +use crate::{Avatar, AvatarAudioStatusIndicator}; pub struct AvatarStory; @@ -25,5 +25,13 @@ impl Render for AvatarStory { Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") .indicator(AvatarAvailabilityIndicator::new(Availability::Busy)), ) + .child( + Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") + .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)), + ) + .child( + Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") + .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)), + ) } } From 4e8ad363f1cb419fa747483db5119b862cbe8593 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 16 Jan 2024 17:09:28 -0500 Subject: [PATCH 19/37] Increase border width used to indicate speaking (#4077) This PR increases the width of the border that we use to indicate when a call participant is speaking. This should make it more apparent in the UI when someone is speaking. Release Notes: - Increased the width of the ring used to indicate when someone is speaking in a call. --- crates/collab_ui/src/collab_titlebar_item.rs | 8 ++- crates/gpui_macros/src/style_helpers.rs | 14 ++++- crates/ui/src/components/avatar/avatar.rs | 15 +++-- crates/ui/src/components/stories/avatar.rs | 64 ++++++++++++++------ 4 files changed, 75 insertions(+), 26 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 9f11870d983a20013666ad61c5a6cd1bd30bdba3..077e08fac78960449824ddb3473e74f916b7577c 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -486,8 +486,12 @@ impl CollabTitlebarItem { .child( Avatar::new(user.avatar_uri.clone()) .grayscale(!is_present) - .when(is_speaking, |avatar| { - avatar.border_color(cx.theme().status().info_border) + .border_color(if is_speaking { + cx.theme().status().info_border + } 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( diff --git a/crates/gpui_macros/src/style_helpers.rs b/crates/gpui_macros/src/style_helpers.rs index b86bb2dfa60b7a97394cafe3fc4e1aaf22b87302..00d3672033fd9042d72ccff5c93b9e8b1f41055e 100644 --- a/crates/gpui_macros/src/style_helpers.rs +++ b/crates/gpui_macros/src/style_helpers.rs @@ -85,6 +85,18 @@ fn generate_methods() -> Vec { } 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, diff --git a/crates/ui/src/components/avatar/avatar.rs b/crates/ui/src/components/avatar/avatar.rs index 5154d90bd2e38718d003ad167a4eabb7a5852479..932cc9e243558fb78cbf6e15340974a0551699c9 100644 --- a/crates/ui/src/components/avatar/avatar.rs +++ b/crates/ui/src/components/avatar/avatar.rs @@ -99,20 +99,27 @@ impl RenderOnce for Avatar { self = self.shape(AvatarShape::Circle); } - let size = self.size.unwrap_or_else(|| cx.rem_size()); + 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(size + px(2.)) + .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().border_color(color) + this.border_width(border_width).border_color(color) }) .child( self.image - .size(size) + .size(image_size) .bg(cx.theme().colors().ghost_element_background), ) .children( diff --git a/crates/ui/src/components/stories/avatar.rs b/crates/ui/src/components/stories/avatar.rs index c3409b1ca81dfcf412111eb832a61b83c1a768bc..9da475b0d9be59d64580b4db416ed99afbb1e402 100644 --- a/crates/ui/src/components/stories/avatar.rs +++ b/crates/ui/src/components/stories/avatar.rs @@ -1,5 +1,5 @@ use gpui::Render; -use story::Story; +use story::{StoryContainer, StoryItem, StorySection}; use crate::{prelude::*, AudioStatus, Availability, AvatarAvailabilityIndicator}; use crate::{Avatar, AvatarAudioStatusIndicator}; @@ -7,31 +7,57 @@ use crate::{Avatar, AvatarAudioStatusIndicator}; pub struct AvatarStory; impl Render for AvatarStory { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - Story::container() - .child(Story::title_for::()) - .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) -> impl IntoElement { + StoryContainer::new("Avatar", "crates/ui/src/components/stories/avatar.rs") .child( - Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") - .indicator(AvatarAvailabilityIndicator::new(Availability::Free)), + 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") - .indicator(AvatarAvailabilityIndicator::new(Availability::Busy)), + 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( - Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") - .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)), + 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( - Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") - .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)), + 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)), + )), ) } } From cce3cf145cfb865bd1237873e6b30dc7bcb84d42 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Jan 2024 15:18:58 -0700 Subject: [PATCH 20/37] Play guess who's to blame --- crates/gpui/src/platform/mac/metal_renderer.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 20e749a2f607fa96ec6fbdfc76574307648a621d..36ded7ba8ecac63007e1e682caef624ee9766e3f 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -14,7 +14,6 @@ use foreign_types::ForeignType; use media::core_video::CVMetalTextureCache; use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange}; use objc::{self, msg_send, sel, sel_impl}; -use smallvec::SmallVec; use std::{ffi::c_void, mem, ptr, sync::Arc}; const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib")); @@ -538,7 +537,7 @@ impl MetalRenderer { ); let mut prev_texture_id = None; - let mut sprites = SmallVec::<[_; 1]>::new(); + let mut sprites = Vec::new(); let mut paths_and_tiles = paths .iter() .map(|path| (path, tiles_by_path_id.get(&path.id).unwrap())) From 00682b8903a8553b5d34b4f502afe6c50c027f23 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 16 Jan 2024 18:31:00 -0500 Subject: [PATCH 21/37] Do not reset timer for each reported event --- crates/client/src/telemetry.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 32cf9efba230da83f68d568818d20a52bfc862dd..102362c422013773c44fa79d67ec6baa0bc8e3a7 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -457,20 +457,24 @@ 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_DEBOUNCE_INTERVAL).await; + this.flush_events(); + })); + } + let signed_in = state.metrics_id.is_some(); state.events_queue.push(EventWrapper { signed_in, event }); + dbg!(&state.events_queue.len()); + if state.installation_id.is_some() { 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(); - })); } } } @@ -534,6 +538,7 @@ impl Telemetry { release_channel: state.release_channel, events, }; + dbg!("flush", &request_body); json_bytes.clear(); serde_json::to_writer(&mut json_bytes, &request_body)?; } From 0c59f510d2d539c6a5196b414d2dff57c7654dec Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 16 Jan 2024 18:33:43 -0500 Subject: [PATCH 22/37] Remove `dbg!()`s --- crates/client/src/telemetry.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 102362c422013773c44fa79d67ec6baa0bc8e3a7..8e8aa4942ca2f8a0ed950a14bcf5886ada14e6bd 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -469,8 +469,6 @@ impl Telemetry { let signed_in = state.metrics_id.is_some(); state.events_queue.push(EventWrapper { signed_in, event }); - dbg!(&state.events_queue.len()); - if state.installation_id.is_some() { if state.events_queue.len() >= MAX_QUEUE_LEN { drop(state); @@ -538,7 +536,6 @@ impl Telemetry { release_channel: state.release_channel, events, }; - dbg!("flush", &request_body); json_bytes.clear(); serde_json::to_writer(&mut json_bytes, &request_body)?; } From 54dcb1d33ca1dcf4a183c38cf3e0f33b87a63ce0 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 16 Jan 2024 18:34:52 -0500 Subject: [PATCH 23/37] Rename variable --- crates/client/src/telemetry.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 8e8aa4942ca2f8a0ed950a14bcf5886ada14e6bd..5ee039a8cb23c939351c4ff85283569fe4d0dd95 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -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, cx: &mut AppContext) -> Arc { @@ -461,7 +461,7 @@ impl Telemetry { 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; + executor.timer(FLUSH_INTERVAL).await; this.flush_events(); })); } From 26a3f68080f16dddba8ff9e480c065a41f1452f1 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 16 Jan 2024 18:49:37 -0500 Subject: [PATCH 24/37] Tweak mute indicator positioning (#4080) This PR tweaks the positioning of the mute indicators so that they cover a little bit less of the avatar: #### Before Screenshot 2024-01-16 at 6 32 51 PM #### After Screenshot 2024-01-16 at 6 26 48 PM (It's a bit hard to tell in the screenshot, but there is a gap between the bottom of the indicator and the top of the color ribbon). Release Notes: - N/A --- .../ui/src/components/avatar/avatar_audio_status_indicator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs b/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs index 55f98d3db3b8a49f827ffac0d13d69c00dfc5d75..943a8d4826110a020d3c35ba562651315ee53c3a 100644 --- a/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs +++ b/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs @@ -37,8 +37,8 @@ impl RenderOnce for AvatarAudioStatusIndicator { div() .absolute() - .bottom(rems(-1. / 16.)) - .right(rems(-4. / 16.)) + .bottom(rems(-3. / 16.)) + .right(rems(-6. / 16.)) .w(width_in_px + padding_x) .h(icon_size.rems()) .child( From 8be798d1c0980c8a614226ad2846802bb727b1c6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Jan 2024 19:47:59 -0700 Subject: [PATCH 25/37] Limit number of collaborators in local Facepiles --- crates/collab_ui/src/collab_titlebar_item.rs | 44 ++++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 077e08fac78960449824ddb3473e74f916b7577c..6bbb02c703c4327146b66c4b3aaa3815a1d8611e 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -3,7 +3,7 @@ use auto_update::AutoUpdateStatus; use call::{ActiveCall, ParticipantLocation, Room}; 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, @@ -480,7 +480,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( @@ -502,18 +504,34 @@ impl CollabTitlebarItem { ) }), ) - .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( + 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(if extra_count > 0 { + Some( + div() + .ml_1() + .child(Label::new(format!("+{extra_count}"))) + .into_any_element(), + ) + } else { + None + }); Some(pile) } From 1d5b237b642ed4049eb2066eebd3acd2acf7e6b4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Jan 2024 19:35:26 -0700 Subject: [PATCH 26/37] Allow leaving calls once project is unshared --- crates/collab_ui/src/collab_titlebar_item.rs | 9 +++------ crates/workspace/src/workspace.rs | 8 ++++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 077e08fac78960449824ddb3473e74f916b7577c..6c0718f41e62e89116dbe4129e48f4b93d1e6ea9 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -3,7 +3,7 @@ use auto_update::AutoUpdateStatus; use call::{ActiveCall, ParticipantLocation, Room}; 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, @@ -19,7 +19,7 @@ use ui::{ }; 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() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f2949f58e5d84233d4d2fc36ba8fc1141cacab0c..1d06db5de3b459b289d3b0392c3fb91ac594a760 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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, @@ -4302,6 +4302,10 @@ fn parse_pixel_size_env_var(value: &str) -> Option> { 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 { @@ -4318,7 +4322,7 @@ impl Element for DisconnectedOverlay { .bg(background) .absolute() .left_0() - .top_0() + .top(titlebar_height(cx)) .size_full() .flex() .items_center() From 97bd3e1fde26deed91f6874058a365b97f54a742 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 17 Jan 2024 09:45:46 +0100 Subject: [PATCH 27/37] Fix segfault caused by wrong size of path sprites bytes length Previously, we were using `size_of` but passing the wrong type in (MonochromeSprite instead of PathSprite). This caused us to read outside of the `sprites` smallvec and triggered the segfault. --- .../gpui/src/platform/mac/metal_renderer.rs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 36ded7ba8ecac63007e1e682caef624ee9766e3f..d3a32cc41d2c4b9d00d95a3b2292c7298a08f613 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -14,6 +14,7 @@ use foreign_types::ForeignType; use media::core_video::CVMetalTextureCache; use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange}; use objc::{self, msg_send, sel, sel_impl}; +use smallvec::SmallVec; use std::{ffi::c_void, mem, ptr, sync::Arc}; const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib")); @@ -81,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::()) as u64, + mem::size_of_val(&unit_vertices) as u64, MTLResourceOptions::StorageModeManaged, ); let instances = device.new_buffer( @@ -339,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::>(); + 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; } @@ -372,7 +374,6 @@ impl MetalRenderer { &texture_size as *const Size as *const _, ); - let vertices_bytes_len = mem::size_of::>() * vertices.len(); let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; unsafe { ptr::copy_nonoverlapping( @@ -429,7 +430,7 @@ impl MetalRenderer { &viewport_size as *const Size 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; @@ -490,7 +491,7 @@ impl MetalRenderer { &viewport_size as *const Size 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; @@ -537,7 +538,7 @@ impl MetalRenderer { ); let mut prev_texture_id = None; - let mut sprites = Vec::new(); + let mut sprites = SmallVec::<[_; 1]>::new(); let mut paths_and_tiles = paths .iter() .map(|path| (path, tiles_by_path_id.get(&path.id).unwrap())) @@ -590,7 +591,7 @@ impl MetalRenderer { command_encoder .set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); - let sprite_bytes_len = mem::size_of::() * 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; @@ -655,17 +656,17 @@ impl MetalRenderer { &viewport_size as *const Size 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) }; 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; + let next_offset = *offset + underline_bytes_len; if next_offset > INSTANCE_BUFFER_SIZE { return false; } @@ -726,7 +727,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; @@ -798,7 +799,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; From 9c337908099ec3c2033722af14245e4ff455e42c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 17 Jan 2024 09:50:55 +0100 Subject: [PATCH 28/37] Check if we exhausted the instance buffer prior to copying underlines This fixes another potential segfault. --- crates/gpui/src/platform/mac/metal_renderer.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index d3a32cc41d2c4b9d00d95a3b2292c7298a08f613..1589757d935894ff676de7371fca97204d1ee63a 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -658,6 +658,12 @@ impl MetalRenderer { 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, @@ -666,11 +672,6 @@ impl MetalRenderer { ); } - let next_offset = *offset + underline_bytes_len; - if next_offset > INSTANCE_BUFFER_SIZE { - return false; - } - command_encoder.draw_primitives_instanced( metal::MTLPrimitiveType::Triangle, 0, From 39dff0e82790c6d99b6c6a9362058d7b5749f63f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 17 Jan 2024 11:06:46 +0200 Subject: [PATCH 29/37] Stop using button for collab notifications --- crates/workspace/src/pane_group.rs | 74 ++++++++++++++---------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index ce58e51678c589c39e97666d71ccc097bc0a5324..b6e3b1a433d358db2856e060f140be0654c06baf 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -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 + ))), }; } @@ -264,7 +242,25 @@ impl Member { .bottom_3() .right_3() .z_index(1) - .child(status_box), + .bg(cx.theme().colors().element_background) + .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() From 04922d649ccfaf149fa77752ae3d862ccc92d38d Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 17 Jan 2024 11:07:20 +0100 Subject: [PATCH 30/37] Fix missing Ctrl-[ bindings in Vim mode This "adds" the keybindings I was missing in Vim mode (e.g. `Ctrl-[` to cancel a selection) by fixing the definitions in the keymap from `Ctrl+[` to `Ctrl-[`. --- assets/keymaps/vim.json | 8 ++++---- crates/vim/src/test.rs | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 81235bb72ad7075be27605c462fb03b822b69140..1da6f0ef8c5da25a60742fda933125c23eac81bf 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -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"] } }, { diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index b23c49c9a3fd4f23d1d547f7165df64cb33c1f75..fa2dcb45cda61f4637b5bcb6ac0cd1d43236def3 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -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; From 5b0b9ff5828251e8e85b698bfb0a4437d64e2df6 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 17 Jan 2024 13:50:55 +0100 Subject: [PATCH 31/37] Submit bigger primitive batches when rendering Before this change we wouldn't submit all possible primitives of the same kind that are less-than the max order. Result was that we would submit, say, 10 paths each in a separate batch instead of actually batching them. This was overly strict because even if the order of two different primitives was the same, we could have still batched the 1st primitive kind, if its implicit ordering was less than 2nd kind. Example: say we have the following primitives and these orders 5x paths, order 3 2x sprites, order 3 Previously, we would submit 1 path, 1 path, 1 path, 1 path, 1 path, then the sprites. With this changes, we batch the 5 paths into one batch. Co-authored-by: Antonio --- crates/gpui/src/scene.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 11341a2cbc524899a3455f490b20652e7f17fc4b..de031704cd2888917534083bffd90e00c7e8b49b 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -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; From 51127460b2ad235e8173d2e20f475a18a36f127f Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 17 Jan 2024 15:01:32 +0100 Subject: [PATCH 32/37] Remove memmove to improve terminal performance Co-authored-by: Antonio --- crates/terminal_view/src/terminal_element.rs | 109 +++++++++---------- 1 file changed, 53 insertions(+), 56 deletions(-) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 1fec041de9c633493993696cdc6fdda142355c77..6298b4c16a07b47054430a6e2dcacd9e4f6e033e 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -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, @@ -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) + } + }); } } From 977832a04ea03b2de94a6bb979e43f548cce46fc Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 17 Jan 2024 09:40:16 -0500 Subject: [PATCH 33/37] Refresh window, bypassing view cache, when opening hover or context menu --- crates/editor/src/hover_popover.rs | 1 + crates/ui/src/components/context_menu.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 8da2f50c198a01136d1318922a5159e3814a894f..609c20ac680819ff738f4a4f3ab08ffa58762146 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -339,6 +339,7 @@ fn show_hover( this.hover_state.info_popover = hover_popover; cx.notify(); + cx.refresh(); })?; Ok::<_, anyhow::Error>(()) diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 5c4f110a415b2e1e8ef0ac2d36b79d7d8bc2b4be..4b6837799976579b6bc469753c51cb964e49e7b0 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -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(), From a601e96b6cf4e2612c7903d7ae4ff7b7c2d0faa0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 17 Jan 2024 16:44:43 +0200 Subject: [PATCH 34/37] Style collab notifications properly --- crates/workspace/src/pane_group.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index b6e3b1a433d358db2856e060f140be0654c06baf..e631cd9c436b6918a974d2afdcc7ac9d4aa37520 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -241,8 +241,9 @@ impl Member { .w_96() .bottom_3() .right_3() + .elevation_2(cx) + .p_1() .z_index(1) - .bg(cx.theme().colors().element_background) .child(status_box) .when_some( leader_join_data, From 9c557aae9e6963b30748d25852d8a04c03521405 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Jan 2024 11:00:59 -0500 Subject: [PATCH 35/37] Fix regression of welcome screen background color (#4091) In #3910 we made the welcome screen use the same background color as the editor. However, this later regressed in cdd5cb16ed896b2ee3bbb041983ee7cb812f6991. This PR fixes that regression and restores the correct color for the welcome page. Release Notes: - Fixed the background color of the welcome screen. --- crates/welcome/src/welcome.rs | 361 +++++++++++++++++----------------- 1 file changed, 184 insertions(+), 177 deletions(-) diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 677b57a0225059430b141ca23573d42433f52b77..53b78b917fec1d43c8c194a1b1bf0ee8198fe2a5 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -60,190 +60,197 @@ pub struct WelcomePage { impl Render for WelcomePage { fn render(&mut self, cx: &mut gpui::ViewContext) -> 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::( + 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::( - 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::( - 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::( + 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::( - 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::( + 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")), + ), + ), + ) } } From 4cdcac1b16da71692802cf72c1b2abab41f5e58e Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 17 Jan 2024 11:39:09 -0500 Subject: [PATCH 36/37] Update docs --- crates/color/src/color.rs | 93 ++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/crates/color/src/color.rs b/crates/color/src/color.rs index d3d832099af16d85f2afd0e5e6551bca966618c6..8529f3bc5feea6b3248875e412914dc24b39a4b5 100644 --- a/crates/color/src/color.rs +++ b/crates/color/src/color.rs @@ -4,25 +4,49 @@ //! //! It is used to create a manipulate colors when building themes. //! -//! **Note:** This crate does not depend on `gpui`, so it does not provide any -//! interfaces for converting to `gpui` style colors. - +//! === 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, } @@ -30,7 +54,7 @@ pub enum BlendMode { /// /// This function supports the following hex formats: /// `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`. -pub fn hex_to_hsla(s: &str) -> Result { +pub fn hex_to_hsla(s: &str) -> Result { let hex = s.trim_start_matches('#'); // Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA @@ -61,16 +85,15 @@ pub fn hex_to_hsla(s: &str) -> Result { let b = ((hex_val >> 8) & 0xFF) as f32 / 255.0; let a = (hex_val & 0xFF) as f32 / 255.0; - let color = Color { r, g, b, a }; + let color = RGBAColor { r, g, b, a }; Ok(color) } -// This implements conversion to and from all Palette colors. +// These derives implement to and from palette's color types. #[derive(FromColorUnclamped, WithAlpha, Debug, Clone)] -// We have to tell Palette that we will take care of converting to/from sRGB. #[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")] -pub struct Color { +pub struct RGBAColor { r: f32, g: f32, b: f32, @@ -79,22 +102,19 @@ pub struct Color { a: f32, } -// There's no blanket implementation for Self -> Self, unlike the From trait. -// This is to better allow cases like Self -> Self. -impl FromColorUnclamped for Color { - fn from_color_unclamped(color: Color) -> Color { +impl FromColorUnclamped for RGBAColor { + fn from_color_unclamped(color: RGBAColor) -> RGBAColor { color } } -// Convert from any kind of f32 sRGB. -impl FromColorUnclamped> for Color +impl FromColorUnclamped> for RGBAColor where Srgb: FromColorUnclamped>, { - fn from_color_unclamped(color: Rgb) -> Color { + fn from_color_unclamped(color: Rgb) -> RGBAColor { let srgb = Srgb::from_color_unclamped(color); - Color { + RGBAColor { r: srgb.red, g: srgb.green, b: srgb.blue, @@ -103,21 +123,19 @@ where } } -// Convert into any kind of f32 sRGB. -impl FromColorUnclamped for Rgb +impl FromColorUnclamped for Rgb where Rgb: FromColorUnclamped, { - fn from_color_unclamped(color: Color) -> Self { + fn from_color_unclamped(color: RGBAColor) -> Self { let srgb = Srgb::new(color.r, color.g, color.b); Self::from_color_unclamped(srgb) } } -// Add the required clamping. -impl Clamp for Color { +impl Clamp for RGBAColor { fn clamp(self) -> Self { - Color { + 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), @@ -126,9 +144,12 @@ impl Clamp for Color { } } -impl Color { +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 { - Color { r, g, b, a } + RGBAColor { r, g, b, a } } /// Returns a set of states for this color. @@ -137,16 +158,16 @@ impl Color { } /// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`. - pub fn mixed(&self, other: Color, mix_ratio: f32) -> Self { + 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); - Color::from_color_unclamped(mixed) + RGBAColor::from_color_unclamped(mixed) } - pub fn blend(&self, other: Color, blend_mode: BlendMode) -> Self { + 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); @@ -169,31 +190,31 @@ impl Color { #[derive(Debug, Clone)] pub struct ColorStates { /// The default color. - pub default: Color, + pub default: RGBAColor, /// The color when the mouse is hovering over the element. - pub hover: Color, + pub hover: RGBAColor, /// The color when the mouse button is held down on the element. - pub active: Color, + pub active: RGBAColor, /// The color when the element is focused with the keyboard. - pub focused: Color, + pub focused: RGBAColor, /// The color when the element is disabled. - pub disabled: Color, + pub disabled: RGBAColor, } /// Returns a set of colors for different states of an element. /// -/// todo!("Test and improve this function") -pub fn states_for_color(color: Color, is_light: bool) -> ColorStates { +/// 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: Color, adjustment: f32| -> Color { + let make_adjustment = |color: RGBAColor, adjustment: f32| -> RGBAColor { // Adjust lightness for each state // Note: Adjustment logic may differ; simplify as needed for sRGB - Color::new( + RGBAColor::new( color.r * adjustment, color.g * adjustment, color.b * adjustment, From df67917768b4fef7e6a30472ecba4403622be8ce Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Jan 2024 11:47:43 -0500 Subject: [PATCH 37/37] Make channel buttons square (#4092) This PR makes the channel buttons square. Release Notes: - Adjusted the shape of the channel buttons. --- crates/collab_ui/src/collab_panel.rs | 4 +-- .../ui/src/components/button/icon_button.rs | 17 +++++++--- .../ui/src/components/stories/icon_button.rs | 31 +++++++++++++++++-- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 47c5463e921eee93bfc4d1032dd74808ffb21fe7..d6de5135711b7e56854eaa541afc6b23a3020544 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2314,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 @@ -2332,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 diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index 1e37a872922b4473616b551352f8100bbd9b1327..cc1e31b65cb0836be9a307107302ba8705597d6c 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -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( diff --git a/crates/ui/src/components/stories/icon_button.rs b/crates/ui/src/components/stories/icon_button.rs index df9f37b164782f35c9a2ca1cfba6aa96d8783d60..ba3d5fd8660988298a38307cb8a690d1a365c7f4 100644 --- a/crates/ui/src/components/stories/icon_button.rs +++ b/crates/ui/src/components/stories/icon_button.rs @@ -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() } }