Begin setting up stories
This commit is contained in:
parent
4b793f44ef
commit
a05cbf8169
17 changed files with 471 additions and 1 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -7606,6 +7606,7 @@ dependencies = [
|
|||
"serde",
|
||||
"settings",
|
||||
"simplelog",
|
||||
"smallvec",
|
||||
"strum",
|
||||
"theme",
|
||||
"util",
|
||||
|
|
|
@ -25,6 +25,18 @@ impl<V: 'static> IntoAnyElement<V> for &'static str {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Figure out how to pass `String` to `child` without this.
|
||||
// This impl doesn't exist in the `gpui2` crate.
|
||||
impl<S: 'static> IntoAnyElement<S> for String {
|
||||
fn into_any(self) -> AnyElement<S> {
|
||||
Text {
|
||||
text: ArcCow::from(self),
|
||||
state_type: PhantomData,
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Text<S> {
|
||||
text: ArcCow<'static, str>,
|
||||
state_type: PhantomData<S>,
|
||||
|
|
|
@ -20,6 +20,7 @@ rust-embed.workspace = true
|
|||
serde.workspace = true
|
||||
settings = { path = "../settings" }
|
||||
simplelog = "0.9"
|
||||
smallvec.workspace = true
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
|
|
3
crates/storybook2/src/stories.rs
Normal file
3
crates/storybook2/src/stories.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod components;
|
||||
pub mod elements;
|
||||
pub mod kitchen_sink;
|
1
crates/storybook2/src/stories/components.rs
Normal file
1
crates/storybook2/src/stories/components.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod panel;
|
35
crates/storybook2/src/stories/components/panel.rs
Normal file
35
crates/storybook2/src/stories/components/panel.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use crate::ui::prelude::*;
|
||||
use crate::ui::{Label, Panel};
|
||||
|
||||
use crate::story::Story;
|
||||
|
||||
#[derive(Element)]
|
||||
pub struct PanelStory<S: 'static + Send + Sync> {
|
||||
state_type: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync> PanelStory<S> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Panel<S>>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(Panel::new(
|
||||
ScrollState::default(),
|
||||
|_, _| {
|
||||
vec![div()
|
||||
.overflow_y_scroll(ScrollState::default())
|
||||
.children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1))))
|
||||
.into_any()]
|
||||
},
|
||||
Box::new(()),
|
||||
))
|
||||
}
|
||||
}
|
1
crates/storybook2/src/stories/elements.rs
Normal file
1
crates/storybook2/src/stories/elements.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod label;
|
28
crates/storybook2/src/stories/elements/label.rs
Normal file
28
crates/storybook2/src/stories/elements/label.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use crate::ui::prelude::*;
|
||||
use crate::ui::Label;
|
||||
|
||||
use crate::story::Story;
|
||||
|
||||
#[derive(Element)]
|
||||
pub struct LabelStory<S: 'static + Send + Sync> {
|
||||
state_type: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync> LabelStory<S> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Label<S>>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(Label::new("Hello, world!"))
|
||||
.child(Story::label(cx, "Highlighted"))
|
||||
.child(Label::new("Hello, world!").with_highlights(vec![0, 1, 2, 7, 8, 12]))
|
||||
}
|
||||
}
|
36
crates/storybook2/src/stories/kitchen_sink.rs
Normal file
36
crates/storybook2/src/stories/kitchen_sink.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::story::Story;
|
||||
use crate::story_selector::{ComponentStory, ElementStory};
|
||||
use crate::ui::prelude::*;
|
||||
|
||||
#[derive(Element)]
|
||||
pub struct KitchenSinkStory<S: 'static + Send + Sync> {
|
||||
state_type: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync> KitchenSinkStory<S> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
let element_stories = ElementStory::iter().map(|selector| selector.story());
|
||||
let component_stories = ComponentStory::iter().map(|selector| selector.story());
|
||||
|
||||
Story::container(cx)
|
||||
.overflow_y_scroll(ScrollState::default())
|
||||
.child(Story::title(cx, "Kitchen Sink"))
|
||||
.child(Story::label(cx, "Elements"))
|
||||
.child(div().flex().flex_col().children_any(element_stories))
|
||||
.child(Story::label(cx, "Components"))
|
||||
.child(div().flex().flex_col().children_any(component_stories))
|
||||
// Add a bit of space at the bottom of the kitchen sink so elements
|
||||
// don't end up squished right up against the bottom of the screen.
|
||||
.child(div().p_4())
|
||||
}
|
||||
}
|
52
crates/storybook2/src/story.rs
Normal file
52
crates/storybook2/src/story.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use crate::theme::theme;
|
||||
use crate::ui::prelude::*;
|
||||
use gpui3::Div;
|
||||
|
||||
pub struct Story {}
|
||||
|
||||
impl Story {
|
||||
pub fn container<S: 'static + Send + Sync>(cx: &mut ViewContext<S>) -> Div<S> {
|
||||
let theme = theme(cx);
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.pt_2()
|
||||
.px_4()
|
||||
.font("Zed Mono Extended")
|
||||
.fill(theme.lowest.base.default.background)
|
||||
}
|
||||
|
||||
pub fn title<S: 'static + Send + Sync>(
|
||||
cx: &mut ViewContext<S>,
|
||||
title: &str,
|
||||
) -> impl Element<State = S> {
|
||||
let theme = theme(cx);
|
||||
|
||||
div()
|
||||
.text_xl()
|
||||
.text_color(theme.lowest.base.default.foreground)
|
||||
.child(title.to_owned())
|
||||
}
|
||||
|
||||
pub fn title_for<S: 'static + Send + Sync, T>(
|
||||
cx: &mut ViewContext<S>,
|
||||
) -> impl Element<State = S> {
|
||||
Self::title(cx, std::any::type_name::<T>())
|
||||
}
|
||||
|
||||
pub fn label<S: 'static + Send + Sync>(
|
||||
cx: &mut ViewContext<S>,
|
||||
label: &str,
|
||||
) -> impl Element<State = S> {
|
||||
let theme = theme(cx);
|
||||
|
||||
div()
|
||||
.mt_4()
|
||||
.mb_2()
|
||||
.text_xs()
|
||||
.text_color(theme.lowest.base.default.foreground)
|
||||
.child(label.to_owned())
|
||||
}
|
||||
}
|
116
crates/storybook2/src/story_selector.rs
Normal file
116
crates/storybook2/src/story_selector.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
use std::str::FromStr;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use clap::builder::PossibleValue;
|
||||
use clap::ValueEnum;
|
||||
use gpui3::AnyElement;
|
||||
use strum::{EnumIter, EnumString, IntoEnumIterator};
|
||||
|
||||
use crate::ui::prelude::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ElementStory {
|
||||
Label,
|
||||
}
|
||||
|
||||
impl ElementStory {
|
||||
pub fn story<S: 'static + Send + Sync>(&self) -> AnyElement<S> {
|
||||
use crate::stories::elements;
|
||||
|
||||
match self {
|
||||
Self::Label => elements::label::LabelStory::new().into_any(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ComponentStory {
|
||||
Panel,
|
||||
}
|
||||
|
||||
impl ComponentStory {
|
||||
pub fn story<S: 'static + Send + Sync>(&self) -> AnyElement<S> {
|
||||
use crate::stories::components;
|
||||
|
||||
match self {
|
||||
Self::Panel => components::panel::PanelStory::new().into_any(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum StorySelector {
|
||||
Element(ElementStory),
|
||||
Component(ComponentStory),
|
||||
KitchenSink,
|
||||
}
|
||||
|
||||
impl FromStr for StorySelector {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
|
||||
let story = raw_story_name.to_ascii_lowercase();
|
||||
|
||||
if story == "kitchen_sink" {
|
||||
return Ok(Self::KitchenSink);
|
||||
}
|
||||
|
||||
if let Some((_, story)) = story.split_once("elements/") {
|
||||
let element_story = ElementStory::from_str(story)
|
||||
.with_context(|| format!("story not found for element '{story}'"))?;
|
||||
|
||||
return Ok(Self::Element(element_story));
|
||||
}
|
||||
|
||||
if let Some((_, story)) = story.split_once("components/") {
|
||||
let component_story = ComponentStory::from_str(story)
|
||||
.with_context(|| format!("story not found for component '{story}'"))?;
|
||||
|
||||
return Ok(Self::Component(component_story));
|
||||
}
|
||||
|
||||
Err(anyhow!("story not found for '{raw_story_name}'"))
|
||||
}
|
||||
}
|
||||
|
||||
impl StorySelector {
|
||||
pub fn story<S: 'static + Send + Sync>(&self) -> AnyElement<S> {
|
||||
match self {
|
||||
Self::Element(element_story) => element_story.story(),
|
||||
Self::Component(component_story) => component_story.story(),
|
||||
Self::KitchenSink => crate::stories::kitchen_sink::KitchenSinkStory::new().into_any(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The list of all stories available in the storybook.
|
||||
static ALL_STORY_SELECTORS: OnceLock<Vec<StorySelector>> = OnceLock::new();
|
||||
|
||||
impl ValueEnum for StorySelector {
|
||||
fn value_variants<'a>() -> &'a [Self] {
|
||||
let stories = ALL_STORY_SELECTORS.get_or_init(|| {
|
||||
let element_stories = ElementStory::iter().map(StorySelector::Element);
|
||||
let component_stories = ComponentStory::iter().map(StorySelector::Component);
|
||||
|
||||
element_stories
|
||||
.chain(component_stories)
|
||||
.chain(std::iter::once(StorySelector::KitchenSink))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
stories
|
||||
}
|
||||
|
||||
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
|
||||
let value = match self {
|
||||
Self::Element(story) => format!("elements/{story}"),
|
||||
Self::Component(story) => format!("components/{story}"),
|
||||
Self::KitchenSink => "kitchen_sink".to_string(),
|
||||
};
|
||||
|
||||
Some(PossibleValue::new(value))
|
||||
}
|
||||
}
|
|
@ -9,6 +9,9 @@ use workspace::workspace;
|
|||
|
||||
mod assets;
|
||||
mod collab_panel;
|
||||
mod stories;
|
||||
mod story;
|
||||
mod story_selector;
|
||||
mod theme;
|
||||
mod themes;
|
||||
mod ui;
|
||||
|
|
|
@ -2,9 +2,11 @@ mod children;
|
|||
mod components;
|
||||
mod elements;
|
||||
pub mod prelude;
|
||||
mod theme;
|
||||
mod tokens;
|
||||
|
||||
pub use children::*;
|
||||
pub use components::*;
|
||||
pub use elements::*;
|
||||
pub use theme::*;
|
||||
pub use tokens::*;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
mod label;
|
||||
mod stack;
|
||||
|
||||
pub use label::*;
|
||||
pub use stack::*;
|
||||
|
|
165
crates/storybook2/src/ui/elements/label.rs
Normal file
165
crates/storybook2/src/ui/elements/label.rs
Normal file
|
@ -0,0 +1,165 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use gpui3::{Hsla, WindowContext};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::theme::theme;
|
||||
use crate::ui::prelude::*;
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum LabelColor {
|
||||
#[default]
|
||||
Default,
|
||||
Muted,
|
||||
Created,
|
||||
Modified,
|
||||
Deleted,
|
||||
Disabled,
|
||||
Hidden,
|
||||
Placeholder,
|
||||
Accent,
|
||||
}
|
||||
|
||||
impl LabelColor {
|
||||
pub fn hsla(&self, cx: &WindowContext) -> Hsla {
|
||||
let theme = theme(cx);
|
||||
|
||||
match self {
|
||||
Self::Default => theme.middle.base.default.foreground,
|
||||
Self::Muted => theme.middle.variant.default.foreground,
|
||||
Self::Created => theme.middle.positive.default.foreground,
|
||||
Self::Modified => theme.middle.warning.default.foreground,
|
||||
Self::Deleted => theme.middle.negative.default.foreground,
|
||||
Self::Disabled => theme.middle.base.disabled.foreground,
|
||||
Self::Hidden => theme.middle.variant.default.foreground,
|
||||
Self::Placeholder => theme.middle.base.disabled.foreground,
|
||||
Self::Accent => theme.middle.accent.default.foreground,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum LabelSize {
|
||||
#[default]
|
||||
Default,
|
||||
Small,
|
||||
}
|
||||
|
||||
#[derive(Element, Clone)]
|
||||
pub struct Label<S: 'static + Send + Sync> {
|
||||
state_type: PhantomData<S>,
|
||||
label: String,
|
||||
color: LabelColor,
|
||||
size: LabelSize,
|
||||
highlight_indices: Vec<usize>,
|
||||
strikethrough: bool,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync> Label<S> {
|
||||
pub fn new<L>(label: L) -> Self
|
||||
where
|
||||
L: Into<String>,
|
||||
{
|
||||
Self {
|
||||
state_type: PhantomData,
|
||||
label: label.into(),
|
||||
color: LabelColor::Default,
|
||||
size: LabelSize::Default,
|
||||
highlight_indices: Vec::new(),
|
||||
strikethrough: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: LabelColor) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: LabelSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_highlights(mut self, indices: Vec<usize>) -> Self {
|
||||
self.highlight_indices = indices;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
let theme = theme(cx);
|
||||
|
||||
let highlight_color = theme.lowest.accent.default.foreground;
|
||||
|
||||
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
|
||||
|
||||
let mut runs: SmallVec<[Run; 8]> = SmallVec::new();
|
||||
|
||||
for (char_ix, char) in self.label.char_indices() {
|
||||
let mut color = self.color.hsla(cx);
|
||||
|
||||
if let Some(highlight_ix) = highlight_indices.peek() {
|
||||
if char_ix == *highlight_ix {
|
||||
color = highlight_color;
|
||||
|
||||
highlight_indices.next();
|
||||
}
|
||||
}
|
||||
|
||||
let last_run = runs.last_mut();
|
||||
|
||||
let start_new_run = if let Some(last_run) = last_run {
|
||||
if color == last_run.color {
|
||||
last_run.text.push(char);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if start_new_run {
|
||||
runs.push(Run {
|
||||
text: char.to_string(),
|
||||
color,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
div()
|
||||
.flex()
|
||||
// .when(self.strikethrough, |this| {
|
||||
// this.relative().child(
|
||||
// div()
|
||||
// .absolute()
|
||||
// .top_px()
|
||||
// .my_auto()
|
||||
// .w_full()
|
||||
// .h_px()
|
||||
// .fill(LabelColor::Hidden.hsla(cx)),
|
||||
// )
|
||||
// })
|
||||
.children(runs.into_iter().map(|run| {
|
||||
let mut div = div();
|
||||
|
||||
if self.size == LabelSize::Small {
|
||||
div = div.text_xs();
|
||||
} else {
|
||||
div = div.text_sm();
|
||||
}
|
||||
|
||||
div.text_color(run.color).child(run.text)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// A run of text that receives the same style.
|
||||
struct Run {
|
||||
pub text: String,
|
||||
pub color: Hsla,
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
pub use gpui3::{Element, IntoAnyElement, ParentElement, ScrollState, StyleHelpers, ViewContext};
|
||||
pub use gpui3::{
|
||||
div, Element, IntoAnyElement, ParentElement, ScrollState, StyleHelpers, ViewContext,
|
||||
};
|
||||
|
||||
pub use crate::ui::{HackyChildren, HackyChildrenPayload};
|
||||
|
|
10
crates/storybook2/src/ui/theme.rs
Normal file
10
crates/storybook2/src/ui/theme.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use gpui3::WindowContext;
|
||||
|
||||
use crate::theme::Theme;
|
||||
use crate::themes::rose_pine_dawn;
|
||||
|
||||
pub fn theme(cx: &WindowContext) -> Arc<Theme> {
|
||||
Arc::new(rose_pine_dawn())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue