Compare commits

...

19 Commits

Author SHA1 Message Date
Nate Butler
f8103b9ffa Checkpoint 2024-12-10 23:39:34 -05:00
Nate Butler
7719afeafa Checkpoint 2024-12-10 23:31:07 -05:00
Nate Butler
27b25100c9 Checkpoint 2024-12-10 23:18:13 -05:00
Nate Butler
4d16a3a2a8 Checkpoint 2024-12-10 23:02:47 -05:00
Nate Butler
d36230e46e wip 2024-12-10 19:31:37 -05:00
Nate Butler
8dddbf31bf remove unused 2024-12-10 19:12:30 -05:00
Nate Butler
0c8bb20424 Remove unused ui::Component trait 2024-12-09 16:08:48 -05:00
Nate Butler
5edb86511b component_system & component preview 2024-12-09 15:55:26 -05:00
Nate Butler
7e75244620 wip 2024-12-09 15:02:41 -05:00
Nate Butler
175290c43a wip 2024-12-09 14:53:44 -05:00
Nate Butler
27c7541e5b Clean up unused 2024-12-09 13:58:41 -05:00
Nate Butler
ae9f51d0e0 update checkbox preview 2024-12-09 13:57:26 -05:00
Nate Butler
f609429ac4 Update component preview 2024-12-09 13:37:03 -05:00
Nate Butler
7cbf651cf9 Add component preview util 2024-12-09 12:50:25 -05:00
Nate Butler
81475ef182 wip 2024-12-09 12:23:17 -05:00
Nate Butler
9ce110f9ad Checkpoint 2024-12-06 13:11:09 -05:00
Nate Butler
f500de9c3b Checkpoint 2024-12-06 12:33:44 -05:00
Nate Butler
9d810a44fd Checkpoint 2024-12-06 10:42:40 -05:00
Nate Butler
3248bb9172 Checkpoint 2024-12-06 07:54:26 -05:00
31 changed files with 971 additions and 909 deletions

31
Cargo.lock generated
View File

