Compare commits

...

13 Commits

Author SHA1 Message Date
Nate Butler
6240740605 Fix preview width eagerly growing 2024-11-10 09:01:37 -05:00
Nate Butler
2306d9fd91 Add icon preview 2024-11-10 08:13:26 -05:00
Nate Butler
e7cf1305b8 Add disclosure preview 2024-11-10 08:13:19 -05:00
Nate Butler
f699083b5b Update content box example 2024-11-10 08:13:11 -05:00
Nate Butler
956eb5b3b0 Add component preview for IconButton 2024-11-09 17:11:03 -05:00
Nate Butler
af237f81a3 Fix typo 2024-11-09 17:10:51 -05:00
Nate Butler
d6b1949143 Remove log 2024-11-09 17:10:45 -05:00
Nate Butler
e5c738daaa Fix incorrect content box default 2024-11-09 17:10:38 -05:00
Nate Butler
eba035eb5b register facepille 2024-11-09 17:04:12 -05:00
Nate Butler
1c65253b0f Initialize ui component registry 2024-11-09 08:53:43 -05:00
Nate Butler
5d4bc1e492 component registry first approach 2024-11-09 08:21:56 -05:00
Nate Butler
dc84dbb6e2 Move linkme to the cargo workspace 2024-11-09 08:12:38 -05:00
Nate Butler
903343e608 Add ContentBox 2024-11-09 08:12:13 -05:00
20 changed files with 511 additions and 53 deletions

3
Cargo.lock generated
View File

@@ -13262,9 +13262,12 @@ name = "ui"
version = "0.1.0"
dependencies = [
"chrono",
"collections",
"gpui",
"itertools 0.13.0",
"linkme",
"menu",
"once_cell",
"serde",
"settings",
"smallvec",

View File

@@ -371,6 +371,7 @@ itertools = "0.13.0"
jsonwebtoken = "9.3"
libc = "0.2"
linkify = "0.10.0"
linkme = "0.3"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"

View File

@@ -80,7 +80,7 @@ gpui_macros.workspace = true
http_client = { optional = true, workspace = true }
image = "0.25.1"
itertools.workspace = true
linkme = "0.3"
linkme.workspace = true
log.workspace = true
num_cpus = "1.13"
parking = "2.0.0"

View File

@@ -14,9 +14,12 @@ path = "src/ui.rs"
[dependencies]
chrono.workspace = true
collections.workspace = true
gpui.workspace = true
itertools = { workspace = true, optional = true }
linkme.workspace = true
menu.workspace = true
once_cell.workspace = true
serde.workspace = true
settings.workspace = true
smallvec.workspace = true

View File

@@ -0,0 +1,57 @@
use collections::HashMap;
use gpui::{AnyElement, WindowContext};
use once_cell::sync::Lazy;
use std::sync::Mutex;
pub type ComponentPreviewFn = fn(&WindowContext) -> AnyElement;
static COMPONENTS: Lazy<Mutex<HashMap<&'static str, Vec<(&'static str, ComponentPreviewFn)>>>> =
Lazy::new(|| Mutex::new(HashMap::default()));
pub fn register_component(scope: &'static str, name: &'static str, preview: ComponentPreviewFn) {
let mut components = COMPONENTS.lock().unwrap();
components
.entry(scope)
.or_insert_with(Vec::new)
.push((name, preview));
}
/// Initializes all components that have been registered
/// in the UI component registry.
pub fn init_component_registry() {
for register in __COMPONENT_REGISTRATIONS {
register();
}
}
/// Returns a map of all registered components and their previews.
pub fn get_all_component_previews() -> HashMap<&'static str, Vec<(&'static str, ComponentPreviewFn)>>
{
COMPONENTS.lock().unwrap().clone()
}
#[doc(hidden)]
#[linkme::distributed_slice]
pub static __COMPONENT_REGISTRATIONS: [fn()];
/// Defines components that should be registered in the component registry.
///
/// This allows components to be previewed, and eventually tracked for documentation
/// purposes and to help the systems team to understand component usage across the codebase.
#[macro_export]
macro_rules! register_components {
($scope:ident, [ $($component:ty),+ $(,)? ]) => {
const _: () = {
#[linkme::distributed_slice($crate::component_registry::__COMPONENT_REGISTRATIONS)]
fn register() {
$(
$crate::component_registry::register_component(
stringify!($scope),
stringify!($component),
|cx: &$crate::WindowContext| <$component>::render_component_previews(cx),
);
)+
}
};
};
}

