diff --git a/Cargo.lock b/Cargo.lock index a3b967c76d..3fb5ee2f9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2942,6 +2942,28 @@ dependencies = [ "gpui", ] +[[package]] +name = "component" +version = "0.1.0" +dependencies = [ + "collections", + "gpui", + "linkme", + "once_cell", + "parking_lot", + "theme", +] + +[[package]] +name = "component_preview" +version = "0.1.0" +dependencies = [ + "component", + "gpui", + "ui", + "workspace", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -7280,6 +7302,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "linkme" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566336154b9e58a4f055f6dd4cbab62c7dc0826ce3c0a04e63b2d2ecd784cdae" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edbe595006d355eaf9ae11db92707d4338cd2384d16866131cc1afdbdd35d8d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -8693,9 +8735,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "oo7" @@ -14320,8 +14362,10 @@ name = "ui" version = "0.1.0" dependencies = [ "chrono", + "component", "gpui", "itertools 0.14.0", + "linkme", "menu", "serde", "settings", @@ -14349,6 +14393,7 @@ name = "ui_macros" version = "0.1.0" dependencies = [ "convert_case 0.7.1", + "linkme", "proc-macro2", "quote", "syn 1.0.109", @@ -16120,6 +16165,7 @@ dependencies = [ "client", "clock", "collections", + "component", "db", "derive_more", "env_logger 0.11.6", @@ -16554,6 +16600,7 @@ dependencies = [ "collections", "command_palette", "command_palette_hooks", + "component_preview", "copilot", "db", "diagnostics", diff --git a/Cargo.toml b/Cargo.toml index ee6a66f909..147d2c32e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ members = [ "crates/collections", "crates/command_palette", "crates/command_palette_hooks", + "crates/component", + "crates/component_preview", "crates/context_server", "crates/context_server_settings", "crates/copilot", @@ -226,6 +228,8 @@ collab_ui = { path = "crates/collab_ui" } collections = { path = "crates/collections" } command_palette = { path = "crates/command_palette" } command_palette_hooks = { path = "crates/command_palette_hooks" } +component = { path = "crates/component" } +component_preview = { path = "crates/component_preview" } context_server = { path = "crates/context_server" } context_server_settings = { path = "crates/context_server_settings" } copilot = { path = "crates/copilot" } @@ -426,6 +430,7 @@ jupyter-websocket-client = { version = "0.9.0" } libc = "0.2" libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } linkify = "0.10.0" +linkme = "0.3.31" livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "811ceae29fabee455f110c56cd66b3f49a7e5003", features = [ "dispatcher", "services-dispatcher", diff --git a/crates/component/Cargo.toml b/crates/component/Cargo.toml new file mode 100644 index 0000000000..33f951ff95 --- /dev/null +++ b/crates/component/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "component" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/component.rs" + +[dependencies] +collections.workspace = true +gpui.workspace = true +linkme.workspace = true +once_cell = "1.20.3" +parking_lot.workspace = true +theme.workspace = true + +[features] +default = [] diff --git a/crates/component/LICENSE-GPL b/crates/component/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/component/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/component/src/component.rs b/crates/component/src/component.rs new file mode 100644 index 0000000000..e4a2ae7921 --- /dev/null +++ b/crates/component/src/component.rs @@ -0,0 +1,305 @@ +use std::ops::{Deref, DerefMut}; + +use collections::HashMap; +use gpui::{div, prelude::*, AnyElement, App, IntoElement, RenderOnce, SharedString, Window}; +use linkme::distributed_slice; +use once_cell::sync::Lazy; +use parking_lot::RwLock; +use theme::ActiveTheme; + +pub trait Component { + fn scope() -> Option<&'static str>; + fn name() -> &'static str { + std::any::type_name::() + } + fn description() -> Option<&'static str> { + None + } +} + +pub trait ComponentPreview: Component { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement; +} + +#[distributed_slice] +pub static __ALL_COMPONENTS: [fn()] = [..]; + +#[distributed_slice] +pub static __ALL_PREVIEWS: [fn()] = [..]; + +pub static COMPONENT_DATA: Lazy> = + Lazy::new(|| RwLock::new(ComponentRegistry::new())); + +pub struct ComponentRegistry { + components: Vec<(Option<&'static str>, &'static str, Option<&'static str>)>, + previews: HashMap<&'static str, fn(&mut Window, &App) -> AnyElement>, +} + +impl ComponentRegistry { + fn new() -> Self { + ComponentRegistry { + components: Vec::new(), + previews: HashMap::default(), + } + } +} + +pub fn init() { + let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect(); + let preview_fns: Vec<_> = __ALL_PREVIEWS.iter().cloned().collect(); + + for f in component_fns { + f(); + } + for f in preview_fns { + f(); + } +} + +pub fn register_component() { + let component_data = (T::scope(), T::name(), T::description()); + COMPONENT_DATA.write().components.push(component_data); +} + +pub fn register_preview() { + let preview_data = (T::name(), T::preview as fn(&mut Window, &App) -> AnyElement); + COMPONENT_DATA + .write() + .previews + .insert(preview_data.0, preview_data.1); +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ComponentId(pub &'static str); + +#[derive(Clone)] +pub struct ComponentMetadata { + name: SharedString, + scope: Option, + description: Option, + preview: Option AnyElement>, +} + +impl ComponentMetadata { + pub fn name(&self) -> SharedString { + self.name.clone() + } + + pub fn scope(&self) -> Option { + self.scope.clone() + } + + pub fn description(&self) -> Option { + self.description.clone() + } + + pub fn preview(&self) -> Option AnyElement> { + self.preview + } +} + +pub struct AllComponents(pub HashMap); + +impl AllComponents { + pub fn new() -> Self { + AllComponents(HashMap::default()) + } + + /// Returns all components with previews + pub fn all_previews(&self) -> Vec<&ComponentMetadata> { + self.0.values().filter(|c| c.preview.is_some()).collect() + } + + /// Returns all components with previews sorted by name + pub fn all_previews_sorted(&self) -> Vec { + let mut previews: Vec = + self.all_previews().into_iter().cloned().collect(); + previews.sort_by_key(|a| a.name()); + previews + } + + /// Returns all components + pub fn all(&self) -> Vec<&ComponentMetadata> { + self.0.values().collect() + } + + /// Returns all components sorted by name + pub fn all_sorted(&self) -> Vec { + let mut components: Vec = self.all().into_iter().cloned().collect(); + components.sort_by_key(|a| a.name()); + components + } +} + +impl Deref for AllComponents { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for AllComponents { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub fn components() -> AllComponents { + let data = COMPONENT_DATA.read(); + let mut all_components = AllComponents::new(); + + for &(scope, name, description) in &data.components { + let scope = scope.map(Into::into); + let preview = data.previews.get(name).cloned(); + all_components.insert( + ComponentId(name), + ComponentMetadata { + name: name.into(), + scope, + description: description.map(Into::into), + preview, + }, + ); + } + + all_components +} + +/// Which side of the preview to show labels on +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExampleLabelSide { + /// Left side + Left, + /// Right side + Right, + #[default] + /// Top side + Top, + /// Bottom side + Bottom, +} + +/// A single example of a component. +#[derive(IntoElement)] +pub struct ComponentExample { + variant_name: SharedString, + element: AnyElement, + label_side: ExampleLabelSide, + grow: bool, +} + +impl RenderOnce for ComponentExample { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let base = div().flex(); + + let base = match self.label_side { + ExampleLabelSide::Right => base.flex_row(), + ExampleLabelSide::Left => base.flex_row_reverse(), + ExampleLabelSide::Bottom => base.flex_col(), + ExampleLabelSide::Top => base.flex_col_reverse(), + }; + + base.gap_1() + .text_xs() + .text_color(cx.theme().colors().text_muted) + .when(self.grow, |this| this.flex_1()) + .child(self.element) + .child(self.variant_name) + .into_any_element() + } +} + +impl ComponentExample { + /// Create a new example with the given variant name and example value. + pub fn new(variant_name: impl Into, element: AnyElement) -> Self { + Self { + variant_name: variant_name.into(), + element, + label_side: ExampleLabelSide::default(), + grow: false, + } + } + + /// Set the example to grow to fill the available horizontal space. + pub fn grow(mut self) -> Self { + self.grow = true; + self + } +} + +/// A group of component examples. +#[derive(IntoElement)] +pub struct ComponentExampleGroup { + pub title: Option, + pub examples: Vec, + pub grow: bool, +} + +impl RenderOnce for ComponentExampleGroup { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + div() + .flex_col() + .text_sm() + .text_color(cx.theme().colors().text_muted) + .when(self.grow, |this| this.w_full().flex_1()) + .when_some(self.title, |this, title| this.gap_4().child(title)) + .child( + div() + .flex() + .items_start() + .w_full() + .gap_6() + .children(self.examples) + .into_any_element(), + ) + .into_any_element() + } +} + +impl ComponentExampleGroup { + /// Create a new group of examples with the given title. + pub fn new(examples: Vec) -> Self { + Self { + title: None, + examples, + grow: false, + } + } + + /// Create a new group of examples with the given title. + pub fn with_title(title: impl Into, examples: Vec) -> Self { + Self { + title: Some(title.into()), + examples, + grow: false, + } + } + + /// Set the group to grow to fill the available horizontal space. + pub fn grow(mut self) -> Self { + self.grow = true; + self + } +} + +/// Create a single example +pub fn single_example( + variant_name: impl Into, + example: AnyElement, +) -> ComponentExample { + ComponentExample::new(variant_name, example) +} + +/// Create a group of examples without a title +pub fn example_group(examples: Vec) -> ComponentExampleGroup { + ComponentExampleGroup::new(examples) +} + +/// Create a group of examples with a title +pub fn example_group_with_title( + title: impl Into, + examples: Vec, +) -> ComponentExampleGroup { + ComponentExampleGroup::with_title(title, examples) +} diff --git a/crates/component_preview/Cargo.toml b/crates/component_preview/Cargo.toml new file mode 100644 index 0000000000..d909991a18 --- /dev/null +++ b/crates/component_preview/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "component_preview" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/component_preview.rs" + +[features] +default = [] + +[dependencies] +component.workspace = true +gpui.workspace = true +ui.workspace = true +workspace.workspace = true diff --git a/crates/component_preview/LICENSE-GPL b/crates/component_preview/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/component_preview/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/component_preview/src/component_preview.rs b/crates/component_preview/src/component_preview.rs new file mode 100644 index 0000000000..84e00f751c --- /dev/null +++ b/crates/component_preview/src/component_preview.rs @@ -0,0 +1,178 @@ +//! # Component Preview +//! +//! A view for exploring Zed components. + +use component::{components, ComponentMetadata}; +use gpui::{prelude::*, App, EventEmitter, FocusHandle, Focusable, Window}; +use ui::prelude::*; + +use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId}; + +pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _, _cx| { + workspace.register_action( + |workspace, _: &workspace::OpenComponentPreview, window, cx| { + let component_preview = cx.new(ComponentPreview::new); + workspace.add_item_to_active_pane( + Box::new(component_preview), + None, + true, + window, + cx, + ) + }, + ); + }) + .detach(); +} + +struct ComponentPreview { + focus_handle: FocusHandle, +} + +impl ComponentPreview { + pub fn new(cx: &mut Context) -> Self { + Self { + focus_handle: cx.focus_handle(), + } + } + + fn render_sidebar(&self, _window: &Window, _cx: &Context) -> impl IntoElement { + let components = components().all_sorted(); + let sorted_components = components.clone(); + + v_flex().gap_px().p_1().children( + sorted_components + .into_iter() + .map(|component| self.render_sidebar_entry(&component, _cx)), + ) + } + + fn render_sidebar_entry( + &self, + component: &ComponentMetadata, + _cx: &Context, + ) -> impl IntoElement { + h_flex() + .w_40() + .px_1p5() + .py_1() + .child(component.name().clone()) + } + + fn render_preview( + &self, + component: &ComponentMetadata, + window: &mut Window, + cx: &Context, + ) -> impl IntoElement { + let name = component.name(); + let scope = component.scope(); + + let description = component.description(); + + v_group() + .w_full() + .gap_4() + .p_8() + .rounded_md() + .child( + v_flex() + .gap_1() + .child( + h_flex() + .gap_1() + .text_xl() + .child(div().child(name)) + .when_some(scope, |this, scope| { + this.child(div().opacity(0.5).child(format!("({})", scope))) + }), + ) + .when_some(description, |this, description| { + this.child( + div() + .text_ui_sm(cx) + .text_color(cx.theme().colors().text_muted) + .max_w(px(600.0)) + .child(description), + ) + }), + ) + .when_some(component.preview(), |this, preview| { + this.child(preview(window, cx)) + }) + .into_any_element() + } + + fn render_previews(&self, window: &mut Window, cx: &Context) -> impl IntoElement { + v_flex() + .id("component-previews") + .size_full() + .overflow_y_scroll() + .p_4() + .gap_2() + .children( + components() + .all_previews_sorted() + .iter() + .map(|component| self.render_preview(component, window, cx)), + ) + } +} + +impl Render for ComponentPreview { + fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement { + h_flex() + .id("component-preview") + .key_context("ComponentPreview") + .items_start() + .overflow_hidden() + .size_full() + .max_h_full() + .track_focus(&self.focus_handle) + .px_2() + .bg(cx.theme().colors().editor_background) + .child(self.render_sidebar(window, cx)) + .child(self.render_previews(window, cx)) + } +} + +impl EventEmitter for ComponentPreview {} + +impl Focusable for ComponentPreview { + fn focus_handle(&self, _: &App) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for ComponentPreview { + type Event = ItemEvent; + + fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { + Some("Component Preview".into()) + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + + fn show_toolbar(&self) -> bool { + false + } + + fn clone_on_split( + &self, + _workspace_id: Option, + _window: &mut Window, + cx: &mut Context, + ) -> Option> + where + Self: Sized, + { + Some(cx.new(Self::new)) + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { + f(*event) + } +} diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index dc893d6643..ba7c89a8a6 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -14,8 +14,10 @@ path = "src/ui.rs" [dependencies] chrono.workspace = true +component.workspace = true gpui.workspace = true itertools = { workspace = true, optional = true } +linkme.workspace = true menu.workspace = true serde.workspace = true settings.workspace = true @@ -31,3 +33,7 @@ windows.workspace = true [features] default = [] stories = ["dep:itertools", "dep:story"] + +# cargo-machete doesn't understand that linkme is used in the component macro +[package.metadata.cargo-machete] +ignored = ["linkme"] diff --git a/crates/ui/src/components/avatar/avatar.rs b/crates/ui/src/components/avatar/avatar.rs index e7335a9e75..82f3ea7ae2 100644 --- a/crates/ui/src/components/avatar/avatar.rs +++ b/crates/ui/src/components/avatar/avatar.rs @@ -1,4 +1,4 @@ -use crate::prelude::*; +use crate::{prelude::*, Indicator}; use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled}; @@ -14,7 +14,7 @@ use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled}; /// .grayscale(true) /// .border_color(gpui::red()); /// ``` -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Avatar { image: Img, size: Option, @@ -96,3 +96,60 @@ impl RenderOnce for Avatar { .children(self.indicator.map(|indicator| div().child(indicator))) } } + +impl ComponentPreview for Avatar { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4"; + + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Sizes", + vec![ + single_example( + "Default", + Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4") + .into_any_element(), + ), + single_example( + "Small", + Avatar::new(example_avatar).size(px(24.)).into_any_element(), + ), + single_example( + "Large", + Avatar::new(example_avatar).size(px(48.)).into_any_element(), + ), + ], + ), + example_group_with_title( + "Styles", + vec![ + single_example("Default", Avatar::new(example_avatar).into_any_element()), + single_example( + "Grayscale", + Avatar::new(example_avatar) + .grayscale(true) + .into_any_element(), + ), + single_example( + "With Border", + Avatar::new(example_avatar) + .border_color(gpui::red()) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "With Indicator", + vec![single_example( + "Dot", + Avatar::new(example_avatar) + .indicator(Indicator::dot().color(Color::Success)) + .into_any_element(), + )], + ), + ]) + .into_any_element() + } +} diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index c9b6186661..4194b3c8d2 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -1,5 +1,7 @@ #![allow(missing_docs)] -use gpui::{AnyView, DefiniteLength}; +use component::{example_group_with_title, single_example, ComponentPreview}; +use gpui::{AnyElement, AnyView, DefiniteLength}; +use ui_macros::IntoComponent; use crate::{ prelude::*, Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding, @@ -78,7 +80,7 @@ use super::button_icon::ButtonIcon; /// }); /// ``` /// -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Button { base: ButtonLike, label: SharedString, @@ -455,101 +457,124 @@ impl RenderOnce for Button { } impl ComponentPreview for Button { - fn description() -> impl Into> { - "A button allows users to take actions, and make choices, with a single tap." - } - - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - vec![ - example_group_with_title( - "Styles", - vec![ - single_example("Default", Button::new("default", "Default")), - single_example( - "Filled", - Button::new("filled", "Filled").style(ButtonStyle::Filled), - ), - single_example( - "Subtle", - Button::new("outline", "Subtle").style(ButtonStyle::Subtle), - ), - single_example( - "Transparent", - Button::new("transparent", "Transparent").style(ButtonStyle::Transparent), - ), - ], - ), - example_group_with_title( - "Tinted", - vec![ - single_example( - "Accent", - Button::new("tinted_accent", "Accent") - .style(ButtonStyle::Tinted(TintColor::Accent)), - ), - single_example( - "Error", - Button::new("tinted_negative", "Error") - .style(ButtonStyle::Tinted(TintColor::Error)), - ), - single_example( - "Warning", - Button::new("tinted_warning", "Warning") - .style(ButtonStyle::Tinted(TintColor::Warning)), - ), - single_example( - "Success", - Button::new("tinted_positive", "Success") - .style(ButtonStyle::Tinted(TintColor::Success)), - ), - ], - ), - example_group_with_title( - "States", - vec![ - single_example("Default", Button::new("default_state", "Default")), - single_example( - "Disabled", - Button::new("disabled", "Disabled").disabled(true), - ), - single_example( - "Selected", - Button::new("selected", "Selected").toggle_state(true), - ), - ], - ), - example_group_with_title( - "With Icons", - vec![ - single_example( - "Icon Start", - Button::new("icon_start", "Icon Start") - .icon(IconName::Check) - .icon_position(IconPosition::Start), - ), - single_example( - "Icon End", - Button::new("icon_end", "Icon End") - .icon(IconName::Check) - .icon_position(IconPosition::End), - ), - single_example( - "Icon Color", - Button::new("icon_color", "Icon Color") - .icon(IconName::Check) - .icon_color(Color::Accent), - ), - single_example( - "Tinted Icons", - Button::new("tinted_icons", "Error") - .style(ButtonStyle::Tinted(TintColor::Error)) - .color(Color::Error) - .icon_color(Color::Error) - .icon(IconName::Trash) - .icon_position(IconPosition::Start), - ), - ], - ), - ] + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Styles", + vec![ + single_example( + "Default", + Button::new("default", "Default").into_any_element(), + ), + single_example( + "Filled", + Button::new("filled", "Filled") + .style(ButtonStyle::Filled) + .into_any_element(), + ), + single_example( + "Subtle", + Button::new("outline", "Subtle") + .style(ButtonStyle::Subtle) + .into_any_element(), + ), + single_example( + "Transparent", + Button::new("transparent", "Transparent") + .style(ButtonStyle::Transparent) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Tinted", + vec![ + single_example( + "Accent", + Button::new("tinted_accent", "Accent") + .style(ButtonStyle::Tinted(TintColor::Accent)) + .into_any_element(), + ), + single_example( + "Error", + Button::new("tinted_negative", "Error") + .style(ButtonStyle::Tinted(TintColor::Error)) + .into_any_element(), + ), + single_example( + "Warning", + Button::new("tinted_warning", "Warning") + .style(ButtonStyle::Tinted(TintColor::Warning)) + .into_any_element(), + ), + single_example( + "Success", + Button::new("tinted_positive", "Success") + .style(ButtonStyle::Tinted(TintColor::Success)) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "States", + vec![ + single_example( + "Default", + Button::new("default_state", "Default").into_any_element(), + ), + single_example( + "Disabled", + Button::new("disabled", "Disabled") + .disabled(true) + .into_any_element(), + ), + single_example( + "Selected", + Button::new("selected", "Selected") + .toggle_state(true) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "With Icons", + vec![ + single_example( + "Icon Start", + Button::new("icon_start", "Icon Start") + .icon(IconName::Check) + .icon_position(IconPosition::Start) + .into_any_element(), + ), + single_example( + "Icon End", + Button::new("icon_end", "Icon End") + .icon(IconName::Check) + .icon_position(IconPosition::End) + .into_any_element(), + ), + single_example( + "Icon Color", + Button::new("icon_color", "Icon Color") + .icon(IconName::Check) + .icon_color(Color::Accent) + .into_any_element(), + ), + single_example( + "Tinted Icons", + Button::new("tinted_icons", "Error") + .style(ButtonStyle::Tinted(TintColor::Error)) + .color(Color::Error) + .icon_color(Color::Error) + .icon(IconName::Trash) + .icon_position(IconPosition::Start) + .into_any_element(), + ), + ], + ), + ]) + .into_any_element() } } diff --git a/crates/ui/src/components/content_group.rs b/crates/ui/src/components/content_group.rs index b1ffae1490..1a57838c2e 100644 --- a/crates/ui/src/components/content_group.rs +++ b/crates/ui/src/components/content_group.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use component::{example_group, single_example, ComponentPreview}; use gpui::{AnyElement, IntoElement, ParentElement, StyleRefinement, Styled}; use smallvec::SmallVec; @@ -22,7 +23,8 @@ pub fn h_group() -> ContentGroup { } /// A flexible container component that can hold other elements. -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] +#[component(scope = "layout")] pub struct ContentGroup { base: Div, border: bool, @@ -87,16 +89,8 @@ impl RenderOnce for ContentGroup { } impl ComponentPreview for ContentGroup { - fn description() -> impl Into> { - "A flexible container component that can hold other elements. It can be customized with or without a border and background fill." - } - - fn example_label_side() -> ExampleLabelSide { - ExampleLabelSide::Bottom - } - - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - vec![example_group(vec![ + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + example_group(vec![ single_example( "Default", ContentGroup::new() @@ -104,7 +98,8 @@ impl ComponentPreview for ContentGroup { .items_center() .justify_center() .h_48() - .child(Label::new("Default ContentBox")), + .child(Label::new("Default ContentBox")) + .into_any_element(), ) .grow(), single_example( @@ -115,7 +110,8 @@ impl ComponentPreview for ContentGroup { .justify_center() .h_48() .borderless() - .child(Label::new("Borderless ContentBox")), + .child(Label::new("Borderless ContentBox")) + .into_any_element(), ) .grow(), single_example( @@ -126,10 +122,11 @@ impl ComponentPreview for ContentGroup { .justify_center() .h_48() .unfilled() - .child(Label::new("Unfilled ContentBox")), + .child(Label::new("Unfilled ContentBox")) + .into_any_element(), ) .grow(), ]) - .grow()] + .into_any_element() } } diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs index 875b1dfb2a..d965bc598a 100644 --- a/crates/ui/src/components/facepile.rs +++ b/crates/ui/src/components/facepile.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, Avatar}; +use crate::prelude::*; use gpui::{AnyElement, StyleRefinement}; use smallvec::SmallVec; @@ -60,60 +60,60 @@ impl RenderOnce for Facepile { } } -impl ComponentPreview for Facepile { - fn description() -> impl Into> { - "A facepile is a collection of faces stacked horizontally–\ - always with the leftmost face on top and descending in z-index.\ - \n\nFacepiles are used to display a group of people or things,\ - such as a list of participants in a collaboration session." - } - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - let few_faces: [&'static str; 3] = [ - "https://avatars.githubusercontent.com/u/1714999?s=60&v=4", - "https://avatars.githubusercontent.com/u/67129314?s=60&v=4", - "https://avatars.githubusercontent.com/u/482957?s=60&v=4", - ]; +// impl ComponentPreview for Facepile { +// fn description() -> impl Into> { +// "A facepile is a collection of faces stacked horizontally–\ +// always with the leftmost face on top and descending in z-index.\ +// \n\nFacepiles are used to display a group of people or things,\ +// such as a list of participants in a collaboration session." +// } +// fn examples(_window: &mut Window, _: &mut App) -> Vec> { +// let few_faces: [&'static str; 3] = [ +// "https://avatars.githubusercontent.com/u/1714999?s=60&v=4", +// "https://avatars.githubusercontent.com/u/67129314?s=60&v=4", +// "https://avatars.githubusercontent.com/u/482957?s=60&v=4", +// ]; - let many_faces: [&'static str; 6] = [ - "https://avatars.githubusercontent.com/u/326587?s=60&v=4", - "https://avatars.githubusercontent.com/u/2280405?s=60&v=4", - "https://avatars.githubusercontent.com/u/1789?s=60&v=4", - "https://avatars.githubusercontent.com/u/67129314?s=60&v=4", - "https://avatars.githubusercontent.com/u/482957?s=60&v=4", - "https://avatars.githubusercontent.com/u/1714999?s=60&v=4", - ]; +// let many_faces: [&'static str; 6] = [ +// "https://avatars.githubusercontent.com/u/326587?s=60&v=4", +// "https://avatars.githubusercontent.com/u/2280405?s=60&v=4", +// "https://avatars.githubusercontent.com/u/1789?s=60&v=4", +// "https://avatars.githubusercontent.com/u/67129314?s=60&v=4", +// "https://avatars.githubusercontent.com/u/482957?s=60&v=4", +// "https://avatars.githubusercontent.com/u/1714999?s=60&v=4", +// ]; - vec![example_group_with_title( - "Examples", - vec![ - single_example( - "Few Faces", - Facepile::new( - few_faces - .iter() - .map(|&url| Avatar::new(url).into_any_element()) - .collect(), - ), - ), - single_example( - "Many Faces", - Facepile::new( - many_faces - .iter() - .map(|&url| Avatar::new(url).into_any_element()) - .collect(), - ), - ), - single_example( - "Custom Size", - Facepile::new( - few_faces - .iter() - .map(|&url| Avatar::new(url).size(px(24.)).into_any_element()) - .collect(), - ), - ), - ], - )] - } -} +// vec![example_group_with_title( +// "Examples", +// vec![ +// single_example( +// "Few Faces", +// Facepile::new( +// few_faces +// .iter() +// .map(|&url| Avatar::new(url).into_any_element()) +// .collect(), +// ), +// ), +// single_example( +// "Many Faces", +// Facepile::new( +// many_faces +// .iter() +// .map(|&url| Avatar::new(url).into_any_element()) +// .collect(), +// ), +// ), +// single_example( +// "Custom Size", +// Facepile::new( +// few_faces +// .iter() +// .map(|&url| Avatar::new(url).size(px(24.)).into_any_element()) +// .collect(), +// ), +// ), +// ], +// )] +// } +// } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 4ea5ca9c54..c23c41cbcf 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -7,17 +7,13 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; pub use decorated_icon::*; -use gpui::{img, svg, AnimationElement, Hsla, IntoElement, Rems, Transformation}; +use gpui::{img, svg, AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation}; pub use icon_decoration::*; use serde::{Deserialize, Serialize}; use strum::{EnumIter, EnumString, IntoStaticStr}; use ui_macros::DerivePathStr; -use crate::{ - prelude::*, - traits::component_preview::{ComponentExample, ComponentPreview}, - Indicator, -}; +use crate::{prelude::*, Indicator}; #[derive(IntoElement)] pub enum AnyIcon { @@ -364,7 +360,7 @@ impl IconSource { } } -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Icon { source: IconSource, color: Color, @@ -494,24 +490,41 @@ impl RenderOnce for IconWithIndicator { } impl ComponentPreview for Icon { - fn examples(_window: &mut Window, _cx: &mut App) -> Vec> { - let arrow_icons = vec![ - IconName::ArrowDown, - IconName::ArrowLeft, - IconName::ArrowRight, - IconName::ArrowUp, - IconName::ArrowCircle, - ]; - - vec![example_group_with_title( - "Arrow Icons", - arrow_icons - .into_iter() - .map(|icon| { - let name = format!("{:?}", icon).to_string(); - ComponentExample::new(name, Icon::new(icon)) - }) - .collect(), - )] + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Sizes", + vec![ + single_example("Default", Icon::new(IconName::Star).into_any_element()), + single_example( + "Small", + Icon::new(IconName::Star) + .size(IconSize::Small) + .into_any_element(), + ), + single_example( + "Large", + Icon::new(IconName::Star) + .size(IconSize::XLarge) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Colors", + vec![ + single_example("Default", Icon::new(IconName::Bell).into_any_element()), + single_example( + "Custom Color", + Icon::new(IconName::Bell) + .color(Color::Error) + .into_any_element(), + ), + ], + ), + ]) + .into_any_element() } } diff --git a/crates/ui/src/components/icon/decorated_icon.rs b/crates/ui/src/components/icon/decorated_icon.rs index 1a441bf6ea..c973dc6096 100644 --- a/crates/ui/src/components/icon/decorated_icon.rs +++ b/crates/ui/src/components/icon/decorated_icon.rs @@ -1,10 +1,8 @@ -use gpui::{IntoElement, Point}; +use gpui::{AnyElement, IntoElement, Point}; -use crate::{ - prelude::*, traits::component_preview::ComponentPreview, IconDecoration, IconDecorationKind, -}; +use crate::{prelude::*, IconDecoration, IconDecorationKind}; -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct DecoratedIcon { icon: Icon, decoration: Option, @@ -27,12 +25,7 @@ impl RenderOnce for DecoratedIcon { } impl ComponentPreview for DecoratedIcon { - fn examples(_: &mut Window, cx: &mut App) -> Vec> { - let icon_1 = Icon::new(IconName::FileDoc); - let icon_2 = Icon::new(IconName::FileDoc); - let icon_3 = Icon::new(IconName::FileDoc); - let icon_4 = Icon::new(IconName::FileDoc); - + fn preview(_window: &mut Window, cx: &App) -> AnyElement { let decoration_x = IconDecoration::new( IconDecorationKind::X, cx.theme().colors().surface_background, @@ -66,22 +59,32 @@ impl ComponentPreview for DecoratedIcon { y: px(-2.), }); - let examples = vec![ - single_example("no_decoration", DecoratedIcon::new(icon_1, None)), - single_example( - "with_decoration", - DecoratedIcon::new(icon_2, Some(decoration_x)), - ), - single_example( - "with_decoration", - DecoratedIcon::new(icon_3, Some(decoration_triangle)), - ), - single_example( - "with_decoration", - DecoratedIcon::new(icon_4, Some(decoration_dot)), - ), - ]; - - vec![example_group(examples)] + v_flex() + .gap_6() + .children(vec![example_group_with_title( + "Decorations", + vec![ + single_example( + "No Decoration", + DecoratedIcon::new(Icon::new(IconName::FileDoc), None).into_any_element(), + ), + single_example( + "X Decoration", + DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_x)) + .into_any_element(), + ), + single_example( + "Triangle Decoration", + DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_triangle)) + .into_any_element(), + ), + single_example( + "Dot Decoration", + DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_dot)) + .into_any_element(), + ), + ], + )]) + .into_any_element() } } diff --git a/crates/ui/src/components/icon/icon_decoration.rs b/crates/ui/src/components/icon/icon_decoration.rs index 75a04265f9..ba73e5a2cb 100644 --- a/crates/ui/src/components/icon/icon_decoration.rs +++ b/crates/ui/src/components/icon/icon_decoration.rs @@ -1,8 +1,8 @@ use gpui::{svg, Hsla, IntoElement, Point}; -use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr}; +use strum::{EnumIter, EnumString, IntoStaticStr}; use ui_macros::DerivePathStr; -use crate::{prelude::*, traits::component_preview::ComponentPreview}; +use crate::prelude::*; const ICON_DECORATION_SIZE: Pixels = px(11.); @@ -149,21 +149,3 @@ impl RenderOnce for IconDecoration { .child(background) } } - -impl ComponentPreview for IconDecoration { - fn examples(_: &mut Window, cx: &mut App) -> Vec> { - let all_kinds = IconDecorationKind::iter().collect::>(); - - let examples = all_kinds - .iter() - .map(|kind| { - single_example( - format!("{kind:?}"), - IconDecoration::new(*kind, cx.theme().colors().surface_background, cx), - ) - }) - .collect(); - - vec![example_group(examples)] - } -} diff --git a/crates/ui/src/components/indicator.rs b/crates/ui/src/components/indicator.rs index bb275cd941..0cf4cab72e 100644 --- a/crates/ui/src/components/indicator.rs +++ b/crates/ui/src/components/indicator.rs @@ -83,34 +83,3 @@ impl RenderOnce for Indicator { } } } - -impl ComponentPreview for Indicator { - fn description() -> impl Into> { - "An indicator visually represents a status or state." - } - - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - vec![ - example_group_with_title( - "Types", - vec![ - single_example("Dot", Indicator::dot().color(Color::Info)), - single_example("Bar", Indicator::bar().color(Color::Player(2))), - single_example( - "Icon", - Indicator::icon(Icon::new(IconName::Check).color(Color::Success)), - ), - ], - ), - example_group_with_title( - "Examples", - vec![ - single_example("Info", Indicator::dot().color(Color::Info)), - single_example("Success", Indicator::dot().color(Color::Success)), - single_example("Warning", Indicator::dot().color(Color::Warning)), - single_example("Error", Indicator::dot().color(Color::Error)), - ], - ), - ] - } -} diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs index 2239cf0790..2abb93ea40 100644 --- a/crates/ui/src/components/keybinding_hint.rs +++ b/crates/ui/src/components/keybinding_hint.rs @@ -1,6 +1,6 @@ use crate::{h_flex, prelude::*}; use crate::{ElevationIndex, KeyBinding}; -use gpui::{point, App, BoxShadow, IntoElement, Window}; +use gpui::{point, AnyElement, App, BoxShadow, IntoElement, Window}; use smallvec::smallvec; /// Represents a hint for a keybinding, optionally with a prefix and suffix. @@ -17,7 +17,7 @@ use smallvec::smallvec; /// .prefix("Save:") /// .size(Pixels::from(14.0)); /// ``` -#[derive(Debug, IntoElement, Clone)] +#[derive(Debug, IntoElement, IntoComponent)] pub struct KeybindingHint { prefix: Option, suffix: Option, @@ -206,102 +206,99 @@ impl RenderOnce for KeybindingHint { } impl ComponentPreview for KeybindingHint { - fn description() -> impl Into> { - "Used to display hint text for keyboard shortcuts. Can have a prefix and suffix." - } - - fn examples(window: &mut Window, _cx: &mut App) -> Vec> { - let home_fallback = gpui::KeyBinding::new("home", menu::SelectFirst, None); - let home = KeyBinding::for_action(&menu::SelectFirst, window) - .unwrap_or(KeyBinding::new(home_fallback)); - - let end_fallback = gpui::KeyBinding::new("end", menu::SelectLast, None); - let end = KeyBinding::for_action(&menu::SelectLast, window) - .unwrap_or(KeyBinding::new(end_fallback)); - + fn preview(window: &mut Window, _cx: &App) -> AnyElement { let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None); let enter = KeyBinding::for_action(&menu::Confirm, window) .unwrap_or(KeyBinding::new(enter_fallback)); - let escape_fallback = gpui::KeyBinding::new("escape", menu::Cancel, None); - let escape = KeyBinding::for_action(&menu::Cancel, window) - .unwrap_or(KeyBinding::new(escape_fallback)); - - vec![ - example_group_with_title( - "Basic", - vec![ - single_example( - "With Prefix", - KeybindingHint::with_prefix("Go to Start:", home.clone()), - ), - single_example( - "With Suffix", - KeybindingHint::with_suffix(end.clone(), "Go to End"), - ), - single_example( - "With Prefix and Suffix", - KeybindingHint::new(enter.clone()) - .prefix("Confirm:") - .suffix("Execute selected action"), - ), - ], - ), - example_group_with_title( - "Sizes", - vec![ - single_example( - "Small", - KeybindingHint::new(home.clone()) - .size(Pixels::from(12.0)) - .prefix("Small:"), - ), - single_example( - "Medium", - KeybindingHint::new(end.clone()) - .size(Pixels::from(16.0)) - .suffix("Medium"), - ), - single_example( - "Large", - KeybindingHint::new(enter.clone()) - .size(Pixels::from(20.0)) - .prefix("Large:") - .suffix("Size"), - ), - ], - ), - example_group_with_title( - "Elevations", - vec![ - single_example( - "Surface", - KeybindingHint::new(home.clone()) - .elevation(ElevationIndex::Surface) - .prefix("Surface:"), - ), - single_example( - "Elevated Surface", - KeybindingHint::new(end.clone()) - .elevation(ElevationIndex::ElevatedSurface) - .suffix("Elevated"), - ), - single_example( - "Editor Surface", - KeybindingHint::new(enter.clone()) - .elevation(ElevationIndex::EditorSurface) - .prefix("Editor:") - .suffix("Surface"), - ), - single_example( - "Modal Surface", - KeybindingHint::new(escape.clone()) - .elevation(ElevationIndex::ModalSurface) - .prefix("Modal:") - .suffix("Escape"), - ), - ], - ), - ] + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Basic", + vec![ + single_example( + "With Prefix", + KeybindingHint::with_prefix("Go to Start:", enter.clone()) + .into_any_element(), + ), + single_example( + "With Suffix", + KeybindingHint::with_suffix(enter.clone(), "Go to End") + .into_any_element(), + ), + single_example( + "With Prefix and Suffix", + KeybindingHint::new(enter.clone()) + .prefix("Confirm:") + .suffix("Execute selected action") + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Sizes", + vec![ + single_example( + "Small", + KeybindingHint::new(enter.clone()) + .size(Pixels::from(12.0)) + .prefix("Small:") + .into_any_element(), + ), + single_example( + "Medium", + KeybindingHint::new(enter.clone()) + .size(Pixels::from(16.0)) + .suffix("Medium") + .into_any_element(), + ), + single_example( + "Large", + KeybindingHint::new(enter.clone()) + .size(Pixels::from(20.0)) + .prefix("Large:") + .suffix("Size") + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Elevations", + vec![ + single_example( + "Surface", + KeybindingHint::new(enter.clone()) + .elevation(ElevationIndex::Surface) + .prefix("Surface:") + .into_any_element(), + ), + single_example( + "Elevated Surface", + KeybindingHint::new(enter.clone()) + .elevation(ElevationIndex::ElevatedSurface) + .suffix("Elevated") + .into_any_element(), + ), + single_example( + "Editor Surface", + KeybindingHint::new(enter.clone()) + .elevation(ElevationIndex::EditorSurface) + .prefix("Editor:") + .suffix("Surface") + .into_any_element(), + ), + single_example( + "Modal Surface", + KeybindingHint::new(enter.clone()) + .elevation(ElevationIndex::ModalSurface) + .prefix("Modal:") + .suffix("Enter") + .into_any_element(), + ), + ], + ), + ]) + .into_any_element() } } diff --git a/crates/ui/src/components/label/label.rs b/crates/ui/src/components/label/label.rs index ff2687d047..59243998df 100644 --- a/crates/ui/src/components/label/label.rs +++ b/crates/ui/src/components/label/label.rs @@ -1,6 +1,6 @@ #![allow(missing_docs)] -use gpui::{App, StyleRefinement, Window}; +use gpui::{AnyElement, App, StyleRefinement, Window}; use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle}; @@ -32,7 +32,7 @@ use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle}; /// /// let my_label = Label::new("Deleted").strikethrough(true); /// ``` -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Label { base: LabelLike, label: SharedString, @@ -184,3 +184,53 @@ impl RenderOnce for Label { self.base.child(self.label) } } + +impl ComponentPreview for Label { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Sizes", + vec![ + single_example("Default", Label::new("Default Label").into_any_element()), + single_example("Small", Label::new("Small Label").size(LabelSize::Small).into_any_element()), + single_example("Large", Label::new("Large Label").size(LabelSize::Large).into_any_element()), + ], + ), + example_group_with_title( + "Colors", + vec![ + single_example("Default", Label::new("Default Color").into_any_element()), + single_example("Accent", Label::new("Accent Color").color(Color::Accent).into_any_element()), + single_example("Error", Label::new("Error Color").color(Color::Error).into_any_element()), + ], + ), + example_group_with_title( + "Styles", + vec![ + single_example("Default", Label::new("Default Style").into_any_element()), + single_example("Bold", Label::new("Bold Style").weight(gpui::FontWeight::BOLD).into_any_element()), + single_example("Italic", Label::new("Italic Style").italic(true).into_any_element()), + single_example("Strikethrough", Label::new("Strikethrough Style").strikethrough(true).into_any_element()), + single_example("Underline", Label::new("Underline Style").underline(true).into_any_element()), + ], + ), + example_group_with_title( + "Line Height Styles", + vec![ + single_example("Default", Label::new("Default Line Height").into_any_element()), + single_example("UI Label", Label::new("UI Label Line Height").line_height_style(LineHeightStyle::UiLabel).into_any_element()), + ], + ), + example_group_with_title( + "Special Cases", + vec![ + single_example("Single Line", Label::new("Single\nLine\nText").single_line().into_any_element()), + single_example("Text Ellipsis", Label::new("This is a very long text that should be truncated with an ellipsis").text_ellipsis().into_any_element()), + ], + ), + ]) + .into_any_element() + } +} diff --git a/crates/ui/src/components/radio.rs b/crates/ui/src/components/radio.rs index 6e98a10e0b..d7ee106d2d 100644 --- a/crates/ui/src/components/radio.rs +++ b/crates/ui/src/components/radio.rs @@ -4,9 +4,6 @@ use std::sync::Arc; use crate::prelude::*; -/// A [`Checkbox`] that has a [`Label`]. -/// -/// [`Checkbox`]: crate::components::Checkbox #[derive(IntoElement)] pub struct RadioWithLabel { id: ElementId, diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index 4d991bd6ce..362f1a41a5 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -27,7 +27,7 @@ pub enum TabCloseSide { End, } -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Tab { div: Stateful
, selected: bool, @@ -171,3 +171,48 @@ impl RenderOnce for Tab { ) } } + +impl ComponentPreview for Tab { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![example_group_with_title( + "Variations", + vec![ + single_example( + "Default", + Tab::new("default").child("Default Tab").into_any_element(), + ), + single_example( + "Selected", + Tab::new("selected") + .toggle_state(true) + .child("Selected Tab") + .into_any_element(), + ), + single_example( + "First", + Tab::new("first") + .position(TabPosition::First) + .child("First Tab") + .into_any_element(), + ), + single_example( + "Middle", + Tab::new("middle") + .position(TabPosition::Middle(Ordering::Equal)) + .child("Middle Tab") + .into_any_element(), + ), + single_example( + "Last", + Tab::new("last") + .position(TabPosition::Last) + .child("Last Tab") + .into_any_element(), + ), + ], + )]) + .into_any_element() + } +} diff --git a/crates/ui/src/components/table.rs b/crates/ui/src/components/table.rs index c191882979..aa955a6d08 100644 --- a/crates/ui/src/components/table.rs +++ b/crates/ui/src/components/table.rs @@ -2,7 +2,7 @@ use crate::{prelude::*, Indicator}; use gpui::{div, AnyElement, FontWeight, IntoElement, Length}; /// A table component -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Table { column_headers: Vec, rows: Vec>, @@ -152,88 +152,110 @@ where } impl ComponentPreview for Table { - fn description() -> impl Into> { - "Used for showing tabular data. Tables may show both text and elements in their cells." - } - - fn example_label_side() -> ExampleLabelSide { - ExampleLabelSide::Top - } - - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - vec![ - example_group(vec![ - single_example( - "Simple Table", - Table::new(vec!["Name", "Age", "City"]) - .width(px(400.)) - .row(vec!["Alice", "28", "New York"]) - .row(vec!["Bob", "32", "San Francisco"]) - .row(vec!["Charlie", "25", "London"]), + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Basic Tables", + vec![ + single_example( + "Simple Table", + Table::new(vec!["Name", "Age", "City"]) + .width(px(400.)) + .row(vec!["Alice", "28", "New York"]) + .row(vec!["Bob", "32", "San Francisco"]) + .row(vec!["Charlie", "25", "London"]) + .into_any_element(), + ), + single_example( + "Two Column Table", + Table::new(vec!["Category", "Value"]) + .width(px(300.)) + .row(vec!["Revenue", "$100,000"]) + .row(vec!["Expenses", "$75,000"]) + .row(vec!["Profit", "$25,000"]) + .into_any_element(), + ), + ], ), - single_example( - "Two Column Table", - Table::new(vec!["Category", "Value"]) - .width(px(300.)) - .row(vec!["Revenue", "$100,000"]) - .row(vec!["Expenses", "$75,000"]) - .row(vec!["Profit", "$25,000"]), + example_group_with_title( + "Styled Tables", + vec![ + single_example( + "Default", + Table::new(vec!["Product", "Price", "Stock"]) + .width(px(400.)) + .row(vec!["Laptop", "$999", "In Stock"]) + .row(vec!["Phone", "$599", "Low Stock"]) + .row(vec!["Tablet", "$399", "Out of Stock"]) + .into_any_element(), + ), + single_example( + "Striped", + Table::new(vec!["Product", "Price", "Stock"]) + .width(px(400.)) + .striped() + .row(vec!["Laptop", "$999", "In Stock"]) + .row(vec!["Phone", "$599", "Low Stock"]) + .row(vec!["Tablet", "$399", "Out of Stock"]) + .row(vec!["Headphones", "$199", "In Stock"]) + .into_any_element(), + ), + ], ), - ]), - example_group(vec![single_example( - "Striped Table", - Table::new(vec!["Product", "Price", "Stock"]) - .width(px(600.)) - .striped() - .row(vec!["Laptop", "$999", "In Stock"]) - .row(vec!["Phone", "$599", "Low Stock"]) - .row(vec!["Tablet", "$399", "Out of Stock"]) - .row(vec!["Headphones", "$199", "In Stock"]), - )]), - example_group_with_title( - "Mixed Content Table", - vec![single_example( - "Table with Elements", - Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"]) - .width(px(840.)) - .row(vec![ - element_cell(Indicator::dot().color(Color::Success).into_any_element()), - string_cell("Project A"), - string_cell("High"), - string_cell("2023-12-31"), - element_cell( - Button::new("view_a", "View") - .style(ButtonStyle::Filled) - .full_width() - .into_any_element(), - ), - ]) - .row(vec![ - element_cell(Indicator::dot().color(Color::Warning).into_any_element()), - string_cell("Project B"), - string_cell("Medium"), - string_cell("2024-03-15"), - element_cell( - Button::new("view_b", "View") - .style(ButtonStyle::Filled) - .full_width() - .into_any_element(), - ), - ]) - .row(vec![ - element_cell(Indicator::dot().color(Color::Error).into_any_element()), - string_cell("Project C"), - string_cell("Low"), - string_cell("2024-06-30"), - element_cell( - Button::new("view_c", "View") - .style(ButtonStyle::Filled) - .full_width() - .into_any_element(), - ), - ]), - )], - ), - ] + example_group_with_title( + "Mixed Content Table", + vec![single_example( + "Table with Elements", + Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"]) + .width(px(840.)) + .row(vec![ + element_cell( + Indicator::dot().color(Color::Success).into_any_element(), + ), + string_cell("Project A"), + string_cell("High"), + string_cell("2023-12-31"), + element_cell( + Button::new("view_a", "View") + .style(ButtonStyle::Filled) + .full_width() + .into_any_element(), + ), + ]) + .row(vec![ + element_cell( + Indicator::dot().color(Color::Warning).into_any_element(), + ), + string_cell("Project B"), + string_cell("Medium"), + string_cell("2024-03-15"), + element_cell( + Button::new("view_b", "View") + .style(ButtonStyle::Filled) + .full_width() + .into_any_element(), + ), + ]) + .row(vec![ + element_cell( + Indicator::dot().color(Color::Error).into_any_element(), + ), + string_cell("Project C"), + string_cell("Low"), + string_cell("2024-06-30"), + element_cell( + Button::new("view_c", "View") + .style(ButtonStyle::Filled) + .full_width() + .into_any_element(), + ), + ]) + .into_any_element(), + )], + ), + ]) + .into_any_element() } } diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index 0413891811..c287f2f846 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -1,5 +1,6 @@ use gpui::{ - div, hsla, prelude::*, AnyView, CursorStyle, ElementId, Hsla, IntoElement, Styled, Window, + div, hsla, prelude::*, AnyElement, AnyView, CursorStyle, ElementId, Hsla, IntoElement, Styled, + Window, }; use std::sync::Arc; @@ -38,7 +39,8 @@ pub enum ToggleStyle { /// Checkboxes are used for multiple choices, not for mutually exclusive choices. /// Each checkbox works independently from other checkboxes in the list, /// therefore checking an additional box does not affect any other selections. -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] +#[component(scope = "input")] pub struct Checkbox { id: ElementId, toggle_state: ToggleState, @@ -237,7 +239,8 @@ impl RenderOnce for Checkbox { } /// A [`Checkbox`] that has a [`Label`]. -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] +#[component(scope = "input")] pub struct CheckboxWithLabel { id: ElementId, label: Label, @@ -314,7 +317,8 @@ impl RenderOnce for CheckboxWithLabel { /// # Switch /// /// Switches are used to represent opposite states, such as enabled or disabled. -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] +#[component(scope = "input")] pub struct Switch { id: ElementId, toggle_state: ToggleState, @@ -446,285 +450,190 @@ impl RenderOnce for Switch { } impl ComponentPreview for Checkbox { - fn description() -> impl Into> { - "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state." - } - - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - vec![ - example_group_with_title( - "Default", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_unselected", ToggleState::Unselected), - ), - single_example( - "Indeterminate", - Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate), - ), - single_example( - "Selected", - Checkbox::new("checkbox_selected", ToggleState::Selected), - ), - ], - ), - example_group_with_title( - "Default (Filled)", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_unselected", ToggleState::Unselected).fill(), - ), - single_example( - "Indeterminate", - Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate).fill(), - ), - single_example( - "Selected", - Checkbox::new("checkbox_selected", ToggleState::Selected).fill(), - ), - ], - ), - example_group_with_title( - "ElevationBased", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_unfilled_unselected", ToggleState::Unselected) - .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), - ), - single_example( - "Indeterminate", - Checkbox::new( - "checkbox_unfilled_indeterminate", - ToggleState::Indeterminate, - ) - .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), - ), - single_example( - "Selected", - Checkbox::new("checkbox_unfilled_selected", ToggleState::Selected) - .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), - ), - ], - ), - example_group_with_title( - "ElevationBased (Filled)", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_filled_unselected", ToggleState::Unselected) - .fill() - .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), - ), - single_example( - "Indeterminate", - Checkbox::new("checkbox_filled_indeterminate", ToggleState::Indeterminate) - .fill() - .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), - ), - single_example( - "Selected", - Checkbox::new("checkbox_filled_selected", ToggleState::Selected) - .fill() - .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), - ), - ], - ), - example_group_with_title( - "Custom Color", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_custom_unselected", ToggleState::Unselected) - .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), - ), - single_example( - "Indeterminate", - Checkbox::new("checkbox_custom_indeterminate", ToggleState::Indeterminate) - .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), - ), - single_example( - "Selected", - Checkbox::new("checkbox_custom_selected", ToggleState::Selected) - .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), - ), - ], - ), - example_group_with_title( - "Custom Color (Filled)", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_custom_filled_unselected", ToggleState::Unselected) - .fill() - .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), - ), - single_example( - "Indeterminate", - Checkbox::new( - "checkbox_custom_filled_indeterminate", - ToggleState::Indeterminate, - ) - .fill() - .style(ToggleStyle::Custom(hsla( - 142.0 / 360., - 0.68, - 0.45, - 0.7, - ))), - ), - single_example( - "Selected", - Checkbox::new("checkbox_custom_filled_selected", ToggleState::Selected) - .fill() - .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), - ), - ], - ), - example_group_with_title( - "Disabled", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected) - .disabled(true), - ), - single_example( - "Indeterminate", - Checkbox::new( - "checkbox_disabled_indeterminate", - ToggleState::Indeterminate, - ) - .disabled(true), - ), - single_example( - "Selected", - Checkbox::new("checkbox_disabled_selected", ToggleState::Selected) - .disabled(true), - ), - ], - ), - example_group_with_title( - "Disabled (Filled)", - vec![ - single_example( - "Unselected", - Checkbox::new( - "checkbox_disabled_filled_unselected", - ToggleState::Unselected, - ) - .fill() - .disabled(true), - ), - single_example( - "Indeterminate", - Checkbox::new( - "checkbox_disabled_filled_indeterminate", - ToggleState::Indeterminate, - ) - .fill() - .disabled(true), - ), - single_example( - "Selected", - Checkbox::new("checkbox_disabled_filled_selected", ToggleState::Selected) - .fill() - .disabled(true), - ), - ], - ), - ] + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "States", + vec![ + single_example( + "Unselected", + Checkbox::new("checkbox_unselected", ToggleState::Unselected) + .into_any_element(), + ), + single_example( + "Indeterminate", + Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate) + .into_any_element(), + ), + single_example( + "Selected", + Checkbox::new("checkbox_selected", ToggleState::Selected) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Styles", + vec![ + single_example( + "Default", + Checkbox::new("checkbox_default", ToggleState::Selected) + .into_any_element(), + ), + single_example( + "Filled", + Checkbox::new("checkbox_filled", ToggleState::Selected) + .fill() + .into_any_element(), + ), + single_example( + "ElevationBased", + Checkbox::new("checkbox_elevation", ToggleState::Selected) + .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)) + .into_any_element(), + ), + single_example( + "Custom Color", + Checkbox::new("checkbox_custom", ToggleState::Selected) + .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Disabled", + vec![ + single_example( + "Unselected", + Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected) + .disabled(true) + .into_any_element(), + ), + single_example( + "Selected", + Checkbox::new("checkbox_disabled_selected", ToggleState::Selected) + .disabled(true) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "With Label", + vec![single_example( + "Default", + Checkbox::new("checkbox_with_label", ToggleState::Selected) + .label("Always save on quit") + .into_any_element(), + )], + ), + ]) + .into_any_element() } } impl ComponentPreview for Switch { - fn description() -> impl Into> { - "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting." - } - - fn examples(_window: &mut Window, _cx: &mut App) -> Vec> { - vec![ - example_group_with_title( - "Default", - vec![ - single_example( - "Off", - Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _, _cx| {}), - ), - single_example( - "On", - Switch::new("switch_on", ToggleState::Selected).on_click(|_, _, _cx| {}), - ), - ], - ), - example_group_with_title( - "Disabled", - vec![ - single_example( - "Off", - Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true), - ), - single_example( - "On", - Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true), - ), - ], - ), - example_group_with_title( - "Label Permutations", - vec![ - single_example( - "Label", - Switch::new("switch_with_label", ToggleState::Selected) - .label("Always save on quit"), - ), - single_example( - "Keybinding", - Switch::new("switch_with_label", ToggleState::Selected) - .key_binding(theme_preview_keybinding("cmd-shift-e")), - ), - ], - ), - ] + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "States", + vec![ + single_example( + "Off", + Switch::new("switch_off", ToggleState::Unselected) + .on_click(|_, _, _cx| {}) + .into_any_element(), + ), + single_example( + "On", + Switch::new("switch_on", ToggleState::Selected) + .on_click(|_, _, _cx| {}) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Disabled", + vec![ + single_example( + "Off", + Switch::new("switch_disabled_off", ToggleState::Unselected) + .disabled(true) + .into_any_element(), + ), + single_example( + "On", + Switch::new("switch_disabled_on", ToggleState::Selected) + .disabled(true) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "With Label", + vec![ + single_example( + "Label", + Switch::new("switch_with_label", ToggleState::Selected) + .label("Always save on quit") + .into_any_element(), + ), + // TODO: Where did theme_preview_keybinding go? + // single_example( + // "Keybinding", + // Switch::new("switch_with_keybinding", ToggleState::Selected) + // .key_binding(theme_preview_keybinding("cmd-shift-e")) + // .into_any_element(), + // ), + ], + ), + ]) + .into_any_element() } } impl ComponentPreview for CheckboxWithLabel { - fn description() -> impl Into> { - "A checkbox with an associated label, allowing users to select an option while providing a descriptive text." - } - - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - vec![example_group(vec![ - single_example( - "Unselected", - CheckboxWithLabel::new( - "checkbox_with_label_unselected", - Label::new("Always save on quit"), - ToggleState::Unselected, - |_, _, _| {}, - ), - ), - single_example( - "Indeterminate", - CheckboxWithLabel::new( - "checkbox_with_label_indeterminate", - Label::new("Always save on quit"), - ToggleState::Indeterminate, - |_, _, _| {}, - ), - ), - single_example( - "Selected", - CheckboxWithLabel::new( - "checkbox_with_label_selected", - Label::new("Always save on quit"), - ToggleState::Selected, - |_, _, _| {}, - ), - ), - ])] + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![example_group_with_title( + "States", + vec![ + single_example( + "Unselected", + CheckboxWithLabel::new( + "checkbox_with_label_unselected", + Label::new("Always save on quit"), + ToggleState::Unselected, + |_, _, _| {}, + ) + .into_any_element(), + ), + single_example( + "Indeterminate", + CheckboxWithLabel::new( + "checkbox_with_label_indeterminate", + Label::new("Always save on quit"), + ToggleState::Indeterminate, + |_, _, _| {}, + ) + .into_any_element(), + ), + single_example( + "Selected", + CheckboxWithLabel::new( + "checkbox_with_label_selected", + Label::new("Always save on quit"), + ToggleState::Selected, + |_, _, _| {}, + ) + .into_any_element(), + ), + ], + )]) + .into_any_element() } } diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index 640bb8dc90..c2a3ae69eb 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -1,12 +1,13 @@ #![allow(missing_docs)] -use gpui::{Action, AnyView, AppContext as _, FocusHandle, IntoElement, Render}; +use gpui::{Action, AnyElement, AnyView, AppContext as _, FocusHandle, IntoElement, Render}; use settings::Settings; use theme::ThemeSettings; use crate::prelude::*; use crate::{h_flex, v_flex, Color, KeyBinding, Label, LabelSize, StyledExt}; +#[derive(IntoComponent)] pub struct Tooltip { title: SharedString, meta: Option, @@ -204,3 +205,15 @@ impl Render for LinkPreview { }) } } + +impl ComponentPreview for Tooltip { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + example_group(vec![single_example( + "Text only", + Button::new("delete-example", "Delete") + .tooltip(Tooltip::text("This is a tooltip!")) + .into_any_element(), + )]) + .into_any_element() + } +} diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 6bb9d2cb40..ba02dd5aea 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -6,9 +6,11 @@ pub use gpui::{ InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, Window, }; +pub use component::{example_group, example_group_with_title, single_example, ComponentPreview}; +pub use ui_macros::IntoComponent; + pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle, StyledTypography, TextSize}; pub use crate::traits::clickable::*; -pub use crate::traits::component_preview::*; pub use crate::traits::disableable::*; pub use crate::traits::fixed::*; pub use crate::traits::styled_ext::*; diff --git a/crates/ui/src/styles/typography.rs b/crates/ui/src/styles/typography.rs index 1f6c2e9112..ec9c92cef9 100644 --- a/crates/ui/src/styles/typography.rs +++ b/crates/ui/src/styles/typography.rs @@ -1,5 +1,7 @@ +use crate::prelude::*; use gpui::{ - div, rems, App, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled, Window, + div, rems, AnyElement, App, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled, + Window, }; use settings::Settings; use theme::{ActiveTheme, ThemeSettings}; @@ -188,7 +190,7 @@ impl HeadlineSize { /// A headline element, used to emphasize some text and /// create a visual hierarchy. -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Headline { size: HeadlineSize, text: SharedString, @@ -230,3 +232,44 @@ impl Headline { self } } + +impl ComponentPreview for Headline { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![example_group_with_title( + "Headline Sizes", + vec![ + single_example( + "XLarge", + Headline::new("XLarge Headline") + .size(HeadlineSize::XLarge) + .into_any_element(), + ), + single_example( + "Large", + Headline::new("Large Headline") + .size(HeadlineSize::Large) + .into_any_element(), + ), + single_example( + "Medium (Default)", + Headline::new("Medium Headline").into_any_element(), + ), + single_example( + "Small", + Headline::new("Small Headline") + .size(HeadlineSize::Small) + .into_any_element(), + ), + single_example( + "XSmall", + Headline::new("XSmall Headline") + .size(HeadlineSize::XSmall) + .into_any_element(), + ), + ], + )]) + .into_any_element() + } +} diff --git a/crates/ui/src/traits.rs b/crates/ui/src/traits.rs index 1b4d761711..628c76aadd 100644 --- a/crates/ui/src/traits.rs +++ b/crates/ui/src/traits.rs @@ -1,5 +1,4 @@ pub mod clickable; -pub mod component_preview; pub mod disableable; pub mod fixed; pub mod styled_ext; diff --git a/crates/ui/src/traits/component_preview.rs b/crates/ui/src/traits/component_preview.rs deleted file mode 100644 index 42c6cf9e4c..0000000000 --- a/crates/ui/src/traits/component_preview.rs +++ /dev/null @@ -1,205 +0,0 @@ -#![allow(missing_docs)] -use crate::{prelude::*, KeyBinding}; -use gpui::{AnyElement, SharedString}; - -/// Which side of the preview to show labels on -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] -pub enum ExampleLabelSide { - /// Left side - Left, - /// Right side - Right, - #[default] - /// Top side - Top, - /// Bottom side - Bottom, -} - -/// Implement this trait to enable rich UI previews with metadata in the Theme Preview tool. -pub trait ComponentPreview: IntoElement { - fn title() -> &'static str { - std::any::type_name::() - } - - fn description() -> impl Into> { - None - } - - fn example_label_side() -> ExampleLabelSide { - ExampleLabelSide::default() - } - - fn examples(_window: &mut Window, _cx: &mut App) -> Vec>; - - fn custom_example(_window: &mut Window, _cx: &mut App) -> impl Into> { - None:: - } - - fn component_previews(window: &mut Window, cx: &mut App) -> Vec { - Self::examples(window, cx) - .into_iter() - .map(|example| Self::render_example_group(example)) - .collect() - } - - fn render_component_previews(window: &mut Window, cx: &mut App) -> AnyElement { - let title = Self::title(); - let (source, title) = title - .rsplit_once("::") - .map_or((None, title), |(s, t)| (Some(s), t)); - let description = Self::description().into(); - - v_flex() - .w_full() - .gap_6() - .p_4() - .border_1() - .border_color(cx.theme().colors().border) - .rounded_md() - .child( - v_flex() - .gap_1() - .child( - h_flex() - .gap_1() - .child(Headline::new(title).size(HeadlineSize::Small)) - .when_some(source, |this, source| { - this.child(Label::new(format!("({})", source)).color(Color::Muted)) - }), - ) - .when_some(description, |this, description| { - this.child( - div() - .text_ui_sm(cx) - .text_color(cx.theme().colors().text_muted) - .max_w(px(600.0)) - .child(description), - ) - }), - ) - .when_some( - Self::custom_example(window, cx).into(), - |this, custom_example| this.child(custom_example), - ) - .children(Self::component_previews(window, cx)) - .into_any_element() - } - - fn render_example_group(group: ComponentExampleGroup) -> AnyElement { - v_flex() - .gap_6() - .when(group.grow, |this| this.w_full().flex_1()) - .when_some(group.title, |this, title| { - this.child(Label::new(title).size(LabelSize::Small)) - }) - .child( - h_flex() - .w_full() - .gap_6() - .children(group.examples.into_iter().map(Self::render_example)) - .into_any_element(), - ) - .into_any_element() - } - - fn render_example(example: ComponentExample) -> AnyElement { - let base = div().flex(); - - let base = match Self::example_label_side() { - ExampleLabelSide::Right => base.flex_row(), - ExampleLabelSide::Left => base.flex_row_reverse(), - ExampleLabelSide::Bottom => base.flex_col(), - ExampleLabelSide::Top => base.flex_col_reverse(), - }; - - base.gap_1() - .when(example.grow, |this| this.flex_1()) - .child(example.element) - .child( - Label::new(example.variant_name) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .into_any_element() - } -} - -/// A single example of a component. -pub struct ComponentExample { - variant_name: SharedString, - element: T, - grow: bool, -} - -impl ComponentExample { - /// Create a new example with the given variant name and example value. - pub fn new(variant_name: impl Into, example: T) -> Self { - Self { - variant_name: variant_name.into(), - element: example, - grow: false, - } - } - - /// Set the example to grow to fill the available horizontal space. - pub fn grow(mut self) -> Self { - self.grow = true; - self - } -} - -/// A group of component examples. -pub struct ComponentExampleGroup { - pub title: Option, - pub examples: Vec>, - pub grow: bool, -} - -impl ComponentExampleGroup { - /// Create a new group of examples with the given title. - pub fn new(examples: Vec>) -> Self { - Self { - title: None, - examples, - grow: false, - } - } - - /// Create a new group of examples with the given title. - pub fn with_title(title: impl Into, examples: Vec>) -> Self { - Self { - title: Some(title.into()), - examples, - grow: false, - } - } - - /// Set the group to grow to fill the available horizontal space. - pub fn grow(mut self) -> Self { - self.grow = true; - self - } -} - -/// Create a single example -pub fn single_example(variant_name: impl Into, example: T) -> ComponentExample { - ComponentExample::new(variant_name, example) -} - -/// Create a group of examples without a title -pub fn example_group(examples: Vec>) -> ComponentExampleGroup { - ComponentExampleGroup::new(examples) -} - -/// Create a group of examples with a title -pub fn example_group_with_title( - title: impl Into, - examples: Vec>, -) -> ComponentExampleGroup { - ComponentExampleGroup::with_title(title, examples) -} - -pub fn theme_preview_keybinding(keystrokes: &str) -> KeyBinding { - KeyBinding::new(gpui::KeyBinding::new(keystrokes, gpui::NoAction {}, None)) -} diff --git a/crates/ui_macros/Cargo.toml b/crates/ui_macros/Cargo.toml index 773c07d238..cf9fef994f 100644 --- a/crates/ui_macros/Cargo.toml +++ b/crates/ui_macros/Cargo.toml @@ -13,7 +13,8 @@ path = "src/ui_macros.rs" proc-macro = true [dependencies] +convert_case.workspace = true +linkme.workspace = true proc-macro2.workspace = true quote.workspace = true syn.workspace = true -convert_case.workspace = true diff --git a/crates/ui_macros/src/derive_component.rs b/crates/ui_macros/src/derive_component.rs new file mode 100644 index 0000000000..5103d219c2 --- /dev/null +++ b/crates/ui_macros/src/derive_component.rs @@ -0,0 +1,97 @@ +use convert_case::{Case, Casing}; +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput, Lit, Meta, MetaList, MetaNameValue, NestedMeta}; + +pub fn derive_into_component(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut scope_val = None; + let mut description_val = None; + + for attr in &input.attrs { + if attr.path.is_ident("component") { + if let Ok(Meta::List(MetaList { nested, .. })) = attr.parse_meta() { + for item in nested { + if let NestedMeta::Meta(Meta::NameValue(MetaNameValue { + path, + lit: Lit::Str(s), + .. + })) = item + { + let ident = path.get_ident().map(|i| i.to_string()).unwrap_or_default(); + if ident == "scope" { + scope_val = Some(s.value()); + } else if ident == "description" { + description_val = Some(s.value()); + } + } + } + } + } + } + + let name = &input.ident; + + let scope_impl = if let Some(s) = scope_val { + quote! { + fn scope() -> Option<&'static str> { + Some(#s) + } + } + } else { + quote! { + fn scope() -> Option<&'static str> { + None + } + } + }; + + let description_impl = if let Some(desc) = description_val { + quote! { + fn description() -> Option<&'static str> { + Some(#desc) + } + } + } else { + quote! {} + }; + + let register_component_name = syn::Ident::new( + &format!( + "__register_component_{}", + Casing::to_case(&name.to_string(), Case::Snake) + ), + name.span(), + ); + let register_preview_name = syn::Ident::new( + &format!( + "__register_preview_{}", + Casing::to_case(&name.to_string(), Case::Snake) + ), + name.span(), + ); + + let expanded = quote! { + impl component::Component for #name { + #scope_impl + + fn name() -> &'static str { + stringify!(#name) + } + + #description_impl + } + + #[linkme::distributed_slice(component::__ALL_COMPONENTS)] + fn #register_component_name() { + component::register_component::<#name>(); + } + + #[linkme::distributed_slice(component::__ALL_PREVIEWS)] + fn #register_preview_name() { + component::register_preview::<#name>(); + } + }; + + expanded.into() +} diff --git a/crates/ui_macros/src/ui_macros.rs b/crates/ui_macros/src/ui_macros.rs index cd4b852766..7898f226b0 100644 --- a/crates/ui_macros/src/ui_macros.rs +++ b/crates/ui_macros/src/ui_macros.rs @@ -1,3 +1,4 @@ +mod derive_component; mod derive_path_str; mod dynamic_spacing; @@ -58,3 +59,27 @@ pub fn path_str(_args: TokenStream, input: TokenStream) -> TokenStream { pub fn derive_dynamic_spacing(input: TokenStream) -> TokenStream { dynamic_spacing::derive_spacing(input) } + +/// Derives the `Component` trait for a struct. +/// +/// This macro generates implementations for the `Component` trait and associated +/// registration functions for the component system. +/// +/// # Attributes +/// +/// - `#[component(scope = "...")]`: Required. Specifies the scope of the component. +/// - `#[component(description = "...")]`: Optional. Provides a description for the component. +/// +/// # Example +/// +/// ``` +/// use ui_macros::Component; +/// +/// #[derive(Component)] +/// #[component(scope = "toggle", description = "A element that can be toggled on and off")] +/// struct Checkbox; +/// ``` +#[proc_macro_derive(IntoComponent, attributes(component))] +pub fn derive_component(input: TokenStream) -> TokenStream { + derive_component::derive_into_component(input) +} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 81bd40970e..83ed9d5390 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -34,6 +34,7 @@ call.workspace = true client.workspace = true clock.workspace = true collections.workspace = true +component.workspace = true db.workspace = true derive_more.workspace = true fs.workspace = true diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index 656fb9a4ac..da2d6b3ff1 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -27,7 +27,6 @@ pub fn init(cx: &mut App) { enum ThemePreviewPage { Overview, Typography, - Components, } impl ThemePreviewPage { @@ -35,7 +34,6 @@ impl ThemePreviewPage { match self { Self::Overview => "Overview", Self::Typography => "Typography", - Self::Components => "Components", } } } @@ -64,9 +62,6 @@ impl ThemePreview { ThemePreviewPage::Typography => { self.render_typography_page(window, cx).into_any_element() } - ThemePreviewPage::Components => { - self.render_components_page(window, cx).into_any_element() - } } } } @@ -392,28 +387,6 @@ impl ThemePreview { ) } - fn render_components_page(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let layer = ElevationIndex::Surface; - - v_flex() - .id("theme-preview-components") - .overflow_scroll() - .size_full() - .gap_2() - .child(Button::render_component_previews(window, cx)) - .child(Checkbox::render_component_previews(window, cx)) - .child(CheckboxWithLabel::render_component_previews(window, cx)) - .child(ContentGroup::render_component_previews(window, cx)) - .child(DecoratedIcon::render_component_previews(window, cx)) - .child(Facepile::render_component_previews(window, cx)) - .child(Icon::render_component_previews(window, cx)) - .child(IconDecoration::render_component_previews(window, cx)) - .child(KeybindingHint::render_component_previews(window, cx)) - .child(Indicator::render_component_previews(window, cx)) - .child(Switch::render_component_previews(window, cx)) - .child(Table::render_component_previews(window, cx)) - } - fn render_page_nav(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .id("theme-preview-nav") diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e4087fad4f..78c950b535 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -148,6 +148,7 @@ actions!( Open, OpenFiles, OpenInTerminal, + OpenComponentPreview, ReloadActiveItem, SaveAs, SaveWithoutFormat, @@ -378,6 +379,7 @@ fn prompt_and_open_paths(app_state: Arc, options: PathPromptOptions, c pub fn init(app_state: Arc, cx: &mut App) { init_settings(cx); + component::init(); theme_preview::init(cx); cx.on_action(Workspace::close_global); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6106a382e1..bf53db6d48 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -39,6 +39,7 @@ collab_ui.workspace = true collections.workspace = true command_palette.workspace = true command_palette_hooks.workspace = true +component_preview.workspace = true copilot.workspace = true db.workspace = true diagnostics.workspace = true @@ -54,8 +55,8 @@ file_icons.workspace = true fs.workspace = true futures.workspace = true git.workspace = true -git_ui.workspace = true git_hosting_providers.workspace = true +git_ui.workspace = true go_to_line.workspace = true gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] } gpui_tokio.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 78cd4d19cd..cbd2519e60 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -490,6 +490,7 @@ fn main() { project_panel::init(Assets, cx); git_ui::git_panel::init(cx); outline_panel::init(Assets, cx); + component_preview::init(cx); tasks_ui::init(cx); snippets_ui::init(cx); channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);