@@ -2862,6 +2862,29 @@ dependencies = [
"gpui",
]
[[package]]
name = "component_preview"
version = "0.1.0"
dependencies = [
"component_system",
"gpui",
"strum 0.25.0",
"ui",
"workspace",
]
[[package]]
name = "component_system"
version = "0.1.0"
dependencies = [
"collections",
"gpui",
"lazy_static",
"linkme",
"once_cell",
"parking_lot",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -13809,9 +13832,14 @@ name = "ui"
version = "0.1.0"
dependencies = [
"chrono",
"collections",
"component_system",
"gpui",
"itertools 0.13.0",
"lazy_static",
"linkme",
"menu",
"once_cell",
"serde",
"settings",
"smallvec",
@@ -13837,6 +13865,7 @@ dependencies = [
name = "ui_macros"
version = "0.1.0"
dependencies = [
"component_system",
"convert_case 0.6.0",
"proc-macro2",
"quote",
@@ -15620,6 +15649,7 @@ dependencies = [
"client",
"clock",
"collections",
"component_system",
"db",
"derive_more",
"env_logger 0.11.5",
@@ -16051,6 +16081,7 @@ dependencies = [
"collections",
"command_palette",
"command_palette_hooks",
"component_preview",
"copilot",
"db",
"diagnostics",

View File

@@ -21,6 +21,8 @@ members = [
"crates/collab",
"crates/collab_ui",
"crates/collections",
"crates/component_preview",
"crates/component_system",
"crates/command_palette",
"crates/command_palette_hooks",
"crates/context_server",
@@ -207,6 +209,8 @@ clock = { path = "crates/clock" }
collab = { path = "crates/collab" }
collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections" }
component_system = { path = "crates/component_system" }
component_preview = { path = "crates/component_preview" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
context_server = { path = "crates/context_server" }
@@ -393,9 +397,11 @@ itertools = "0.13.0"
jsonwebtoken = "9.3"
jupyter-protocol = { version = "0.5.0" }
jupyter-websocket-client = { version = "0.8.0" }
lazy_static = "1.5.0"
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
linkme = "0.3"
livekit = { git = "https://github.com/zed-industries/rust-sdks", rev="799f10133d93ba2a88642cd480d01ec4da53408c", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"

View File

@@ -0,0 +1,22 @@
[package]
name = "component_preview"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/component_preview.rs"
[dependencies]
gpui.workspace = true
ui.workspace = true
component_system.workspace = true
strum.workspace = true
workspace.workspace = true
[features]
default = []

View File

@@ -0,0 +1,110 @@
//! # Component Preview
//!
//! A view for exploring Zed components.
use component_system::components;
use gpui::{prelude::*, AppContext, EventEmitter, FocusHandle, FocusableView};
use strum::{EnumIter, IntoEnumIterator};
use ui::{prelude::*, TintColor};
use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace.register_action(|workspace, _: &workspace::ComponentPreview, cx| {
let component_preview = cx.new_view(ComponentPreview::new);
workspace.add_item_to_active_pane(Box::new(component_preview), None, true, cx)
});
})
.detach();
}
struct ComponentPreview {
focus_handle: FocusHandle,
}
impl ComponentPreview {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
fn render_sidebar(&self, cx: &ViewContext<Self>) -> impl IntoElement {
h_flex().children(components().all().iter().map(|component| {
Button::new(component.name().clone(), component.name()).on_click(cx.listener(
move |_this, _, _cx| {
// Handle button click
},
))
}))
}
fn render_preview(&self, cx: &ViewContext<Self>) -> impl IntoElement {
v_flex()
.size_full()
.children(components().all_previews().iter().map(|component| {
if let Some(preview) = component.preview() {
preview(cx)
} else {
div().into_any_element()
}
}))
}
}
impl Render for ComponentPreview {
fn render(&mut self, cx: &mut ViewContext<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(cx))
.child(self.render_preview(cx))
}
}
impl EventEmitter<ItemEvent> for ComponentPreview {}
impl FocusableView for ComponentPreview {
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Item for ComponentPreview {
type Event = ItemEvent;
fn tab_content_text(&self, _cx: &WindowContext) -> 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>,
cx: &mut ViewContext<Self>,
) -> Option<gpui::View<Self>>
where
Self: Sized,
{
Some(cx.new_view(Self::new))
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
f(*event)
}
}

View File

@@ -0,0 +1,23 @@
[package]
name = "component_system"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/component_system.rs"
[dependencies]
gpui.workspace = true
lazy_static.workspace = true
linkme.workspace = true
once_cell.workspace = true
collections.workspace = true
parking_lot.workspace = true
[features]
default = []

View File

@@ -0,0 +1,142 @@
use collections::HashMap;
use gpui::{AnyElement, SharedString, WindowContext};
use linkme::distributed_slice;
use once_cell::sync::Lazy;
use parking_lot::RwLock;
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(_cx: &WindowContext) -> AnyElement;
}
#[distributed_slice]
pub static __ALL_COMPONENTS: [fn()] = [..];
#[distributed_slice]
pub static __ALL_PREVIEWS: [fn()] = [..];
pub static COMPONENT_DATA: Lazy<RwLock<ComponentData>> =
Lazy::new(|| RwLock::new(ComponentData::new()));
pub struct ComponentData {
components: Vec<(Option<&'static str>, &'static str, Option<&'static str>)>,
previews: HashMap<&'static str, fn(&WindowContext) -> AnyElement>,
}
impl ComponentData {
fn new() -> Self {
ComponentData {
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(&WindowContext) -> AnyElement);
COMPONENT_DATA
.write()
.previews
.insert(preview_data.0, preview_data.1);
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ComponentId(pub &'static str);
pub struct ComponentMetadata {
name: SharedString,
scope: Option<SharedString>,
description: Option<SharedString>,
preview: Option<fn(&WindowContext) -> 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(&WindowContext) -> AnyElement> {
self.preview
}
}
pub struct AllComponents(pub HashMap<ComponentId, ComponentMetadata>);
impl AllComponents {
pub fn new() -> Self {
AllComponents(HashMap::default())
}
pub fn add(&mut self, id: ComponentId, metadata: ComponentMetadata) {
self.0.insert(id, metadata);
}
pub fn get(&self, id: &ComponentId) -> Option<&ComponentMetadata> {
self.0.get(id)
}
/// 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
pub fn all(&self) -> Vec<&ComponentMetadata> {
self.0.values().collect()
}
}
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.add(
ComponentId(name),
ComponentMetadata {
name: name.into(),
scope,
description: description.map(Into::into),
preview,
},
);
}
all_components
}

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,8 +14,12 @@ path = "src/ui.rs"
[dependencies]
chrono.workspace = true
collections.workspace = true
gpui.workspace = true
itertools = { workspace = true, optional = true }
lazy_static.workspace = true
linkme.workspace = true
once_cell.workspace = true
menu.workspace = true
serde.workspace = true
settings.workspace = true
@@ -24,6 +28,7 @@ story = { workspace = true, optional = true }
strum = { workspace = true, features = ["derive"] }
theme.workspace = true
ui_macros.workspace = true
component_system.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View File

@@ -1,6 +1,12 @@
use crate::prelude::*;
use crate::{
prelude::*,
utils::{component_preview, component_preview_group},
Indicator,
};
use component_system::ComponentPreview;
use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
use ui_macros::IntoComponent;
/// An element that renders a user avatar with customizable appearance options.
///
@@ -14,7 +20,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 +102,35 @@ impl RenderOnce for Avatar {
.children(self.indicator.map(|indicator| div().child(indicator)))
}
}
impl ComponentPreview for Avatar {
fn preview(cx: &WindowContext) -> AnyElement {
let avatar_1 = "https://avatars.githubusercontent.com/u/1789?s=70&v=4";
let avatar_2 = "https://avatars.githubusercontent.com/u/482957?s=70&v=4";
let avatar_3 = "https://avatars.githubusercontent.com/u/326587?s=70&v=4";
v_flex()
.gap_4()
.child(
component_preview_group()
.child(component_preview("Default").child(Avatar::new(avatar_1)))
.child(
component_preview("Custom Size").child(Avatar::new(avatar_2).size(px(48.))),
)
.child(
component_preview("Grayscale").child(Avatar::new(avatar_3).grayscale(true)),
),
)
.child(
component_preview_group()
.child(
component_preview("With Border")
.child(Avatar::new(avatar_1).border_color(cx.theme().colors().border)),
)
.child(component_preview("With Indicator").child(
Avatar::new(avatar_2).indicator(Indicator::dot().color(Color::Success)),
)),
)
.into_any_element()
}
}

View File

@@ -1,5 +1,5 @@
#![allow(missing_docs)]
use gpui::{AnyView, DefiniteLength};
use gpui::{AnyElement, AnyView, DefiniteLength};
use crate::{
prelude::*, Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding, TintColor,
@@ -440,102 +440,87 @@ 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."
}
// register_components!(input, [Button]);
fn examples(_: &WindowContext) -> 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(
"Negative",
Button::new("tinted_negative", "Negative")
.style(ButtonStyle::Tinted(TintColor::Negative)),
),
single_example(
"Warning",
Button::new("tinted_warning", "Warning")
.style(ButtonStyle::Tinted(TintColor::Warning)),
),
single_example(
"Positive",
Button::new("tinted_positive", "Positive")
.style(ButtonStyle::Tinted(TintColor::Positive)),
),
],
),
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").selected(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("icon_color", "Delete")
.style(ButtonStyle::Tinted(TintColor::Negative))
.color(Color::Error)
.icon_color(Color::Error)
.icon(IconName::Trash)
.icon_position(IconPosition::Start),
),
],
),
]
}
}
// impl ComponentElement for Button {
// fn scope() -> &'static str {
// "input"
// }
// fn description() -> impl Into<Option<&'static str>> {
// "A button allows users to take actions, and make choices, with a single tap."
// }
// fn preview(_cx: &WindowContext) -> Option<AnyElement> {
// Some(
// v_flex()
// .gap_4()
// .child(
// h_flex()
// .gap_4()
// .child(Button::new("default", "Default"))
// .child(Button::new("filled", "Filled").style(ButtonStyle::Filled))
// .child(Button::new("subtle", "Subtle").style(ButtonStyle::Subtle))
// .child(
// Button::new("transparent", "Transparent")
// .style(ButtonStyle::Transparent),
// ),
// )
// .child(
// h_flex()
// .gap_4()
// .child(
// Button::new("tinted_accent", "Accent")
// .style(ButtonStyle::Tinted(TintColor::Accent)),
// )
// .child(
// Button::new("tinted_negative", "Negative")
// .style(ButtonStyle::Tinted(TintColor::Negative)),
// )
// .child(
// Button::new("tinted_warning", "Warning")
// .style(ButtonStyle::Tinted(TintColor::Warning)),
// )
// .child(
// Button::new("tinted_positive", "Positive")
// .style(ButtonStyle::Tinted(TintColor::Positive)),
// ),
// )
// .child(
// h_flex()
// .gap_4()
// .child(Button::new("default_state", "Default"))
// .child(Button::new("disabled", "Disabled").disabled(true))
// .child(Button::new("selected", "Selected").selected(true)),
// )
// .child(
// h_flex()
// .gap_4()
// .child(
// Button::new("icon_start", "Icon Start")
// .icon(IconName::Check)
// .icon_position(IconPosition::Start),
// )
// .child(
// Button::new("icon_end", "Icon End")
// .icon(IconName::Check)
// .icon_position(IconPosition::End),
// )
// .child(
// Button::new("icon_color", "Icon Color")
// .icon(IconName::Check)
// .icon_color(Color::Accent),
// )
// .child(
// Button::new("icon_color", "Delete")
// .style(ButtonStyle::Tinted(TintColor::Negative))
// .color(Color::Error)
// .icon_color(Color::Error)
// .icon(IconName::Trash)
// .icon_position(IconPosition::Start),
// ),
// )
// .into_any_element(),
// )
// }
// }

