Remove 2 suffix for ui, storybook, text

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-01-03 12:33:51 -08:00
parent 0cf65223ce
commit 4305c5fdbe
142 changed files with 106 additions and 5018 deletions

2919
crates/storybook/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,39 @@
[package]
name = "storybook"
version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
name = "storybook"
path = "src/storybook.rs"
[dependencies]
anyhow.workspace = true
# TODO: Remove after diagnosing stack overflow.
backtrace-on-stack-overflow = "0.3.0"
chrono = "0.4"
clap = { version = "4.4", features = ["derive", "string"] }
dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { package = "gpui2", path = "../gpui2" }
indoc.workspace = true
itertools = "0.11.0"
language = { path = "../language" }
log.workspace = true
rust-embed.workspace = true
serde.workspace = true
settings = { path = "../settings" }
simplelog = "0.9"
smallvec.workspace = true
story = { path = "../story" }
strum = { version = "0.25.0", features = ["derive"] }
theme2 = { path = "../theme2" }
menu = { path = "../menu" }
ui = { path = "../ui", features = ["stories"] }
util = { path = "../util" }
picker = { path = "../picker" }
[dev-dependencies]
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }

View file

@ -0,0 +1,5 @@
fn main() {
// Find WebRTC.framework as a sibling of the executable when running outside of an application bundle.
// TODO: We shouldn't depend on WebRTC in editor
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
}

View file

@ -0,0 +1,72 @@
Much of element styling is now handled by an external engine.
How do I make an element hover.
There's a hover style.
Hoverable needs to wrap another element. That element can be styled.
```rs
struct Hoverable<E: Element> {
}
impl<V> Element<V> for Hoverable {
}
```
```rs
#[derive(Styled, Interactive)]
pub struct Div {
declared_style: StyleRefinement,
interactions: Interactions
}
pub trait Styled {
fn declared_style(&mut self) -> &mut StyleRefinement;
fn compute_style(&mut self) -> Style {
Style::default().refine(self.declared_style())
}
// All the tailwind classes, modifying self.declared_style()
}
impl Style {
pub fn paint_background<V>(layout: Layout, cx: &mut PaintContext<V>);
pub fn paint_foreground<V>(layout: Layout, cx: &mut PaintContext<V>);
}
pub trait Interactive<V> {
fn interactions(&mut self) -> &mut Interactions<V>;
fn on_click(self, )
}
struct Interactions<V> {
click: SmallVec<[<Rc<dyn Fn(&mut V, &dyn Any, )>; 1]>,
}
```
```rs
trait Stylable {
type Style;
fn with_style(self, style: Self::Style) -> Self;
}
```

View file

@ -0,0 +1,30 @@
use std::borrow::Cow;
use anyhow::{anyhow, Result};
use gpui::{AssetSource, SharedString};
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "../../assets"]
#[include = "fonts/**/*"]
#[include = "icons/**/*"]
#[include = "themes/**/*"]
#[include = "sounds/**/*"]
#[include = "*.md"]
#[exclude = "*.DS_Store"]
pub struct Assets;
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Cow<[u8]>> {
Self::get(path)
.map(|f| f.data)
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
Ok(Self::iter()
.filter(|p| p.starts_with(path))
.map(SharedString::from)
.collect())
}
}

View file

@ -0,0 +1,21 @@
mod auto_height_editor;
mod cursor;
mod focus;
mod kitchen_sink;
mod overflow_scroll;
mod picker;
mod scroll;
mod text;
mod viewport_units;
mod z_index;
pub use auto_height_editor::*;
pub use cursor::*;
pub use focus::*;
pub use kitchen_sink::*;
pub use overflow_scroll::*;
pub use picker::*;
pub use scroll::*;
pub use text::*;
pub use viewport_units::*;
pub use z_index::*;

View file

@ -0,0 +1,32 @@
use editor::Editor;
use gpui::{
div, white, IntoElement, KeyBinding, ParentElement, Render, Styled, View, ViewContext,
VisualContext, WindowContext,
};
pub struct AutoHeightEditorStory {
editor: View<Editor>,
}
impl AutoHeightEditorStory {
pub fn new(cx: &mut WindowContext) -> View<Self> {
cx.bind_keys([KeyBinding::new("enter", editor::Newline, Some("Editor"))]);
cx.new_view(|cx| Self {
editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(3, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor
}),
})
}
}
impl Render for AutoHeightEditorStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.size_full()
.bg(white())
.text_sm()
.child(div().w_32().bg(gpui::black()).child(self.editor.clone()))
}
}

