Begin setting up stories

This commit is contained in:
Marshall Bowers 2023-10-04 12:49:06 -04:00
parent 4b793f44ef
commit a05cbf8169
17 changed files with 471 additions and 1 deletions

1
Cargo.lock generated
View file

@ -7606,6 +7606,7 @@ dependencies = [
"serde", "serde",
"settings", "settings",
"simplelog", "simplelog",
"smallvec",
"strum", "strum",
"theme", "theme",
"util", "util",

View file

@ -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> { pub struct Text<S> {
text: ArcCow<'static, str>, text: ArcCow<'static, str>,
state_type: PhantomData<S>, state_type: PhantomData<S>,

View file

@ -20,6 +20,7 @@ rust-embed.workspace = true
serde.workspace = true serde.workspace = true
settings = { path = "../settings" } settings = { path = "../settings" }
simplelog = "0.9" simplelog = "0.9"
smallvec.workspace = true
strum = { version = "0.25.0", features = ["derive"] } strum = { version = "0.25.0", features = ["derive"] }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }

View file

@ -0,0 +1,3 @@
pub mod components;
pub mod elements;
pub mod kitchen_sink;

View file

@ -0,0 +1 @@
pub mod panel;

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

View file

@ -0,0 +1 @@
pub mod label;

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

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

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

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

View file

@ -9,6 +9,9 @@ use workspace::workspace;
mod assets; mod assets;
mod collab_panel; mod collab_panel;
mod stories;
mod story;
mod story_selector;
mod theme; mod theme;
mod themes; mod themes;
mod ui; mod ui;

View file

@ -2,9 +2,11 @@ mod children;
mod components; mod components;
mod elements; mod elements;
pub mod prelude; pub mod prelude;
mod theme;
mod tokens; mod tokens;
pub use children::*; pub use children::*;
pub use components::*; pub use components::*;
pub use elements::*; pub use elements::*;
pub use theme::*;
pub use tokens::*; pub use tokens::*;

View file

@ -1,3 +1,5 @@
mod label;
mod stack; mod stack;
pub use label::*;
pub use stack::*; pub use stack::*;

View 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,
}

View file

@ -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}; pub use crate::ui::{HackyChildren, HackyChildrenPayload};

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