View File

@@ -1,5 +1,7 @@
#![allow(missing_docs)]
use crate::{prelude::*, Icon, IconName, IconSize};
use gpui::AnyElement;
use crate::{prelude::*, Icon, IconName, IconSize, TintColor};
/// An icon that appears within a button.
///
@@ -99,3 +101,53 @@ impl RenderOnce for ButtonIcon {
Icon::new(icon).size(self.size).color(icon_color)
}
}
// register_components!(input, [ButtonIcon]);
// impl ComponentElement for ButtonIcon {
// fn scope() -> &'static str {
// "input"
// }
// fn description() -> impl Into<Option<&'static str>> {
// "A ButtonIcon is an icon that appears within a button, used either alongside a label or as a standalone icon."
// }
// fn preview(_cx: &WindowContext) -> Option<AnyElement> {
// Some(
// v_flex()
// .gap_4()
// .child(
// h_flex()
// .gap_4()
// .child(ButtonIcon::new(IconName::Check))
// .child(ButtonIcon::new(IconName::Check).color(Color::Accent))
// .child(ButtonIcon::new(IconName::Check).size(IconSize::XSmall))
// .child(ButtonIcon::new(IconName::Check).size(IconSize::Small)),
// )
// .child(
// h_flex()
// .gap_4()
// .child(ButtonIcon::new(IconName::Check).selected(true))
// .child(ButtonIcon::new(IconName::Check).disabled(true))
// .child(ButtonIcon::new(IconName::Check).selected_icon(IconName::X))
// .child(ButtonIcon::new(IconName::Check).selected_icon_color(Color::Error)),
// )
// .child(
// h_flex()
// .gap_4()
// .child(ButtonIcon::new(IconName::Check).selected_style(ButtonStyle::Filled))
// .child(ButtonIcon::new(IconName::Check).selected_style(ButtonStyle::Subtle))
// .child(
// ButtonIcon::new(IconName::Check)
// .selected_style(ButtonStyle::Transparent),
// )
// .child(
// ButtonIcon::new(IconName::Check)
// .selected_style(ButtonStyle::Tinted(TintColor::Accent)),
// ),
// )
// .into_any_element(),
// )
// }
// }

