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

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