View file

@ -0,0 +1,109 @@
use gpui::{Div, Render, Stateful};
use story::Story;
use ui::prelude::*;
pub struct CursorStory;
impl Render for CursorStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let all_cursors: [(&str, Box<dyn Fn(Stateful<Div>) -> Stateful<Div>>); 19] = [
(
"cursor_default",
Box::new(|el: Stateful<Div>| el.cursor_default()),
),
(
"cursor_pointer",
Box::new(|el: Stateful<Div>| el.cursor_pointer()),
),
(
"cursor_text",
Box::new(|el: Stateful<Div>| el.cursor_text()),
),
(
"cursor_move",
Box::new(|el: Stateful<Div>| el.cursor_move()),
),
(
"cursor_not_allowed",
Box::new(|el: Stateful<Div>| el.cursor_not_allowed()),
),
(
"cursor_context_menu",
Box::new(|el: Stateful<Div>| el.cursor_context_menu()),
),
(
"cursor_crosshair",
Box::new(|el: Stateful<Div>| el.cursor_crosshair()),
),
(
"cursor_vertical_text",
Box::new(|el: Stateful<Div>| el.cursor_vertical_text()),
),
(
"cursor_alias",
Box::new(|el: Stateful<Div>| el.cursor_alias()),
),
(
"cursor_copy",
Box::new(|el: Stateful<Div>| el.cursor_copy()),
),
(
"cursor_no_drop",
Box::new(|el: Stateful<Div>| el.cursor_no_drop()),
),
(
"cursor_grab",
Box::new(|el: Stateful<Div>| el.cursor_grab()),
),
(
"cursor_grabbing",
Box::new(|el: Stateful<Div>| el.cursor_grabbing()),
),
(
"cursor_col_resize",
Box::new(|el: Stateful<Div>| el.cursor_col_resize()),
),
(
"cursor_row_resize",
Box::new(|el: Stateful<Div>| el.cursor_row_resize()),
),
(
"cursor_n_resize",
Box::new(|el: Stateful<Div>| el.cursor_n_resize()),
),
(
"cursor_e_resize",
Box::new(|el: Stateful<Div>| el.cursor_e_resize()),
),
(
"cursor_s_resize",
Box::new(|el: Stateful<Div>| el.cursor_s_resize()),
),
(
"cursor_w_resize",
Box::new(|el: Stateful<Div>| el.cursor_w_resize()),
),
];
Story::container()
.flex()
.gap_1()
.child(Story::title("cursor"))
.children(all_cursors.map(|(name, apply_cursor)| {
div().gap_1().flex().text_color(gpui::white()).child(
div()
.flex()
.items_center()
.justify_center()
.id(name)
.map(apply_cursor)
.w_64()
.h_8()
.bg(gpui::red())
.active(|style| style.bg(gpui::green()))
.text_sm()
.child(Story::label(name)),
)
}))
}
}

View file