View File

@@ -1,8 +1,8 @@
#![allow(missing_docs)]
use gpui::{AnyView, DefiniteLength};
use gpui::{AnyElement, AnyView, DefiniteLength};
use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle};
use crate::{prelude::*, ElevationIndex, SelectableButton};
use crate::{prelude::*, ElevationIndex, SelectableButton, TintColor};
use crate::{IconName, IconSize};
use super::button_icon::ButtonIcon;
@@ -165,3 +165,86 @@ impl RenderOnce for IconButton {
)
}
}
// register_components!(input, [IconButton]);
// impl ComponentElement for IconButton {
// fn scope() -> &'static str {
// "input"
// }
// fn description() -> impl Into<Option<&'static str>> {
// "An icon button allows users to take actions with a single tap, represented by an icon."
// }
// fn preview(_cx: &WindowContext) -> Option<AnyElement> {
// Some(
// v_flex()
// .gap_4()
// .child(
// h_flex()
// .gap_4()
// .child(IconButton::new("default", IconName::Check))
// .child(
// IconButton::new("filled", IconName::Check).style(ButtonStyle::Filled),
// )
// .child(
// IconButton::new("subtle", IconName::Check).style(ButtonStyle::Subtle),
// )
// .child(
// IconButton::new("transparent", IconName::Check)
// .style(ButtonStyle::Transparent),
// ),
// )
// .child(
// h_flex()
// .gap_4()
// .child(
// IconButton::new("tinted_accent", IconName::Check)
// .style(ButtonStyle::Tinted(TintColor::Accent)),
// )
// .child(
// IconButton::new("tinted_negative", IconName::Check)
// .style(ButtonStyle::Tinted(TintColor::Negative)),
// )
// .child(
// IconButton::new("tinted_warning", IconName::Check)
// .style(ButtonStyle::Tinted(TintColor::Warning)),
// )
// .child(
// IconButton::new("tinted_positive", IconName::Check)
// .style(ButtonStyle::Tinted(TintColor::Positive)),
// ),
// )
// .child(
// h_flex()
// .gap_4()
// .child(IconButton::new("default_state", IconName::Check))
// .child(IconButton::new("disabled", IconName::Check).disabled(true))
// .child(IconButton::new("selected", IconName::Check).selected(true)),
// )
// .child(
// h_flex()
// .gap_4()
// .child(
// IconButton::new("icon_size_small", IconName::Check)
// .icon_size(IconSize::Small),
// )
// .child(
// IconButton::new("icon_size_medium", IconName::Check)
// .icon_size(IconSize::Medium),
// )
// .child(
// IconButton::new("icon_color", IconName::Check)
// .icon_color(Color::Accent),
// )
// .child(
// IconButton::new("icon_delete", IconName::Trash)
// .style(ButtonStyle::Tinted(TintColor::Negative))
// .icon_color(Color::Error),
// ),
// )
// .into_any_element(),
// )
// }
// }

View File

