Zed Weekly: #27

November 24th, 2023

Nathan

After Mikayla and I putting in time over the weekend and Conrad helping us merge the results, we've finally arrived at a satisfying design for GPUI's core view-related traits.

In GPUI, when you open a window, you provide a view, which indicates what that window should display. To implement a view, you implement the Render trait on any type to describe how it appears on screen:

pub trait Render: 'static + Sized {
    type Rendered: RenderOnce + 'static; // We can delete this type in Rust 1.75
 
    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Rendered;
}

For example, you could make a simple TaskList view like this.

use gpui::{prelude::*, Div, div};
 
struct TaskList {
    title: SharedString,
    tasks: Vec<Task>
}
 
struct Task {
    title: SharedString,
    completed: SharedString,
}
 
impl Render for TaskList {
    type Rendered = Div;
 
    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Rendered {
        // In reality this would need more styling.
        // This example is focused on data flow.
        div()
            .child(self.title.clone())
            .children(self.tasks.iter().map(|task| {
                div()
                    .flex()
                    .flex_row()
                    .child(checkbox(task.completed))
                    .child(task.title.clone())
            }))
    }
}

A sibling of the Render trait is RenderOnce, which renders an object by moving it.

pub trait RenderOnce: Sized {
    type Element: IntoElement;
 
    fn render(self) -> Self::Element;
}

Typically, you would use this trait to implement custom UI elements that can be composed from other elements, e.g.:

#[derive(IntoElement)]
struct Button {
    title: SharedString,
    icon_path: Option<SharedString>,
    on_click: ClickListener,
}
 
impl RenderOnce for Button {
    type Rendered = Div;
 
    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
        div()
            .flex()
            .flex_row()
            .child(self.title.clone())
            .children(self
                .icon_path
                .clone().map(|path| svg(path)))
    }
}

Note the #[derive(IntoElement)] attribute macro on Button. This automatically derives the IntoElement trait, which for example is useful to pass an arbitrary child to a parent element such as div:

// Note that you don't need to call `Button::render` here.
div().child(Button::new("button"))

Nate

Happy Thanksgiving week for US folks! We actually celebrate Thanksgiving in Canada in October, so I'm still here grinding away ๐Ÿ˜„

I wanted to talk a bit about UI scale and how it fits into the way we write gpui2 UI.

We've adopted rem units alongside px to make it easier to scale UI elements. A rem is a unit of measurement that is relative to the font size of the root element in CSS.

In gpui2 we similarly treat the size of 1 rem as the size of ui_font_size, a new setting that we've added to the app. It defaults to 16 (16px), which is the default font size in most browsers, and divides nicely into 4-based grids.

Many UI libraries and design systems use a 4-based grid due to a natural progression of sizes that are easy to work with.

An element might have: - 4 pixels of padding - a border radius of 4 pixels - a font size of 12 or 16 pixels - if it contains multiple elements, they might be spaced 4 pixels apart

This allows us to build as if we were using pixels, but scale the UI up or down by changing the ui_font_size setting.

We still have some issues to figure out however. What does 0.25px actually mean? When you get into really small UI sizes you start to run into issues that we are still figuring out.

UI Scale example - 14px/16px/20px
UI Scale example - 14px/16px/20px

Above is an example of ui_font_size set to 14px, 16px, and 20px. UI scale can be set independently of buffer_font_size, so you can have a large UI with a small font size, or vice versa if desired.

We are in the early days of scalable UI in Zed, so there are still some rough edges.

Kirill

This week, I concentrated on fixing existing bugs and pushing Zed's worktree file features. Now, there's a file_scan_exclusions list in the settings that by default hides a bunch of .DS_Store-like files away from Zed.

With exclusions shipped, there seems to be only one outstanding issue with worktrees, quite opposite to exclusions: various lookups in gitignored file hierarchies.

  • Project panel (file tree) already dealt with gitignore files, graying them out and (re)loading gitignored directories only when they are "open" in the file tree.

  • Project search could not search in gitignored files until this week I've covered that part. After implementing the exclusions part, this bit was relatively simple to enable, but the performance part has a few low-hanging fruits left. Any average node_modles or target may overwhelm with results for short queries, so it's not very clear where to draw the base line here now.

  • File finder (open file panel) does not match against gitignored file paths either and seems to be the last element that would drastically benefit from doing so. Yet, this seems to be the most complex issue to solve among all: right now, Zed neither eagerly scans gitignored directories nor tracks FS events for them: node_modules or target directories might be much bigger than regular projects and it seems very wasteful. In fact, we've done something similar before and it was quite taxing for many users' workloads. The "panel" itself is a simple input field with no extra filters, seems that it would be "all gitignored files or none" toggle maximum that would be appropriate to add, and with the input query being a fuzzy matching string, this all means that Zed has to search all gitignored directory roots up to the end to be able to properly match the results. It would be lucky to hit the maximum macthing paths limit eaarly, but if the average experience would be to wait a few seconds to produce less that 100 extra lines with paths, it is not good enough. Adding extra settings for certain gigitnored directories to be included seems complicated (we already have the exclusions!) and not flexible enough. I'm still bouncing off compromises in uncertainty, trying to find a good UX for the feature.

Finally, to at least somehow help with Zed2, I've participated in the ground work for Zed nightly releases, helping Mikayla to spin things up for Zed on gpui2 builds.