@ -0,0 +1,119 @@
use gpui::{
actions, div, prelude::*, FocusHandle, KeyBinding, Render, Subscription, View, WindowContext,
};
use ui::prelude::*;
actions!(focus, [ActionA, ActionB, ActionC]);
pub struct FocusStory {
parent_focus: FocusHandle,
child_1_focus: FocusHandle,
child_2_focus: FocusHandle,
_focus_subscriptions: Vec<Subscription>,
}
impl FocusStory {
pub fn view(cx: &mut WindowContext) -> View<Self> {
cx.bind_keys([
KeyBinding::new("cmd-a", ActionA, Some("parent")),
KeyBinding::new("cmd-a", ActionB, Some("child-1")),
KeyBinding::new("cmd-c", ActionC, None),
]);
cx.new_view(move |cx| {
let parent_focus = cx.focus_handle();
let child_1_focus = cx.focus_handle();
let child_2_focus = cx.focus_handle();
let _focus_subscriptions = vec![
cx.on_focus(&parent_focus, |_, _| {
println!("Parent focused");
}),
cx.on_blur(&parent_focus, |_, _| {
println!("Parent blurred");
}),
cx.on_focus(&child_1_focus, |_, _| {
println!("Child 1 focused");
}),
cx.on_blur(&child_1_focus, |_, _| {
println!("Child 1 blurred");
}),
cx.on_focus(&child_2_focus, |_, _| {
println!("Child 2 focused");
}),
cx.on_blur(&child_2_focus, |_, _| {
println!("Child 2 blurred");
}),
];
Self {
parent_focus,
child_1_focus,
child_2_focus,
_focus_subscriptions,
}
})
}
}
impl Render for FocusStory {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
let theme = cx.theme();
let color_1 = theme.status().created;
let color_2 = theme.status().modified;
let color_4 = theme.status().conflict;
let color_5 = theme.status().ignored;
let color_6 = theme.status().renamed;
let color_7 = theme.status().hint;
div()
.id("parent")
.active(|style| style.bg(color_7))
.track_focus(&self.parent_focus)
.key_context("parent")
.on_action(cx.listener(|_, _action: &ActionA, _cx| {
println!("Action A dispatched on parent");
}))
.on_action(cx.listener(|_, _action: &ActionB, _cx| {
println!("Action B dispatched on parent");
}))
.on_key_down(cx.listener(|_, event, _| println!("Key down on parent {:?}", event)))
.on_key_up(cx.listener(|_, event, _| println!("Key up on parent {:?}", event)))
.size_full()
.bg(color_1)
.focus(|style| style.bg(color_2))
.child(
div()
.track_focus(&self.child_1_focus)
.key_context("child-1")
.on_action(cx.listener(|_, _action: &ActionB, _cx| {
println!("Action B dispatched on child 1 during");
}))
.w_full()
.h_6()
.bg(color_4)
.focus(|style| style.bg(color_5))
.in_focus(|style| style.bg(color_6))
.on_key_down(
cx.listener(|_, event, _| println!("Key down on child 1 {:?}", event)),
)
.on_key_up(cx.listener(|_, event, _| println!("Key up on child 1 {:?}", event)))
.child("Child 1"),
)
.child(
div()
.track_focus(&self.child_2_focus)
.key_context("child-2")
.on_action(cx.listener(|_, _action: &ActionC, _cx| {
println!("Action C dispatched on child 2");
}))
.w_full()
.h_6()
.bg(color_4)
.on_key_down(
cx.listener(|_, event, _| println!("Key down on child 2 {:?}", event)),
)
.on_key_up(cx.listener(|_, event, _| println!("Key up on child 2 {:?}", event)))
.child("Child 2"),
)
}
}

View file

@ -0,0 +1,32 @@
use gpui::{prelude::*, Render, View};
use story::Story;
use strum::IntoEnumIterator;
use ui::prelude::*;
use crate::story_selector::ComponentStory;
pub struct KitchenSinkStory;
impl KitchenSinkStory {
pub fn view(cx: &mut WindowContext) -> View<Self> {
cx.new_view(|_cx| Self)
}
}
impl Render for KitchenSinkStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let component_stories = ComponentStory::iter()
.map(|selector| selector.story(cx))
.collect::<Vec<_>>();
Story::container()
.id("kitchen-sink")
.overflow_y_scroll()
.child(Story::title("Kitchen Sink"))
.child(Story::label("Components"))
.child(div().flex().flex_col().children(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,39 @@
use gpui::Render;
use story::Story;
use ui::prelude::*;
pub struct OverflowScrollStory;
impl Render for OverflowScrollStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
Story::container()
.child(Story::title("Overflow Scroll"))
.child(Story::label("`overflow_x_scroll`"))
.child(
h_stack()
.id("overflow_x_scroll")
.gap_2()
.overflow_x_scroll()
.children((0..100).map(|i| {
div()
.p_4()
.debug_bg_cyan()
.child(SharedString::from(format!("Child {}", i + 1)))
})),
)
.child(Story::label("`overflow_y_scroll`"))
.child(
v_stack()
.id("overflow_y_scroll")
.gap_2()
.overflow_y_scroll()
.children((0..100).map(|i| {
div()
.p_4()
.debug_bg_green()
.child(SharedString::from(format!("Child {}", i + 1)))
})),
)
}
}

View file

