From ce848375fe6511c2e167a9d002af8af4772bf08c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Fri, 13 Sep 2024 17:44:16 -0400 Subject: [PATCH] add `ui::Vector` and separate images from icons (#17815) This PR pulls non-icon assets out of `ui::components::icon` in preparation for icon standardization. In the future icons will have standard names and sizes, and these image assets won't conform to those constraints. We can also add a `ui::components::image::Image` wrapper around the `gpui::img` element in the future for any Zed-specific image styling we want to enforce. Of note: ```rust #[derive(Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString, IntoStaticStr, Serialize, Deserialize, DerivePathStr)] #[strum(serialize_all = "snake_case")] #[path_str(prefix = "images", suffix = ".svg")] pub enum VectorName { ZedLogo, ZedXCopilot, } ``` You can see in the above code we no longer need to manually specify paths for image/icon enums like we currently do in `ui::components::icon`. The icon component will get this same treatment in the future, once we: - do the design work needed to standardize the icons - remove unused icons - update icon names Release Notes: - N/A --- assets/images/zed_logo.svg | 10 ++ assets/images/zed_x_copilot.svg | 14 +++ crates/assets/src/assets.rs | 1 + crates/copilot/src/sign_in.rs | 12 +- .../gpui_macros/src/derive_path_static_str.rs | 73 +++++++++++ crates/gpui_macros/src/gpui_macros.rs | 7 ++ crates/storybook/src/assets.rs | 1 + crates/storybook/src/story_selector.rs | 2 + crates/ui/src/components.rs | 4 + crates/ui/src/components/icon.rs | 2 - crates/ui/src/components/image.rs | 115 ++++++++++++++++++ 11 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 assets/images/zed_logo.svg create mode 100644 assets/images/zed_x_copilot.svg create mode 100644 crates/gpui_macros/src/derive_path_static_str.rs create mode 100644 crates/ui/src/components/image.rs diff --git a/assets/images/zed_logo.svg b/assets/images/zed_logo.svg new file mode 100644 index 0000000000..d1769449c1 --- /dev/null +++ b/assets/images/zed_logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/zed_x_copilot.svg b/assets/images/zed_x_copilot.svg new file mode 100644 index 0000000000..3c5be71074 --- /dev/null +++ b/assets/images/zed_x_copilot.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/crates/assets/src/assets.rs b/crates/assets/src/assets.rs index 395cbf62f6..ee990085f6 100644 --- a/crates/assets/src/assets.rs +++ b/crates/assets/src/assets.rs @@ -8,6 +8,7 @@ use rust_embed::RustEmbed; #[folder = "../../assets"] #[include = "fonts/**/*"] #[include = "icons/**/*"] +#[include = "images/**/*"] #[include = "themes/**/*"] #[exclude = "themes/src/*"] #[include = "sounds/**/*"] diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 1d14e5c1aa..da6b969b72 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -1,10 +1,10 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ - div, svg, AppContext, ClipboardItem, DismissEvent, Element, EventEmitter, FocusHandle, + div, AppContext, ClipboardItem, DismissEvent, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, ParentElement, Render, Styled, Subscription, ViewContext, }; -use ui::{prelude::*, Button, IconName, Label}; +use ui::{prelude::*, Button, Label, Vector, VectorName}; use workspace::ModalView; const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot"; @@ -198,12 +198,8 @@ impl Render for CopilotCodeVerification { cx.focus(&this.focus_handle); })) .child( - svg() - .w_32() - .h_16() - .flex_none() - .path(IconName::ZedXCopilot.path()) - .text_color(cx.theme().colors().icon), + Vector::new(VectorName::ZedXCopilot, rems(8.), rems(4.)) + .color(Color::Custom(cx.theme().colors().icon)), ) .child(prompt) } diff --git a/crates/gpui_macros/src/derive_path_static_str.rs b/crates/gpui_macros/src/derive_path_static_str.rs new file mode 100644 index 0000000000..25531fd2ad --- /dev/null +++ b/crates/gpui_macros/src/derive_path_static_str.rs @@ -0,0 +1,73 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Attribute, Data, DeriveInput, Lit, Meta, NestedMeta}; + +pub fn derive_path_static_str(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + let prefix = get_attr_value(&input.attrs, "prefix").unwrap_or_else(|| "".to_string()); + let suffix = get_attr_value(&input.attrs, "suffix").unwrap_or_else(|| "".to_string()); + let delimiter = get_attr_value(&input.attrs, "delimiter").unwrap_or_else(|| "/".to_string()); + + let path_str_impl = impl_path_str(name, &input.data, &prefix, &suffix, &delimiter); + + let expanded = quote! { + impl #name { + pub fn path_str(&self) -> &'static str { + #path_str_impl + } + } + }; + + TokenStream::from(expanded) +} + +fn impl_path_str( + name: &syn::Ident, + data: &Data, + prefix: &str, + suffix: &str, + delimiter: &str, +) -> proc_macro2::TokenStream { + match *data { + Data::Enum(ref data) => { + let match_arms = data.variants.iter().map(|variant| { + let ident = &variant.ident; + let path = format!("{}{}{}{}{}", prefix, delimiter, ident, delimiter, suffix); + quote! { + #name::#ident => #path, + } + }); + + quote! { + match self { + #(#match_arms)* + } + } + } + _ => panic!("DerivePathStr only supports enums"), + } +} + +fn get_attr_value(attrs: &[Attribute], key: &str) -> Option { + attrs + .iter() + .filter(|attr| attr.path.is_ident("derive_path_static_str")) + .find_map(|attr| { + if let Ok(Meta::List(meta_list)) = attr.parse_meta() { + meta_list.nested.iter().find_map(|nested_meta| { + if let NestedMeta::Meta(Meta::NameValue(name_value)) = nested_meta { + if name_value.path.is_ident(key) { + if let Lit::Str(lit_str) = &name_value.lit { + return Some(lit_str.value()); + } + } + } + None + }) + } else { + None + } + }) +} diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index c4cf5358b3..09cf4027d2 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -1,4 +1,5 @@ mod derive_into_element; +mod derive_path_static_str; mod derive_render; mod register_action; mod styles; @@ -27,6 +28,12 @@ pub fn derive_render(input: TokenStream) -> TokenStream { derive_render::derive_render(input) } +#[proc_macro_derive(PathStaticStr)] +#[doc(hidden)] +pub fn derive_path_static_str(input: TokenStream) -> TokenStream { + derive_path_static_str::derive_path_static_str(input) +} + /// Used by GPUI to generate the style helpers. #[proc_macro] #[doc(hidden)] diff --git a/crates/storybook/src/assets.rs b/crates/storybook/src/assets.rs index da874e5f2d..f45d1457df 100644 --- a/crates/storybook/src/assets.rs +++ b/crates/storybook/src/assets.rs @@ -8,6 +8,7 @@ use rust_embed::RustEmbed; #[folder = "../../assets"] #[include = "fonts/**/*"] #[include = "icons/**/*"] +#[include = "images/**/*"] #[include = "themes/**/*"] #[include = "sounds/**/*"] #[include = "*.md"] diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index 5df02b1df2..881fd83f8f 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -40,6 +40,7 @@ pub enum ComponentStory { ToolStrip, ViewportUnits, WithRemSize, + Vector, } impl ComponentStory { @@ -75,6 +76,7 @@ impl ComponentStory { Self::ToolStrip => cx.new_view(|_| ui::ToolStripStory).into(), Self::ViewportUnits => cx.new_view(|_| crate::stories::ViewportUnitsStory).into(), Self::WithRemSize => cx.new_view(|_| crate::stories::WithRemSizeStory).into(), + Self::Vector => cx.new_view(|_| ui::VectorStory).into(), } } } diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 3a56e46eae..fe63b03502 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -7,6 +7,7 @@ mod divider; mod dropdown_menu; mod facepile; mod icon; +mod image; mod indicator; mod keybinding; mod label; @@ -37,6 +38,7 @@ pub use divider::*; pub use dropdown_menu::*; pub use facepile::*; pub use icon::*; +pub use image::*; pub use indicator::*; pub use keybinding::*; pub use label::*; @@ -55,5 +57,7 @@ pub use tab_bar::*; pub use tool_strip::*; pub use tooltip::*; +#[cfg(feature = "stories")] +pub use image::story::*; #[cfg(feature = "stories")] pub use stories::*; diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 0001ab4a2b..fd4f17ac0e 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -271,7 +271,6 @@ pub enum IconName { XCircle, ZedAssistant, ZedAssistantFilled, - ZedXCopilot, Visible, } @@ -443,7 +442,6 @@ impl IconName { IconName::XCircle => "icons/error.svg", IconName::ZedAssistant => "icons/zed_assistant.svg", IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg", - IconName::ZedXCopilot => "icons/zed_x_copilot.svg", IconName::Visible => "icons/visible.svg", } } diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs new file mode 100644 index 0000000000..286fe7f56f --- /dev/null +++ b/crates/ui/src/components/image.rs @@ -0,0 +1,115 @@ +use gpui::{svg, IntoElement, Rems, RenderOnce, Size, Styled, WindowContext}; +use serde::{Deserialize, Serialize}; +use strum::{EnumIter, EnumString, IntoStaticStr}; +use ui_macros::{path_str, DerivePathStr}; + +use crate::Color; + +#[derive( + Debug, + PartialEq, + Eq, + Copy, + Clone, + EnumIter, + EnumString, + IntoStaticStr, + Serialize, + Deserialize, + DerivePathStr, +)] +#[strum(serialize_all = "snake_case")] +#[path_str(prefix = "images", suffix = ".svg")] +pub enum VectorName { + ZedLogo, + ZedXCopilot, +} + +/// A vector image, such as an SVG. +/// +/// A [Vector] is different from an [Icon] in that it is intended +/// to be displayed at a specific size, or series of sizes, rather +/// than conforming to the standard size of an icons. +#[derive(IntoElement)] +pub struct Vector { + path: &'static str, + color: Color, + size: Size, +} + +impl Vector { + /// Create a new [Vector] image with the given [VectorName] and size. + pub fn new(vector: VectorName, width: Rems, height: Rems) -> Self { + Self { + path: vector.path(), + color: Color::default(), + size: Size { width, height }, + } + } + + /// Create a new [Vector] image where the width and height are the same. + pub fn square(vector: VectorName, size: Rems) -> Self { + Self::new(vector, size, size) + } + + /// Set the image color + pub fn color(mut self, color: Color) -> Self { + self.color = color; + self + } + + /// Set the image size + pub fn size(mut self, size: impl Into>) -> Self { + let size = size.into(); + + self.size = size; + self + } +} + +impl RenderOnce for Vector { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let width = self.size.width; + let height = self.size.height; + + svg() + // By default, prevent the SVG from stretching + // to fill its container. + .flex_none() + .w(width) + .h(height) + .path(self.path) + .text_color(self.color.color(cx)) + } +} + +#[cfg(feature = "stories")] +pub mod story { + use gpui::Render; + use story::{Story, StoryItem, StorySection}; + use strum::IntoEnumIterator; + + use crate::prelude::*; + + use super::{Vector, VectorName}; + + pub struct VectorStory; + + impl Render for VectorStory { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + Story::container().child(StorySection::new().children(VectorName::iter().map( + |vector| StoryItem::new(format!("{:?}", vector), Vector::square(vector, rems(8.))), + ))) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn vector_path() { + assert_eq!(VectorName::ZedLogo.path(), "images/zed_logo.svg"); + } +}