View File

@@ -1,6 +1,7 @@
mod avatar;
mod button;
mod checkbox;
mod content_box;
mod context_menu;
mod disclosure;
mod divider;
@@ -36,6 +37,7 @@ mod stories;
pub use avatar::*;
pub use button::*;
pub use checkbox::*;
pub use content_box::*;
pub use context_menu::*;
pub use disclosure::*;
pub use divider::*;

View File

@@ -1,13 +1,17 @@
#![allow(missing_docs)]
use crate::internal::prelude::*;
use gpui::{AnyView, DefiniteLength};
use crate::{prelude::*, ElevationIndex, IconPosition, KeyBinding, Spacing, TintColor};
use crate::{
ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label, LineHeightStyle,
};
use crate::{ElevationIndex, IconPosition, KeyBinding, Spacing, TintColor};
use super::button_icon::ButtonIcon;
register_components!(button, [Button]);
/// An element that creates a button with a label and an optional icon.
///
/// Common buttons:

View File

@@ -1,12 +1,14 @@
#![allow(missing_docs)]
use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle};
use crate::internal::prelude::*;
use crate::{ElevationIndex, SelectableButton, Tooltip};
use crate::{IconName, IconSize};
use gpui::{AnyView, DefiniteLength};
use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle};
use crate::{prelude::*, ElevationIndex, SelectableButton};
use crate::{IconName, IconSize};
use super::button_icon::ButtonIcon;
register_components!(button, [IconButton]);
/// The shape of an [`IconButton`].
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum IconButtonShape {
@@ -165,3 +167,76 @@ impl RenderOnce for IconButton {
)
}
}
impl ComponentPreview for IconButton {
fn description() -> impl Into<Option<&'static str>> {
"An IconButton is a button that displays only an icon. It's used for actions that can be represented by a single icon."
}
fn examples() -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Basic",
vec![
single_example("Default", IconButton::new("default", IconName::Check)),
single_example(
"Selected",
IconButton::new("selected", IconName::Check).selected(true),
),
single_example(
"Disabled",
IconButton::new("disabled", IconName::Check).disabled(true),
),
],
),
example_group_with_title(
"Shapes",
vec![
single_example(
"Square",
IconButton::new("square", IconName::Check).shape(IconButtonShape::Square),
),
single_example(
"Wide",
IconButton::new("wide", IconName::Check).shape(IconButtonShape::Wide),
),
],
),
example_group_with_title(
"Sizes",
vec![
single_example(
"XSmall",
IconButton::new("xsmall", IconName::Check).icon_size(IconSize::XSmall),
),
single_example(
"Small",
IconButton::new("small", IconName::Check).icon_size(IconSize::Small),
),
single_example(
"Medium",
IconButton::new("medium", IconName::Check).icon_size(IconSize::Medium),
),
],
),
example_group_with_title(
"Icon Color",
vec![
single_example("Default", IconButton::new("default_color", IconName::Check)),
single_example(
"Custom",
IconButton::new("custom_color", IconName::Check).icon_color(Color::Success),
),
],
),
example_group_with_title(
"With Tooltip",
vec![single_example(
"Tooltip",
IconButton::new("tooltip", IconName::Check)
.tooltip(|cx| Tooltip::text("This is a tooltip", cx)),
)],
),
]
}
}

View File

@@ -1,9 +1,10 @@
#![allow(missing_docs)]
use crate::internal::prelude::*;
use crate::{Color, Icon, IconName, Selection};
use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
use crate::prelude::*;
use crate::{Color, Icon, IconName, Selection};
register_components!(checkbox, [Checkbox, CheckboxWithLabel]);
/// # Checkbox
///

View File