@ -0,0 +1,209 @@
use fuzzy::StringMatchCandidate;
use gpui::{div, prelude::*, KeyBinding, Render, SharedString, Styled, Task, View, WindowContext};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use ui::{prelude::*, ListItemSpacing};
use ui::{Label, ListItem};
pub struct PickerStory {
picker: View<Picker<Delegate>>,
}
struct Delegate {
candidates: Arc<[StringMatchCandidate]>,
matches: Vec<usize>,
selected_ix: usize,
}
impl Delegate {
fn new(strings: &[&str]) -> Self {
Self {
candidates: strings
.iter()
.copied()
.enumerate()
.map(|(id, string)| StringMatchCandidate {
id,
char_bag: string.into(),
string: string.into(),
})
.collect(),
matches: vec![],
selected_ix: 0,
}
}
}
impl PickerDelegate for Delegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.candidates.len()
}
fn placeholder_text(&self) -> Arc<str> {
"Test".into()
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut gpui::ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let Some(candidate_ix) = self.matches.get(ix) else {
return None;
};
// TASK: Make StringMatchCandidate::string a SharedString
let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone());
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(Label::new(candidate)),
)
}
fn selected_index(&self) -> usize {
self.selected_ix
}
fn set_selected_index(&mut self, ix: usize, cx: &mut gpui::ViewContext<Picker<Self>>) {
self.selected_ix = ix;
cx.notify();
}
fn confirm(&mut self, secondary: bool, _cx: &mut gpui::ViewContext<Picker<Self>>) {
let candidate_ix = self.matches[self.selected_ix];
let candidate = self.candidates[candidate_ix].string.clone();
if secondary {
eprintln!("Secondary confirmed {}", candidate)
} else {
eprintln!("Confirmed {}", candidate)
}
}
fn dismissed(&mut self, cx: &mut gpui::ViewContext<Picker<Self>>) {
cx.quit();
}
fn update_matches(
&mut self,
query: String,
cx: &mut gpui::ViewContext<Picker<Self>>,
) -> Task<()> {
let candidates = self.candidates.clone();
self.matches = cx
.background_executor()
.block(fuzzy::match_strings(
&candidates,
&query,
true,
100,
&Default::default(),
cx.background_executor().clone(),
))
.into_iter()
.map(|r| r.candidate_id)
.collect();
self.selected_ix = 0;
Task::ready(())
}
}
impl PickerStory {
pub fn new(cx: &mut WindowContext) -> View<Self> {
cx.new_view(|cx| {
cx.bind_keys([
KeyBinding::new("up", menu::SelectPrev, Some("picker")),
KeyBinding::new("pageup", menu::SelectFirst, Some("picker")),
KeyBinding::new("shift-pageup", menu::SelectFirst, Some("picker")),
KeyBinding::new("ctrl-p", menu::SelectPrev, Some("picker")),
KeyBinding::new("down", menu::SelectNext, Some("picker")),
KeyBinding::new("pagedown", menu::SelectLast, Some("picker")),
KeyBinding::new("shift-pagedown", menu::SelectFirst, Some("picker")),
KeyBinding::new("ctrl-n", menu::SelectNext, Some("picker")),
KeyBinding::new("cmd-up", menu::SelectFirst, Some("picker")),
KeyBinding::new("cmd-down", menu::SelectLast, Some("picker")),
KeyBinding::new("enter", menu::Confirm, Some("picker")),
KeyBinding::new("ctrl-enter", menu::ShowContextMenu, Some("picker")),
KeyBinding::new("cmd-enter", menu::SecondaryConfirm, Some("picker")),
KeyBinding::new("escape", menu::Cancel, Some("picker")),
KeyBinding::new("ctrl-c", menu::Cancel, Some("picker")),
]);
PickerStory {
picker: cx.new_view(|cx| {
let mut delegate = Delegate::new(&[
"Baguette (France)",
"Baklava (Turkey)",
"Beef Wellington (UK)",
"Biryani (India)",
"Borscht (Ukraine)",
"Bratwurst (Germany)",
"Bulgogi (Korea)",
"Burrito (USA)",
"Ceviche (Peru)",
"Chicken Tikka Masala (India)",
"Churrasco (Brazil)",
"Couscous (North Africa)",
"Croissant (France)",
"Dim Sum (China)",
"Empanada (Argentina)",
"Fajitas (Mexico)",
"Falafel (Middle East)",
"Feijoada (Brazil)",
"Fish and Chips (UK)",
"Fondue (Switzerland)",
"Goulash (Hungary)",
"Haggis (Scotland)",
"Kebab (Middle East)",
"Kimchi (Korea)",
"Lasagna (Italy)",
"Maple Syrup Pancakes (Canada)",
"Moussaka (Greece)",
"Pad Thai (Thailand)",
"Paella (Spain)",
"Pancakes (USA)",
"Pasta Carbonara (Italy)",
"Pavlova (Australia)",
"Peking Duck (China)",
"Pho (Vietnam)",
"Pierogi (Poland)",
"Pizza (Italy)",
"Poutine (Canada)",
"Pretzel (Germany)",
"Ramen (Japan)",
"Rendang (Indonesia)",
"Sashimi (Japan)",
"Satay (Indonesia)",
"Shepherd's Pie (Ireland)",
"Sushi (Japan)",
"Tacos (Mexico)",
"Tandoori Chicken (India)",
"Tortilla (Spain)",
"Tzatziki (Greece)",
"Wiener Schnitzel (Austria)",
]);
delegate.update_matches("".into(), cx).detach();
let picker = Picker::new(delegate, cx);
picker.focus(cx);
picker
}),
}
})
}
}
impl Render for PickerStory {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
div()
.bg(cx.theme().styles.colors.background)
.size_full()
.child(self.picker.clone())
}
}