@@ -1,9 +1,8 @@
#![allow(missing_docs)]
use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
use crate::prelude::*;
use crate::{Color, Icon, IconName, Selection};
use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
use std::sync::Arc;
/// # Checkbox
///
@@ -113,55 +112,38 @@ impl RenderOnce for Checkbox {
}
}
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."
}
// impl ComponentElement for Checkbox {
// fn description() -> impl Into<Option<&'static str>> {
// "A checkbox allows people to toggle between two states, typically representing on/off, or a pair of opposites."
// }
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Default",
vec![
single_example(
"Unselected",
Checkbox::new("checkbox_unselected", Selection::Unselected),
),
single_example(
"Indeterminate",
Checkbox::new("checkbox_indeterminate", Selection::Indeterminate),
),
single_example(
"Selected",
Checkbox::new("checkbox_selected", Selection::Selected),
),
],
),
example_group_with_title(
"Disabled",
vec![
single_example(
"Unselected",
Checkbox::new("checkbox_disabled_unselected", Selection::Unselected)
.disabled(true),
),
single_example(
"Indeterminate",
Checkbox::new("checkbox_disabled_indeterminate", Selection::Indeterminate)
.disabled(true),
),
single_example(
"Selected",
Checkbox::new("checkbox_disabled_selected", Selection::Selected)
.disabled(true),
),
],
),
]
}
}
// fn scope() -> &'static str {
// "input"
// }
use std::sync::Arc;
// fn preview(_cx: &WindowContext) -> Option<gpui::AnyElement> {
// Some(
// component_preview_group()
// .child(
// component_preview("Default")
// .child(Checkbox::new("checkbox-1", Selection::Unselected)),
// )
// .child(
// component_preview("Selected")
// .child(Checkbox::new("checkbox-2", Selection::Selected)),
// )
// .child(
// component_preview("Indeterminate")
// .child(Checkbox::new("checkbox-3", Selection::Indeterminate)),
// )
// .child(
// component_preview("Disabled")
// .child(Checkbox::new("checkbox-4", Selection::Selected).disabled(true)),
// )
// .into_any_element(),
// )
// }
// }
/// A [`Checkbox`] that has a [`Label`].
#[derive(IntoElement)]
@@ -209,40 +191,64 @@ impl RenderOnce for CheckboxWithLabel {
}
}
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."
}
// impl ComponentElement for CheckboxWithLabel {
// fn description() -> impl Into<Option<&'static str>> {
// "A checkbox with an associated label."
// }
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![example_group(vec![
single_example(
"Unselected",
CheckboxWithLabel::new(
"checkbox_with_label_unselected",
Label::new("Always save on quit"),
Selection::Unselected,
|_, _| {},
),
),
single_example(
"Indeterminate",
CheckboxWithLabel::new(
"checkbox_with_label_indeterminate",
Label::new("Always save on quit"),
Selection::Indeterminate,
|_, _| {},
),
),
single_example(
"Selected",
CheckboxWithLabel::new(
"checkbox_with_label_selected",
Label::new("Always save on quit"),
Selection::Selected,
|_, _| {},
),
),
])]
}
}
// fn scope() -> &'static str {
// "input"
// }
// fn preview(_cx: &WindowContext) -> Option<gpui::AnyElement> {
// Some(
// v_flex()
// .gap_3()
// .child(
// component_preview_group()
// .child(component_preview("Default").child(CheckboxWithLabel::new(
// "checkbox-1",
// Label::new("Show Completions"),
// Selection::Unselected,
// |_, _| {},
// )))
// .child(component_preview("Selected").child(CheckboxWithLabel::new(
// "checkbox-2",
// Label::new("Show Completions"),
// Selection::Selected,
// |_, _| {},
// ))),
// )
// .child(
// component_preview_group().child(
// component_preview("Indeterminate").child(
// v_flex()
// .child(CheckboxWithLabel::new(
// "checkbox-3",
// Label::new("Show Completions"),
// Selection::Indeterminate,
// |_, _| {},
// ))
// .child(h_flex().child(div().w_5().h_full()).child(
// CheckboxWithLabel::new(
// "checkbox-4",
// Label::new("Editor"),
// Selection::Selected,
// |_, _| {},
// ),
// ))
// .child(h_flex().child(div().w_5().h_full()).child(
// CheckboxWithLabel::new(
// "checkbox-5",
// Label::new("Assistant"),
// Selection::Unselected,
// |_, _| {},
// ),
// )),
// ),
// ),
// )
// .into_any_element(),
// )
// }
// }

View File

@@ -26,7 +26,9 @@ pub fn h_group() -> ContentGroup {
pub struct ContentGroup {
base: Div,
border: bool,
padding: Pixels,
fill: bool,
outset_amount: Option<AbsoluteLength>,
children: SmallVec<[AnyElement; 2]>,
}
@@ -36,7 +38,9 @@ impl ContentGroup {
Self {
base: div(),
border: true,
padding: px(8.),
fill: true,
outset_amount: None,
children: SmallVec::new(),
}
}
@@ -52,6 +56,25 @@ impl ContentGroup {
self.fill = false;
self
}
/// Outset the [ContentBox] to create a visual "outset" effect.
///
/// This sets a default outset amount which matches the width of the border and padding,
/// useful for lining up content with other elements.
pub fn outset(mut self) -> Self {
let border_width = if self.border { px(1.) } else { px(0.) };
let outset_amount = self.padding + border_width;
self.outset_amount = Some(outset_amount.into());
self
}
/// Sets the amount of outset to apply to the [ContentBox].
///
/// This will add negative left and right margin to the [ContentBox] to create a visual "outset" effect.
pub fn outset_amount(mut self, amount: impl Into<AbsoluteLength>) -> Self {
self.outset_amount = Some(amount.into());
self
}
}
impl ParentElement for ContentGroup {
@@ -81,55 +104,14 @@ impl RenderOnce for ContentGroup {
this.border_1().border_color(cx.theme().colors().border)
})
.rounded_md()
.p_2()
.p(self.padding)
.when_some(self.outset_amount, |this, amount| {
let outset_rems: Rems = match amount {
AbsoluteLength::Pixels(p) => rems(p / cx.rem_size()),
AbsoluteLength::Rems(r) => r,
};
this.mx(-outset_rems)
})
.children(self.children)
}
}
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(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![example_group(vec![
single_example(
"Default",
ContentGroup::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.child(Label::new("Default ContentBox")),
)
.grow(),
single_example(
"Without Border",
ContentGroup::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.borderless()
.child(Label::new("Borderless ContentBox")),
)
.grow(),
single_example(
"Without Fill",
ContentGroup::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.unfilled()
.child(Label::new("Unfilled ContentBox")),
)
.grow(),
])
.grow()]
}
}

