component: Add component and component_preview crates to power UI components (#24456)

This PR formalizes design components with the Component and
ComponentPreview traits.

You can open the preview UI with `workspace: open component preview`.

Component previews no longer need to return `Self` allowing for more
complex previews, and previews of components like `ui::Tooltip` that
supplement other components rather than are rendered by default.

`cargo-machete` incorrectly identifies `linkme` as an unused dep on
crates that have components deriving `IntoComponent`, so you may need to
add this to that crate's `Cargo.toml`:

```toml
# cargo-machete doesn't understand that linkme is used in the component macro
[package.metadata.cargo-machete]
ignored = ["linkme"]
```

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
This commit is contained in:
Nate Butler 2025-02-09 13:25:03 -05:00 committed by GitHub
parent 56cfc60875
commit 8f1ff189cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1582 additions and 976 deletions

51
Cargo.lock generated
View file

@ -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",

View file

@ -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",

View file

@ -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 = []

View file

@ -0,0 +1 @@
../../LICENSE-GPL

View file

@ -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::<Self>()
}
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<RwLock<ComponentRegistry>> =
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<T: Component>() {
let component_data = (T::scope(), T::name(), T::description());
COMPONENT_DATA.write().components.push(component_data);
}
pub fn register_preview<T: ComponentPreview>() {
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<SharedString>,
description: Option<SharedString>,
preview: Option<fn(&mut Window, &App) -> AnyElement>,
}
impl ComponentMetadata {
pub fn name(&self) -> SharedString {
self.name.clone()
}
pub fn scope(&self) -> Option<SharedString> {
self.scope.clone()
}
pub fn description(&self) -> Option<SharedString> {
self.description.clone()
}
pub fn preview(&self) -> Option<fn(&mut Window, &App) -> AnyElement> {
self.preview
}
}
pub struct AllComponents(pub HashMap<ComponentId, ComponentMetadata>);
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<ComponentMetadata> {
let mut previews: Vec<ComponentMetadata> =
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<ComponentMetadata> {
let mut components: Vec<ComponentMetadata> = self.all().into_iter().cloned().collect();
components.sort_by_key(|a| a.name());
components
}
}
impl Deref for AllComponents {
type Target = HashMap<ComponentId, ComponentMetadata>;
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<SharedString>, 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<SharedString>,
pub examples: Vec<ComponentExample>,
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<ComponentExample>) -> Self {
Self {
title: None,
examples,
grow: false,
}
}
/// Create a new group of examples with the given title.
pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> 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<SharedString>,
example: AnyElement,
) -> ComponentExample {
ComponentExample::new(variant_name, example)
}
/// Create a group of examples without a title
pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
ComponentExampleGroup::new(examples)
}
/// Create a group of examples with a title
pub fn example_group_with_title(
title: impl Into<SharedString>,
examples: Vec<ComponentExample>,
) -> ComponentExampleGroup {
ComponentExampleGroup::with_title(title, examples)
}

View file

@ -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

View file

@ -0,0 +1 @@
../../LICENSE-GPL

View file

@ -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 {
Self {
focus_handle: cx.focus_handle(),
}
}
fn render_sidebar(&self, _window: &Window, _cx: &Context<Self>) -> 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<Self>,
) -> 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<Self>,
) -> 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<Self>) -> 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<ItemEvent> 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<SharedString> {
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<WorkspaceId>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<gpui::Entity<Self>>
where
Self: Sized,
{
Some(cx.new(Self::new))
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
f(*event)
}
}

View file

@ -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"]

View file

@ -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<AbsoluteLength>,
@ -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()
}
}

View file

@ -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<Option<&'static str>> {
"A button allows users to take actions, and make choices, with a single tap."
}
fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
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()
}
}

View file

@ -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<Option<&'static str>> {
"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<ComponentExampleGroup<Self>> {
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()
}
}

View file

@ -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<Option<&'static str>> {
"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<ComponentExampleGroup<Self>> {
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<Option<&'static str>> {
// "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<ComponentExampleGroup<Self>> {
// 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(),
// ),
// ),
// ],
// )]
// }
// }

View file

@ -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<ComponentExampleGroup<Icon>> {
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()
}
}

View file

@ -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<IconDecoration>,
@ -27,12 +25,7 @@ impl RenderOnce for DecoratedIcon {
}
impl ComponentPreview for DecoratedIcon {
fn examples(_: &mut Window, cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
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()
}
}