View file

@ -0,0 +1,52 @@
use gpui::{div, prelude::*, px, Render, SharedString, Styled, View, WindowContext};
use ui::prelude::*;
use ui::Tooltip;
pub struct ScrollStory;
impl ScrollStory {
pub fn view(cx: &mut WindowContext) -> View<ScrollStory> {
cx.new_view(|_cx| ScrollStory)
}
}
impl Render for ScrollStory {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
let theme = cx.theme();
let color_1 = theme.status().created;
let color_2 = theme.status().modified;
div()
.id("parent")
.bg(theme.colors().background)
.size_full()
.overflow_scroll()
.children((0..10).map(|row| {
div()
.w(px(1000.))
.h(px(100.))
.flex()
.flex_row()
.children((0..10).map(|column| {
let id = SharedString::from(format!("{}, {}", row, column));
let bg = if row % 2 == column % 2 {
color_1
} else {
color_2
};
div()
.id(id)
.tooltip(move |cx| Tooltip::text(format!("{}, {}", row, column), cx))
.bg(bg)
.size(px(100. as f32))
.when(row >= 5 && column >= 5, |d| {
d.overflow_scroll()
.child(div().size(px(50.)).bg(color_1))
.child(div().size(px(50.)).bg(color_2))
.child(div().size(px(50.)).bg(color_1))
.child(div().size(px(50.)).bg(color_2))
})
}))
}))
}
}

View file

@ -0,0 +1,175 @@
use gpui::{
div, green, red, HighlightStyle, InteractiveText, IntoElement, ParentElement, Render, Styled,
StyledText, View, VisualContext, WindowContext,
};
use indoc::indoc;
use story::*;
pub struct TextStory;
impl TextStory {
pub fn view(cx: &mut WindowContext) -> View<Self> {
cx.new_view(|_cx| Self)
}
}
impl Render for TextStory {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
StoryContainer::new("Text Story", "crates/storybook/src/stories/text.rs")
.children(
vec![
StorySection::new()
.child(
StoryItem::new("Default", div().bg(gpui::blue()).child("Hello World!"))
.usage(indoc! {r##"
div()
.child("Hello World!")
"##
}),
)
.child(
StoryItem::new("Wrapping Text",
div().max_w_96()
.child(
concat!(
"The quick brown fox jumps over the lazy dog. ",
"Meanwhile, the lazy dog decided it was time for a change. ",
"He started daily workout routines, ate healthier and became the fastest dog in town.",
)
)
)
.description("Set a width or max-width to enable text wrapping.")
.usage(indoc! {r##"
div()
.max_w_96()
.child("Some text that you want to wrap.")
"##
})
)
.child(
StoryItem::new("tbd",
div().flex().w_96().child(div().overflow_hidden().child(concat!(
"flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ",
"Meanwhile, the lazy dog decided it was time for a change. ",
"He started daily workout routines, ate healthier and became the fastest dog in town.",
)))
)
)
.child(
StoryItem::new("Text in Horizontal Flex",
div().flex().w_96().bg(red()).child(concat!(
"flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
"Meanwhile, the lazy dog decided it was time for a change. ",
"He started daily workout routines, ate healthier and became the fastest dog in town.",
))
)
.usage(indoc! {r##"
// NOTE: When rendering text in a horizonal flex container,
// Taffy will not pass width constraints down from the parent.
// To fix this, render text in a parent with overflow: hidden
div()
.max_w_96()
.child("Some text that you want to wrap.")
"##
})
)
.child(
StoryItem::new("Interactive Text",
InteractiveText::new(
"interactive",
StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
(6..11, HighlightStyle {
background_color: Some(green()),
..Default::default()
}),
]),
)
.on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
println!("Clicked range {range_ix}");
})
)
.usage(indoc! {r##"
InteractiveText::new(
"interactive",
StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
(6..11, HighlightStyle {
background_color: Some(green()),
..Default::default()
}),
]),
)
.on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
println!("Clicked range {range_ix}");
})
"##
})
)
]
).into_element()
}
}
// TODO: Check all were updated to new style and remove
// impl Render for TextStory {
// type Element = Div;
// fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
// v_stack()
// .bg(blue())
// .child(
// div()
// .flex()
// .child(div().max_w_96().bg(white()).child(concat!(
// "max-width: 96. The quick brown fox jumps over the lazy dog. ",
// "Meanwhile, the lazy dog decided it was time for a change. ",
// "He started daily workout routines, ate healthier and became the fastest dog in town.",
// ))),
// )
// .child(div().h_5())
// .child(div().flex().flex_col().w_96().bg(white()).child(concat!(
// "flex-col. width: 96; The quick brown fox jumps over the lazy dog. ",
// "Meanwhile, the lazy dog decided it was time for a change. ",
// "He started daily workout routines, ate healthier and became the fastest dog in town.",
// )))
// .child(div().h_5())
// .child(
// div()
// .flex()
// .child(div().min_w_96().bg(white()).child(concat!(
// "min-width: 96. The quick brown fox jumps over the lazy dog. ",
// "Meanwhile, the lazy dog decided it was time for a change. ",
// "He started daily workout routines, ate healthier and became the fastest dog in town.",
// ))))
// .child(div().h_5())
// .child(div().flex().w_96().bg(white()).child(div().overflow_hidden().child(concat!(
// "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ",
// "Meanwhile, the lazy dog decided it was time for a change. ",
// "He started daily workout routines, ate healthier and became the fastest dog in town.",
// ))))
// // NOTE: When rendering text in a horizonal flex container,
// // Taffy will not pass width constraints down from the parent.
// // To fix this, render text in a parent with overflow: hidden
// .child(div().h_5())
// .child(div().flex().w_96().bg(red()).child(concat!(
// "flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
// "Meanwhile, the lazy dog decided it was time for a change. ",
// "He started daily workout routines, ate healthier and became the fastest dog in town.",
// ))).child(
// InteractiveText::new(
// "interactive",
// StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
// (6..11, HighlightStyle {
// background_color: Some(green()),
// ..Default::default()
// }),
// ]),
// )
// .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
// println!("Clicked range {range_ix}");
// })
// )
// }
// }

