This refactor enhances the overall design by promoting reusable and composable UI component structures within the Zed project codebase.
4.5 KiB
Hello World
Let's work through the prototypical "Build a todo app" example to showcase how we might build a simple component from scratch.
Setup
We'll create a headline, a list of todo items, and a form to add new items.
struct TodoList<V: 'static> {
headline: SharedString,
items: Vec<TodoItem>,
submit_form: ClickHandler<V>
}
struct TodoItem<V: 'static> {
text: SharedString,
completed: bool,
delete: ClickHandler<V>
}
impl<V: 'static> TodoList<V> {
pub fn new(
// Here we impl Into<SharedString>
headline: impl Into<SharedString>,
items: Vec<TodoItem>,
submit_form: ClickHandler<V>
) -> Self {
Self {
// and here we call .into() so we can simply pass a string
// when creating the headline. This pattern is used throughout
// outr components
headline: headline.into(),
items: Vec::new(),
submit_form,
}
}
}
All of this is relatively straightforward.
We use [gpui::SharedString] in components instead of [std::string::String]. This allows us to [TODO: someone who actually knows please explain why we use SharedString].
When we want to pass an action we pass a ClickHandler
. Whenever we want to add an action, the struct it belongs to needs to be generic over the view type V
.
use gpui::hsla
impl<V: 'static> TodoList<V> {
// ...
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
div().size_4().bg(hsla(50.0/360.0, 1.0, 0.5, 1.0))
}
}
Every component needs a render method, and it should return impl Element<V>
. This basic component will render a 16x16px yellow square on the screen.
A couple of questions might come to mind:
Why is size_4()
16px, not 4px?
gpui's style system is based on conventions created by Tailwind CSS. Here is an example of the list of sizes for width
: Width - TailwindCSS Docs.
I'll quote from the Tailwind Core Concepts docs here:
Now I know what you’re thinking, “this is an atrocity, what a horrible mess!” and you’re right, it’s kind of ugly. In fact it’s just about impossible to think this is a good idea the first time you see it — you have to actually try it.
As you start using the Tailwind-style conventions you will be surprised how quick it makes it to build out UIs.
Why 50.0/360.0
in hsla()
?
gpui [gpui::Hsla] use 0.0-1.0
for all it's values, but it is common for tools to use 0-360
for hue.
This may change in the future, but this is a little trick that let's you use familiar looking values.
Building out the container
Let's grab our [theme2::colors::ThemeColors] from the theme and start building out a basic container.
We can access the current theme's colors like this:
impl<V: 'static> TodoList<V> {
// ...
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
let color = cx.theme().colors()
div().size_4().hsla(50.0/360.0, 1.0, 0.5, 1.0)
}
}
Now we have access to the complete set of colors defined in the theme.
use gpui::hsla
impl<V: 'static> TodoList<V> {
// ...
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
let color = cx.theme().colors()
div().size_4().bg(color.surface)
}
}
Let's finish up some basic styles for the container then move on to adding the other elements.
use gpui::hsla
impl<V: 'static> TodoList<V> {
// ...
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
let color = cx.theme().colors()
div()
// Flex properties
.flex()
.flex_col() // Stack elements vertically
.gap_2() // Add 8px of space between elements
// Size properties
.w_96() // Set width to 384px
.p_4() // Add 16px of padding on all sides
// Color properties
.bg(color.surface) // Set background color
.text_color(color.text) // Set text color
// Border properties
.rounded_md() // Add 4px of border radius
.border() // Add a 1px border
.border_color(color.border)
.child(
"Hello, world!"
)
}
}
Headline
TODO
List of todo items
TODO
Input
TODO
End result
TODO