View file

@ -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<ComponentExampleGroup<Self>> {
let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
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)]
}
}

View file

@ -83,34 +83,3 @@ impl RenderOnce for Indicator {
}
}
}
impl ComponentPreview for Indicator {
fn description() -> impl Into<Option<&'static str>> {
"An indicator visually represents a status or state."
}
fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
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)),
],
),
]
}
}

View file

@ -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<SharedString>,
suffix: Option<SharedString>,
@ -206,102 +206,99 @@ impl RenderOnce for KeybindingHint {
}
impl ComponentPreview for KeybindingHint {
fn description() -> impl Into<Option<&'static str>> {
"Used to display hint text for keyboard shortcuts. Can have a prefix and suffix."
}
fn examples(window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
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()
}
}

View file

@ -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()
}
}

View file

@ -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,

View file

@ -27,7 +27,7 @@ pub enum TabCloseSide {
End,
}
#[derive(IntoElement)]
#[derive(IntoElement, IntoComponent)]
pub struct Tab {
div: Stateful<Div>,
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()
}
}

View file

@ -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<SharedString>,
rows: Vec<Vec<TableCell>>,
@ -152,88 +152,110 @@ where
}
impl ComponentPreview for Table {
fn description() -> impl Into<Option<&'static str>> {
"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<ComponentExampleGroup<Self>> {
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()
}
}

View file

@ -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<Option<&'static str>> {
"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<ComponentExampleGroup<Self>> {
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<Option<&'static str>> {
"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<ComponentExampleGroup<Self>> {
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<Option<&'static str>> {
"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<ComponentExampleGroup<Self>> {
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()
}
}

View file

@ -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<SharedString>,
@ -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()
}
}

View file

@ -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::*;

View file

@ -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()
}
}

View file

@ -1,5 +1,4 @@
pub mod clickable;
pub mod component_preview;
pub mod disableable;
pub mod fixed;
pub mod styled_ext;

View file

@ -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::<Self>()
}
fn description() -> impl Into<Option<&'static str>> {
None
}
fn example_label_side() -> ExampleLabelSide {
ExampleLabelSide::default()
}
fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>>;
fn custom_example(_window: &mut Window, _cx: &mut App) -> impl Into<Option<AnyElement>> {
None::<AnyElement>
}
fn component_previews(window: &mut Window, cx: &mut App) -> Vec<AnyElement> {
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<Self>) -> 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<Self>) -> 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<T> {
variant_name: SharedString,
element: T,
grow: bool,
}
impl<T> ComponentExample<T> {
/// Create a new example with the given variant name and example value.
pub fn new(variant_name: impl Into<SharedString>, 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<T> {
pub title: Option<SharedString>,
pub examples: Vec<ComponentExample<T>>,
pub grow: bool,
}
impl<T> ComponentExampleGroup<T> {
/// Create a new group of examples with the given title.
pub fn new(examples: Vec<ComponentExample<T>>) -> Self {
Self {
title: None,
examples,
grow: false,
}
}
/// Create a new group of examples with the given title.
pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample<T>>) -> 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<T>(variant_name: impl Into<SharedString>, example: T) -> ComponentExample<T> {
ComponentExample::new(variant_name, example)
}
/// Create a group of examples without a title
pub fn example_group<T>(examples: Vec<ComponentExample<T>>) -> ComponentExampleGroup<T> {
ComponentExampleGroup::new(examples)
}
/// Create a group of examples with a title
pub fn example_group_with_title<T>(
title: impl Into<SharedString>,
examples: Vec<ComponentExample<T>>,
) -> ComponentExampleGroup<T> {
ComponentExampleGroup::with_title(title, examples)
}
pub fn theme_preview_keybinding(keystrokes: &str) -> KeyBinding {
KeyBinding::new(gpui::KeyBinding::new(keystrokes, gpui::NoAction {}, None))
}

View file

@ -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

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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

View file

@ -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<Self>) -> impl IntoElement {
h_flex()
.id("theme-preview-nav")

View file

@ -148,6 +148,7 @@ actions!(
Open,
OpenFiles,
OpenInTerminal,
OpenComponentPreview,
ReloadActiveItem,
SaveAs,
SaveWithoutFormat,
@ -378,6 +379,7 @@ fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, c
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
init_settings(cx);
component::init();
theme_preview::init(cx);
cx.on_action(Workspace::close_global);

View file

@ -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

View file

@ -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);