View file

@ -0,0 +1,32 @@
use gpui::Render;
use story::Story;
use ui::prelude::*;
pub struct ViewportUnitsStory;
impl Render for ViewportUnitsStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
Story::container().child(
div()
.flex()
.flex_row()
.child(
div()
.w(vw(0.5, cx))
.h(vh(0.8, cx))
.bg(gpui::red())
.text_color(gpui::white())
.child("50vw, 80vh"),
)
.child(
div()
.w(vw(0.25, cx))
.h(vh(0.33, cx))
.bg(gpui::green())
.text_color(gpui::white())
.child("25vw, 33vh"),
),
)
}
}

View file

@ -0,0 +1,172 @@
use gpui::{px, rgb, Div, Hsla, IntoElement, Render, RenderOnce};
use story::Story;
use ui::prelude::*;
/// A reimplementation of the MDN `z-index` example, found here:
/// [https://developer.mozilla.org/en-US/docs/Web/CSS/z-index](https://developer.mozilla.org/en-US/docs/Web/CSS/z-index).
pub struct ZIndexStory;
impl Render for ZIndexStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
Story::container().child(Story::title("z-index")).child(
div()
.flex()
.child(
div()
.w(px(250.))
.child(Story::label("z-index: auto"))
.child(ZIndexExample::new(0)),
)
.child(
div()
.w(px(250.))
.child(Story::label("z-index: 1"))
.child(ZIndexExample::new(1)),
)
.child(
div()
.w(px(250.))
.child(Story::label("z-index: 3"))
.child(ZIndexExample::new(3)),
)
.child(
div()
.w(px(250.))
.child(Story::label("z-index: 5"))
.child(ZIndexExample::new(5)),
)
.child(
div()
.w(px(250.))
.child(Story::label("z-index: 7"))
.child(ZIndexExample::new(7)),
),
)
}
}
trait Styles: Styled + Sized {
// Trailing `_` is so we don't collide with `block` style `StyleHelpers`.
fn block_(self) -> Self {
self.absolute()
.w(px(150.))
.h(px(50.))
.text_color(rgb::<Hsla>(0x000000))
}
fn blue(self) -> Self {
self.bg(rgb::<Hsla>(0xe5e8fc))
.border_5()
.border_color(rgb::<Hsla>(0x112382))
.line_height(px(55.))
// HACK: Simulate `text-align: center`.
.pl(px(24.))
}
fn red(self) -> Self {
self.bg(rgb::<Hsla>(0xfce5e7))
.border_5()
.border_color(rgb::<Hsla>(0xe3a1a7))
// HACK: Simulate `text-align: center`.
.pl(px(8.))
}
}
impl Styles for Div {}
#[derive(IntoElement)]
struct ZIndexExample {
z_index: u8,
}
impl RenderOnce for ZIndexExample {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
div()
.relative()
.size_full()
// Example element.
.child(
div()
.absolute()
.top(px(15.))
.left(px(15.))
.w(px(180.))
.h(px(230.))
.bg(rgb::<Hsla>(0xfcfbe5))
.text_color(rgb::<Hsla>(0x000000))
.border_5()
.border_color(rgb::<Hsla>(0xe3e0a1))
.line_height(px(215.))
// HACK: Simulate `text-align: center`.
.pl(px(24.))
.z_index(self.z_index)
.child(SharedString::from(format!(
"z-index: {}",
if self.z_index == 0 {
"auto".to_string()
} else {
self.z_index.to_string()
}
))),
)
// Blue blocks.
.child(
div()
.blue()
.block_()
.top(px(0.))
.left(px(0.))
.z_index(6)
.child("z-index: 6"),
)
.child(
div()
.blue()
.block_()
.top(px(30.))
.left(px(30.))
.z_index(4)
.child("z-index: 4"),
)
.child(
div()
.blue()
.block_()
.top(px(60.))
.left(px(60.))
.z_index(2)
.child("z-index: 2"),
)
// Red blocks.
.child(
div()
.red()
.block_()
.top(px(150.))
.left(px(0.))
.child("z-index: auto"),
)
.child(
div()
.red()
.block_()
.top(px(180.))
.left(px(30.))
.child("z-index: auto"),
)
.child(
div()
.red()
.block_()
.top(px(210.))
.left(px(60.))
.child("z-index: auto"),
)
}
}
impl ZIndexExample {
pub fn new(z_index: u8) -> Self {
Self { z_index }
}
}