View File

@@ -1,4 +1,4 @@
use crate::{prelude::*, Avatar};
use crate::prelude::*;
use gpui::{AnyElement, StyleRefinement};
use smallvec::SmallVec;
@@ -59,61 +59,3 @@ 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(_: &WindowContext) -> 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",
];
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

@@ -1,14 +1,10 @@
#![allow(missing_docs)]
use gpui::{svg, AnimationElement, Hsla, IntoElement, Point, Rems, Transformation};
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr};
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 {
@@ -491,26 +487,6 @@ impl RenderOnce for IconDecoration {
}
}
impl ComponentPreview for IconDecoration {
fn examples(cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
let examples = all_kinds
.iter()
.map(|kind| {
let name = format!("{:?}", kind).to_string();
single_example(
name,
IconDecoration::new(*kind, cx.theme().colors().surface_background, cx),
)
})
.collect();
vec![example_group(examples)]
}
}
#[derive(IntoElement)]
pub struct DecoratedIcon {
icon: Icon,
@@ -533,66 +509,6 @@ impl RenderOnce for DecoratedIcon {
}
}
impl ComponentPreview for DecoratedIcon {
fn examples(cx: &WindowContext) -> 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);
let decoration_x = IconDecoration::new(
IconDecorationKind::X,
cx.theme().colors().surface_background,
cx,
)
.color(cx.theme().status().error)
.position(Point {
x: px(-2.),
y: px(-2.),
});
let decoration_triangle = IconDecoration::new(
IconDecorationKind::Triangle,
cx.theme().colors().surface_background,
cx,
)
.color(cx.theme().status().error)
.position(Point {
x: px(-2.),
y: px(-2.),
});
let decoration_dot = IconDecoration::new(
IconDecorationKind::Dot,
cx.theme().colors().surface_background,
cx,
)
.color(cx.theme().status().error)
.position(Point {
x: px(-2.),
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)]
}
}
#[derive(IntoElement)]
pub struct IconWithIndicator {
icon: Icon,
@@ -651,26 +567,3 @@ impl RenderOnce for IconWithIndicator {
})
}
}
impl ComponentPreview for Icon {
fn examples(_cx: &WindowContext) -> 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(),
)]
}
}

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(_: &WindowContext) -> 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,4 +1,4 @@
use crate::{prelude::*, Indicator};
use crate::prelude::*;
use gpui::{div, AnyElement, FontWeight, IntoElement, Length};
/// A table component
@@ -150,90 +150,3 @@ where
TableCell::String(e.into())
}
}
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(_: &WindowContext) -> 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"]),
),
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(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(),
),
]),
)],
),
]
}
}

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,5 +1,4 @@
pub mod clickable;
pub mod component_preview;
pub mod disableable;
pub mod fixed;
pub mod selectable;

View File

@@ -1,200 +0,0 @@
#![allow(missing_docs)]
use crate::prelude::*;
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(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>>;
fn custom_example(_cx: &WindowContext) -> impl Into<Option<AnyElement>> {
None::<AnyElement>
}
fn component_previews(cx: &WindowContext) -> Vec<AnyElement> {
Self::examples(cx)
.into_iter()
.map(|example| Self::render_example_group(example))
.collect()
}
fn render_component_previews(cx: &WindowContext) -> 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(cx).into(), |this, custom_example| {
this.child(custom_example)
})
.children(Self::component_previews(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)
}

View File

@@ -1,11 +1,13 @@
//! UI-related utilities
mod color_contrast;
mod component_preview;
mod format_distance;
mod search_input;
mod with_rem_size;
pub use color_contrast::*;
pub use component_preview::*;
pub use format_distance::*;
pub use search_input::*;
pub use with_rem_size::*;

View File