@@ -0,0 +1,118 @@
use crate::internal::prelude::*;
use gpui::{AnyElement, IntoElement, ParentElement, StyleRefinement, Styled};
use smallvec::SmallVec;
register_components!(layout, [ContentBox]);
/// A flexible container component that can hold other elements.
#[derive(IntoElement)]
pub struct ContentBox {
base: Div,
border: bool,
fill: bool,
children: SmallVec<[AnyElement; 2]>,
}
impl ContentBox {
/// Creates a new [ContentBox].
pub fn new() -> Self {
Self {
base: div(),
border: true,
fill: true,
children: SmallVec::new(),
}
}
/// Removes the border from the [ContentBox].
pub fn borderless(mut self) -> Self {
self.border = false;
self
}
/// Removes the background fill from the [ContentBox].
pub fn unfilled(mut self) -> Self {
self.fill = false;
self
}
}
impl ParentElement for ContentBox {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl Styled for ContentBox {
fn style(&mut self) -> &mut StyleRefinement {
self.base.style()
}
}
impl RenderOnce for ContentBox {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
// TODO:
// Baked in padding will make scrollable views inside of content boxes awkward.
//
// Do we make the padding optional, or do we push to use a different component?
self.base
.when(self.fill, |this| {
this.bg(cx.theme().colors().text.opacity(0.05))
})
.when(self.border, |this| {
this.border_1().border_color(cx.theme().colors().border)
})
.rounded_md()
.p_2()
.children(self.children)
}
}
impl ComponentPreview for ContentBox {
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() -> Vec<ComponentExampleGroup<Self>> {
vec![example_group(vec![
single_example(
"Default",
ContentBox::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.child(Label::new("Default ContentBox")),
)
.grow(),
single_example(
"Without Border",
ContentBox::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.borderless()
.child(Label::new("Borderless ContentBox")),
)
.grow(),
single_example(
"Without Fill",
ContentBox::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.unfilled()
.child(Label::new("Unfilled ContentBox")),
)
.grow(),
])
.grow()]
}
}

View File

@@ -1,10 +1,13 @@
#![allow(missing_docs)]
use crate::internal::prelude::*;
use gpui::{ClickEvent, CursorStyle};
use std::sync::Arc;
use gpui::{ClickEvent, CursorStyle};
use crate::{Color, IconButton, IconButtonShape, IconName, IconSize};
use crate::{prelude::*, Color, IconButton, IconButtonShape, IconName, IconSize};
register_components!(disclosure, [Disclosure]);
// TODO: This should be DisclosureControl, not Disclosure
#[derive(IntoElement)]
pub struct Disclosure {
id: ElementId,
@@ -71,3 +74,20 @@ impl RenderOnce for Disclosure {
})
}
}
impl ComponentPreview for Disclosure {
fn description() -> impl Into<Option<&'static str>> {
"A Disclosure component is used to show or hide content. It's typically used in expandable/collapsible sections or tree-like structures."
}
fn examples() -> Vec<ComponentExampleGroup<Self>> {
vec![example_group(vec![
single_example("Closed", Disclosure::new("closed", false)),
single_example("Open", Disclosure::new("open", true)),
single_example(
"Open (Selected)",
Disclosure::new("open", true).selected(true),
),
])]
}
}

View File

@@ -1,7 +1,10 @@
use crate::{prelude::*, Avatar};
use crate::internal::prelude::*;
use crate::Avatar;
use gpui::{AnyElement, StyleRefinement};
use smallvec::SmallVec;
register_components!(facepile, [Facepile]);
/// A facepile is a collection of faces stacked horizontally
/// always with the leftmost face on top and descending in z-index
///

View File