View file

@ -0,0 +1,134 @@
use std::str::FromStr;
use std::sync::OnceLock;
use crate::stories::*;
use anyhow::anyhow;
use clap::builder::PossibleValue;
use clap::ValueEnum;
use gpui::{AnyView, VisualContext};
use strum::{EnumIter, EnumString, IntoEnumIterator};
use ui::prelude::*;
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
#[strum(serialize_all = "snake_case")]
pub enum ComponentStory {
AutoHeightEditor,
Avatar,
Button,
Checkbox,
ContextMenu,
Cursor,
Disclosure,
Focus,
Icon,
IconButton,
Keybinding,
Label,
List,
ListHeader,
ListItem,
OverflowScroll,
Scroll,
Tab,
TabBar,
ToggleButton,
Text,
ViewportUnits,
ZIndex,
Picker,
}
impl ComponentStory {
pub fn story(&self, cx: &mut WindowContext) -> AnyView {
match self {
Self::AutoHeightEditor => AutoHeightEditorStory::new(cx).into(),
Self::Avatar => cx.new_view(|_| ui::AvatarStory).into(),
Self::Button => cx.new_view(|_| ui::ButtonStory).into(),
Self::Checkbox => cx.new_view(|_| ui::CheckboxStory).into(),
Self::ContextMenu => cx.new_view(|_| ui::ContextMenuStory).into(),
Self::Cursor => cx.new_view(|_| crate::stories::CursorStory).into(),
Self::Disclosure => cx.new_view(|_| ui::DisclosureStory).into(),
Self::Focus => FocusStory::view(cx).into(),
Self::Icon => cx.new_view(|_| ui::IconStory).into(),
Self::IconButton => cx.new_view(|_| ui::IconButtonStory).into(),
Self::Keybinding => cx.new_view(|_| ui::KeybindingStory).into(),
Self::Label => cx.new_view(|_| ui::LabelStory).into(),
Self::List => cx.new_view(|_| ui::ListStory).into(),
Self::ListHeader => cx.new_view(|_| ui::ListHeaderStory).into(),
Self::ListItem => cx.new_view(|_| ui::ListItemStory).into(),
Self::OverflowScroll => cx.new_view(|_| crate::stories::OverflowScrollStory).into(),
Self::Scroll => ScrollStory::view(cx).into(),
Self::Text => TextStory::view(cx).into(),
Self::Tab => cx.new_view(|_| ui::TabStory).into(),
Self::TabBar => cx.new_view(|_| ui::TabBarStory).into(),
Self::ToggleButton => cx.new_view(|_| ui::ToggleButtonStory).into(),
Self::ViewportUnits => cx.new_view(|_| crate::stories::ViewportUnitsStory).into(),
Self::ZIndex => cx.new_view(|_| ZIndexStory).into(),
Self::Picker => PickerStory::new(cx).into(),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum StorySelector {
Component(ComponentStory),
KitchenSink,
}
impl FromStr for StorySelector {
type Err = anyhow::Error;
fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
use anyhow::Context;
let story = raw_story_name.to_ascii_lowercase();
if story == "kitchen_sink" {
return Ok(Self::KitchenSink);
}
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(&self, cx: &mut WindowContext) -> AnyView {
match self {
Self::Component(component_story) => component_story.story(cx),
Self::KitchenSink => KitchenSinkStory::view(cx).into(),
}
}
}
/// 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 component_stories = ComponentStory::iter().map(StorySelector::Component);
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::Component(story) => format!("components/{story}"),
Self::KitchenSink => "kitchen_sink".to_string(),
};
Some(PossibleValue::new(value))
}
}