@@ -0,0 +1,119 @@
use crate::prelude::*;
use gpui::{AnyElement, Axis};
use smallvec::SmallVec;
/// A component preview with a label and children.
#[derive(IntoElement)]
pub struct ComponentPreview {
label: Option<SharedString>,
children: SmallVec<[AnyElement; 2]>,
}
impl ComponentPreview {
/// Creates a new ComponentPreview with the specified label side.
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: Some(label.into()),
children: SmallVec::new(),
}
}
/// Creates a new ComponentPreview with no label.
pub fn no_label() -> Self {
Self {
label: None,
children: SmallVec::new(),
}
}
/// Sets the label for the ComponentPreview.
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
}
impl RenderOnce for ComponentPreview {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
v_flex()
.flex_none()
.gap_3()
.when_some(self.label, |this, label| {
this.child(Label::new(label).color(Color::Muted))
})
.child(
h_flex()
.gap_1()
.w_full()
.flex_none()
.children(self.children),
)
}
}
impl ParentElement for ComponentPreview {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
/// A group of component previews.
#[derive(IntoElement)]
pub struct ComponentPreviewGroup {
direction: Axis,
children: SmallVec<[AnyElement; 2]>,
}
impl ComponentPreviewGroup {
/// Creates a new ComponentPreviewGroup.
pub fn new() -> Self {
Self {
direction: Axis::Horizontal,
children: SmallVec::new(),
}
}
/// Lay out the previews horizontally.
pub fn horizontal(mut self) -> Self {
self.direction = Axis::Horizontal;
self
}
/// Lay out the previews vertically.
pub fn vertical(mut self) -> Self {
self.direction = Axis::Vertical;
self
}
}
impl ParentElement for ComponentPreviewGroup {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl RenderOnce for ComponentPreviewGroup {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let group = match self.direction {
Axis::Horizontal => h_group(),
Axis::Vertical => v_group(),
};
group
.size_full()
.items_start()
.outset()
.gap_3()
.children(self.children)
}
}
/// Creates a new [ComponentPreview]
pub fn component_preview(label: impl Into<SharedString>) -> ComponentPreview {
ComponentPreview::new(label)
}
/// Creates a new [ComponentPreviewGroup]
pub fn component_preview_group() -> ComponentPreviewGroup {
ComponentPreviewGroup::new()
}

View File

@@ -17,3 +17,4 @@ proc-macro2 = "1.0.66"
quote = "1.0.9"
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
convert_case.workspace = true
component_system.workspace = true

View File

@@ -0,0 +1,81 @@
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 expanded = quote! {
impl component_system::Component for #name {
#scope_impl
fn name() -> &'static str {
stringify!(#name)
}
#description_impl
}
#[linkme::distributed_slice(component_system::__ALL_COMPONENTS)]
fn __register_component() {
component_system::register_component::<#name>();
}
#[linkme::distributed_slice(component_system::__ALL_PREVIEWS)]
fn __register_preview() {
component_system::register_preview::<#name>();
}
};
expanded.into()
}

View File

@@ -1,3 +1,4 @@
mod derive_into_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 `IntoComponent` 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::IntoComponent;
///
/// #[derive(IntoComponent)]
/// #[component(scope = "my_scope", description = "A sample component")]
/// struct MyComponent;
/// ```
#[proc_macro_derive(IntoComponent, attributes(component))]
pub fn derive_into_component(input: TokenStream) -> TokenStream {
derive_into_component::derive_into_component(input)
}

View File

@@ -28,14 +28,15 @@ livekit-macos = ["call/livekit-macos"]
livekit-cross-platform = ["call/livekit-cross-platform"]
[dependencies]
anyhow.workspace = true
any_vec.workspace = true
anyhow.workspace = true
async-recursion.workspace = true
bincode = "1.2.1"
call.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
component_system.workspace = true
db.workspace = true
derive_more.workspace = true
fs.workspace = true
@@ -49,7 +50,6 @@ node_runtime.workspace = true
parking_lot.workspace = true
postage.workspace = true
project.workspace = true
task.workspace = true
remote.workspace = true
schemars.workspace = true
serde.workspace = true
@@ -58,11 +58,12 @@ session.workspace = true
settings.workspace = true
smallvec.workspace = true
sqlez.workspace = true
strum.workspace = true
task.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
strum.workspace = true
[dev-dependencies]
call = { workspace = true, features = ["test-support"] }

View File