@@ -1,14 +1,13 @@
#![allow(missing_docs)]
use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation};
use crate::internal::prelude::*;
use crate::Indicator;
use gpui::{svg, AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation};
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString, IntoStaticStr};
use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr};
use ui_macros::DerivePathStr;
use crate::{
prelude::*,
traits::component_preview::{ComponentExample, ComponentPreview},
Indicator,
};
register_components!(icon, [Icon, DecoratedIcon, IconWithIndicator]);
#[derive(IntoElement)]
pub enum AnyIcon {
@@ -501,24 +500,147 @@ impl RenderOnce for IconWithIndicator {
}
impl ComponentPreview for Icon {
fn examples() -> Vec<ComponentExampleGroup<Icon>> {
let arrow_icons = vec![
IconName::ArrowDown,
IconName::ArrowLeft,
IconName::ArrowRight,
IconName::ArrowUp,
IconName::ArrowCircle,
];
fn description() -> impl Into<Option<&'static str>> {
"Icons are visual symbols used to represent ideas, objects, or actions. They communicate messages at a glance, enhance aesthetic appeal, and are used in buttons, labels, and more."
}
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 custom_example(cx: &WindowContext) -> impl Into<Option<AnyElement>> {
let all_icons = IconName::iter().collect::<Vec<_>>();
let chunk_size = 12;
Some(
v_flex()
.gap_4()
.children(all_icons.chunks(chunk_size).map(|chunk| {
h_flex().gap_4().children(chunk.iter().map(|&icon| {
div()
.flex()
.flex_1()
.justify_center()
.items_center()
.size_8()
.border_1()
.border_color(cx.theme().colors().border)
.child(Icon::new(icon))
}))
}))
.into_any_element(),
)
}
fn examples() -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Sizes",
vec![
single_example("XSmall", Icon::new(IconName::Check).size(IconSize::XSmall)),
single_example("Small", Icon::new(IconName::Check).size(IconSize::Small)),
single_example("Medium", Icon::new(IconName::Check).size(IconSize::Medium)),
],
),
example_group_with_title(
"Colors",
vec![
single_example("Default", Icon::new(IconName::Check)),
single_example("Accent", Icon::new(IconName::Check).color(Color::Accent)),
single_example("Error", Icon::new(IconName::Check).color(Color::Error)),
],
),
]
}
}
impl ComponentPreview for DecoratedIcon {
fn description() -> impl Into<Option<&'static str>> {
"DecoratedIcon adds visual enhancements to an icon, such as a strikethrough or an indicator dot."
}
fn examples() -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Decorations",
vec![
single_example(
"Strikethrough",
DecoratedIcon::new(
Icon::new(IconName::Bell),
IconDecoration::Strikethrough,
),
),
single_example(
"IndicatorDot",
DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::IndicatorDot),
),
single_example(
"X",
DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::X),
),
],
),
example_group_with_title(
"Colors",
vec![
single_example(
"Default",
DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::IndicatorDot),
),
single_example(
"Custom Color",
DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::IndicatorDot)
.decoration_color(Color::Accent),
),
],
),
]
}
}
impl ComponentPreview for IconWithIndicator {
fn description() -> impl Into<Option<&'static str>> {
"IconWithIndicator combines an icon with an indicator, useful for showing status or notifications."
}
fn examples() -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Indicator Types",
vec![
single_example(
"Dot",
IconWithIndicator::new(Icon::new(IconName::Bell), Some(Indicator::dot())),
),
single_example(
"Bar",
IconWithIndicator::new(Icon::new(IconName::Bell), Some(Indicator::bar())),
),
],
),
example_group_with_title(
"Indicator Colors",
vec![
single_example(
"Info",
IconWithIndicator::new(
Icon::new(IconName::Bell),
Some(Indicator::dot().color(Color::Info)),
),
),
single_example(
"Warning",
IconWithIndicator::new(
Icon::new(IconName::Bell),
Some(Indicator::dot().color(Color::Warning)),
),
),
single_example(
"Error",
IconWithIndicator::new(
Icon::new(IconName::Bell),
Some(Indicator::dot().color(Color::Error)),
),
),
],
),
]
}
}

View File