View file

@ -0,0 +1,137 @@
mod assets;
mod stories;
mod story_selector;
use std::sync::Arc;
use clap::Parser;
use dialoguer::FuzzySelect;
use gpui::{
div, px, size, AnyView, AppContext, Bounds, Render, ViewContext, VisualContext, WindowBounds,
WindowOptions,
};
use log::LevelFilter;
use settings::{default_settings, Settings, SettingsStore};
use simplelog::SimpleLogger;
use strum::IntoEnumIterator;
use theme2::{ThemeRegistry, ThemeSettings};
use ui::prelude::*;
use crate::assets::Assets;
use crate::story_selector::{ComponentStory, StorySelector};
pub use indoc::indoc;
// gpui::actions! {
// storybook,
// [ToggleInspector]
// }
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(value_enum)]
story: Option<StorySelector>,
/// The name of the theme to use in the storybook.
///
/// If not provided, the default theme will be used.
#[arg(long)]
theme: Option<String>,
}
fn main() {
// unsafe { backtrace_on_stack_overflow::enable() };
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
let args = Args::parse();
let story_selector = args.story.clone().unwrap_or_else(|| {
let stories = ComponentStory::iter().collect::<Vec<_>>();
let selection = FuzzySelect::new()
.with_prompt("Choose a story to run:")
.items(&stories)
.interact()
.unwrap();
StorySelector::Component(stories[selection])
});
let theme_name = args.theme.unwrap_or("One Dark".to_string());
let asset_source = Arc::new(Assets);
gpui::App::production(asset_source).run(move |cx| {
load_embedded_fonts(cx).unwrap();
let mut store = SettingsStore::default();
store
.set_default_settings(default_settings().as_ref(), cx)
.unwrap();
cx.set_global(store);
theme2::init(theme2::LoadThemes::All, cx);
let selector = story_selector;
let theme_registry = cx.global::<ThemeRegistry>();
let mut theme_settings = ThemeSettings::get_global(cx).clone();
theme_settings.active_theme = theme_registry.get(&theme_name).unwrap();
ThemeSettings::override_global(theme_settings, cx);
language::init(cx);
editor::init(cx);
let _window = cx.open_window(
WindowOptions {
bounds: WindowBounds::Fixed(Bounds {
origin: Default::default(),
size: size(px(1500.), px(780.)).into(),
}),
..Default::default()
},
move |cx| {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
cx.set_rem_size(ui_font_size);
cx.new_view(|cx| StoryWrapper::new(selector.story(cx)))
},
);
cx.activate(true);
});
}
#[derive(Clone)]
pub struct StoryWrapper {
story: AnyView,
}
impl StoryWrapper {
pub(crate) fn new(story: AnyView) -> Self {
Self { story }
}
}
impl Render for StoryWrapper {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.size_full()
.font("Zed Mono")
.child(self.story.clone())
}
}
fn load_embedded_fonts(cx: &AppContext) -> gpui::Result<()> {
let font_paths = cx.asset_source().list("fonts")?;
let mut embedded_fonts = Vec::new();
for font_path in font_paths {
if font_path.ends_with(".ttf") {
let font_bytes = cx.asset_source().load(&font_path)?.to_vec();
embedded_fonts.push(Arc::from(font_bytes));
}
}
cx.text_system().add_fonts(&embedded_fonts)
}