@@ -1,12 +1,10 @@
#![allow(unused, dead_code)]
use gpui::{actions, hsla, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, Hsla};
use gpui::{actions, AppContext, EventEmitter, FocusHandle, FocusableView, Hsla};
use strum::IntoEnumIterator;
use theme::all_theme_colors;
use ui::{
element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus,
Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
Checkbox, CheckboxWithLabel, ContentGroup, DecoratedIcon, ElevationIndex, Facepile,
IconDecoration, Indicator, Table, TintColor, Tooltip,
prelude::*, utils::calculate_contrast_ratio, AudioStatus, Availability, Avatar,
AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, ElevationIndex, Facepile,
TintColor, Tooltip,
};
use crate::{Item, Workspace};
@@ -27,7 +25,6 @@ pub fn init(cx: &mut AppContext) {
enum ThemePreviewPage {
Overview,
Typography,
Components,
}
impl ThemePreviewPage {
@@ -35,7 +32,6 @@ impl ThemePreviewPage {
match self {
Self::Overview => "Overview",
Self::Typography => "Typography",
Self::Components => "Components",
}
}
}
@@ -61,7 +57,6 @@ impl ThemePreview {
match page {
ThemePreviewPage::Overview => self.render_overview_page(cx).into_any_element(),
ThemePreviewPage::Typography => self.render_typography_page(cx).into_any_element(),
ThemePreviewPage::Components => self.render_components_page(cx).into_any_element(),
}
}
}
@@ -101,154 +96,11 @@ impl Item for ThemePreview {
}
}
const AVATAR_URL: &str = "https://avatars.githubusercontent.com/u/1714999?v=4";
impl ThemePreview {
fn preview_bg(cx: &WindowContext) -> Hsla {
cx.theme().colors().editor_background
}
fn render_avatars(&self, cx: &ViewContext<Self>) -> impl IntoElement {
v_flex()
.gap_1()
.child(
Headline::new("Avatars")
.size(HeadlineSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
.items_start()
.gap_4()
.child(Avatar::new(AVATAR_URL).size(px(24.)))
.child(Avatar::new(AVATAR_URL).size(px(24.)).grayscale(true))
.child(
Avatar::new(AVATAR_URL)
.size(px(24.))
.indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)),
)
.child(
Avatar::new(AVATAR_URL)
.size(px(24.))
.indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)),
)
.child(
Avatar::new(AVATAR_URL)
.size(px(24.))
.indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
)
.child(
Avatar::new(AVATAR_URL)
.size(px(24.))
.indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
)
.child(
Facepile::empty()
.child(
Avatar::new(AVATAR_URL)
.border_color(Self::preview_bg(cx))
.size(px(22.))
.into_any_element(),
)
.child(
Avatar::new(AVATAR_URL)
.border_color(Self::preview_bg(cx))
.size(px(22.))
.into_any_element(),
)
.child(
Avatar::new(AVATAR_URL)
.border_color(Self::preview_bg(cx))
.size(px(22.))
.into_any_element(),
)
.child(
Avatar::new(AVATAR_URL)
.border_color(Self::preview_bg(cx))
.size(px(22.))
.into_any_element(),
),
),
)
}
fn render_buttons(&self, layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
v_flex()
.gap_1()
.child(
Headline::new("Buttons")
.size(HeadlineSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
.items_start()
.gap_px()
.child(
IconButton::new("icon_button_transparent", IconName::Check)
.style(ButtonStyle::Transparent),
)
.child(
IconButton::new("icon_button_subtle", IconName::Check)
.style(ButtonStyle::Subtle),
)
.child(
IconButton::new("icon_button_filled", IconName::Check)
.style(ButtonStyle::Filled),
)
.child(
IconButton::new("icon_button_selected_accent", IconName::Check)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.selected(true),
)
.child(IconButton::new("icon_button_selected", IconName::Check).selected(true))
.child(
IconButton::new("icon_button_positive", IconName::Check)
.style(ButtonStyle::Tinted(TintColor::Positive)),
)
.child(
IconButton::new("icon_button_warning", IconName::Check)
.style(ButtonStyle::Tinted(TintColor::Warning)),
)
.child(
IconButton::new("icon_button_negative", IconName::Check)
.style(ButtonStyle::Tinted(TintColor::Negative)),
),
)
.child(
h_flex()
.gap_px()
.child(
Button::new("button_transparent", "Transparent")
.style(ButtonStyle::Transparent),
)
.child(Button::new("button_subtle", "Subtle").style(ButtonStyle::Subtle))
.child(Button::new("button_filled", "Filled").style(ButtonStyle::Filled))
.child(
Button::new("button_selected", "Selected")
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.selected(true),
)
.child(
Button::new("button_selected_tinted", "Selected (Tinted)")
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.selected(true),
)
.child(
Button::new("button_positive", "Tint::Positive")
.style(ButtonStyle::Tinted(TintColor::Positive)),
)
.child(
Button::new("button_warning", "Tint::Warning")
.style(ButtonStyle::Tinted(TintColor::Warning)),
)
.child(
Button::new("button_negative", "Tint::Negative")
.style(ButtonStyle::Tinted(TintColor::Negative)),
),
)
}
fn render_text(&self, layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
let bg = layer.bg(cx);
@@ -410,8 +262,7 @@ impl ThemePreview {
)
}
fn render_colors(&self, layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
let bg = layer.bg(cx);
fn render_colors(&self, _layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
let all_colors = all_theme_colors(cx);
v_flex()
@@ -502,28 +353,6 @@ impl ThemePreview {
)
}
fn render_components_page(&self, cx: &ViewContext<Self>) -> impl IntoElement {
let layer = ElevationIndex::Surface;
v_flex()
.id("theme-preview-components")
.overflow_scroll()
.size_full()
.gap_2()
.child(ContentGroup::render_component_previews(cx))
.child(IconDecoration::render_component_previews(cx))
.child(DecoratedIcon::render_component_previews(cx))
.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))
}
fn render_page_nav(&self, cx: &ViewContext<Self>) -> impl IntoElement {
h_flex()
.id("theme-preview-nav")

View File

@@ -143,12 +143,13 @@ actions!(
FollowNextCollaborator,
NewCenterTerminal,
NewFile,
NewFileSplitVertical,
NewFileSplitHorizontal,
NewFileSplitVertical,
NewSearch,
NewTerminal,
NewWindow,
Open,
ComponentPreview,
OpenInTerminal,
ReloadActiveItem,
SaveAs,
@@ -323,6 +324,7 @@ pub fn init_settings(cx: &mut AppContext) {
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
init_settings(cx);
notifications::init(cx);
component_system::init();
theme_preview::init(cx);
cx.on_action(Workspace::close_global);

View File

@@ -37,6 +37,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

View File

@@ -461,6 +461,7 @@ fn main() {
feedback::init(cx);
markdown_preview::init(cx);
welcome::init(cx);
component_preview::init(cx);
settings_ui::init(cx);
extensions_ui::init(cx);