@@ -1,5 +1,8 @@
#![allow(missing_docs)]
use crate::{prelude::*, AnyIcon};
use crate::internal::prelude::*;
use crate::AnyIcon;
register_components!(indicator, [Indicator]);
#[derive(Default)]
enum IndicatorKind {

View File

@@ -1,4 +1,6 @@
use crate::{prelude::*, Indicator};
use crate::internal::prelude::*;
use crate::Indicator;
use gpui::{div, AnyElement, FontWeight, IntoElement, Length};
/// A table component

View File

@@ -9,7 +9,6 @@ pub use gpui::{
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::selectable::*;

View File

@@ -1,8 +1,8 @@
#![allow(missing_docs)]
use crate::prelude::*;
use gpui::{AnyElement, SharedString};
/// Which side of the preview to show labels on
#[allow(unused)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExampleLabelSide {
/// Left side
@@ -32,6 +32,10 @@ pub trait ComponentPreview: IntoElement {
fn examples() -> Vec<ComponentExampleGroup<Self>>;
fn custom_example(_cx: &WindowContext) -> impl Into<Option<AnyElement>> {
None::<AnyElement>
}
fn component_previews() -> Vec<AnyElement> {
Self::examples()
.into_iter()
@@ -47,7 +51,8 @@ pub trait ComponentPreview: IntoElement {
let description = Self::description().into();
v_flex()
.gap_3()
.w_full()
.gap_6()
.p_4()
.border_1()
.border_color(cx.theme().colors().border)
@@ -73,18 +78,23 @@ pub trait ComponentPreview: IntoElement {
)
}),
)
.when_some(Self::custom_example(cx).into(), |this, custom_example| {
this.child(custom_example)
})
.children(Self::component_previews())
.into_any_element()
}
fn render_example_group(group: ComponentExampleGroup<Self>) -> AnyElement {
v_flex()
.gap_2()
.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(),
@@ -103,6 +113,7 @@ pub trait ComponentPreview: IntoElement {
};
base.gap_1()
.when(example.grow, |this| this.flex_1())
.child(example.element)
.child(
Label::new(example.variant_name)
@@ -117,6 +128,7 @@ pub trait ComponentPreview: IntoElement {
pub struct ComponentExample<T> {
variant_name: SharedString,
element: T,
grow: bool,
}
impl<T> ComponentExample<T> {
@@ -125,14 +137,22 @@ impl<T> ComponentExample<T> {
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> {
@@ -141,15 +161,24 @@ impl<T> ComponentExampleGroup<T> {
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

View File

@@ -10,6 +10,7 @@
//! - [`ui_input`] - the single line input component
//!
mod component_registry;
mod components;
pub mod prelude;
mod styles;
@@ -17,6 +18,17 @@ mod tests;
mod traits;
pub mod utils;
pub use component_registry::{get_all_component_previews, init_component_registry};
pub use components::*;
pub use prelude::*;
pub use styles::*;
pub(crate) mod internal {
/// A crate-internal extension of the prelude, used to expose the crate-specific
/// needs like the component registry or component-preview types
pub mod prelude {
pub use crate::prelude::*;
pub use crate::register_components;
pub use crate::traits::component_preview::*;
}
}

View File

@@ -502,22 +502,24 @@ impl ThemePreview {
}
fn render_components_page(&self, cx: &ViewContext<Self>) -> impl IntoElement {
let layer = ElevationIndex::Surface;
let all_previews = ui::get_all_component_previews();
v_flex()
.id("theme-preview-components")
.overflow_scroll()
.size_full()
.gap_2()
.child(Checkbox::render_component_previews(cx))
.child(CheckboxWithLabel::render_component_previews(cx))
.child(Facepile::render_component_previews(cx))
.child(Button::render_component_previews(cx))
.child(Indicator::render_component_previews(cx))
.child(Icon::render_component_previews(cx))
.child(Table::render_component_previews(cx))
.child(self.render_avatars(cx))
.child(self.render_buttons(layer, cx))
.gap_4()
.children(all_previews.iter().map(|(scope, components)| {
v_flex()
.gap_2()
.child(Headline::new(*scope).size(HeadlineSize::Small))
.children(components.iter().map(|(name, preview_fn)| {
v_flex()
.gap_1()
.child(Label::new(*name).size(LabelSize::Small).color(Color::Muted))
.child(preview_fn(cx))
}))
}))
}
fn render_page_nav(&self, cx: &ViewContext<Self>) -> impl IntoElement {

View File

@@ -92,6 +92,8 @@ pub fn init(cx: &mut AppContext) {
if ReleaseChannel::global(cx) == ReleaseChannel::Dev {
cx.on_action(test_panic);
}
ui::init_component_registry();
}
pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) -> WindowOptions {