component_preview.rs

  1//! # Component Preview
  2//!
  3//! A view for exploring Zed components.
  4
  5use component::{components, ComponentMetadata};
  6use gpui::{list, prelude::*, uniform_list, App, EventEmitter, FocusHandle, Focusable, Window};
  7use gpui::{ListState, ScrollHandle, UniformListScrollHandle};
  8use ui::{prelude::*, ListItem};
  9
 10use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
 11
 12pub fn init(cx: &mut App) {
 13    cx.observe_new(|workspace: &mut Workspace, _, _cx| {
 14        workspace.register_action(
 15            |workspace, _: &workspace::OpenComponentPreview, window, cx| {
 16                let component_preview = cx.new(|cx| ComponentPreview::new(window, cx));
 17                workspace.add_item_to_active_pane(
 18                    Box::new(component_preview),
 19                    None,
 20                    true,
 21                    window,
 22                    cx,
 23                )
 24            },
 25        );
 26    })
 27    .detach();
 28}
 29
 30struct ComponentPreview {
 31    focus_handle: FocusHandle,
 32    _view_scroll_handle: ScrollHandle,
 33    nav_scroll_handle: UniformListScrollHandle,
 34    components: Vec<ComponentMetadata>,
 35    component_list: ListState,
 36    selected_index: usize,
 37}
 38
 39impl ComponentPreview {
 40    pub fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
 41        let components = components().all_sorted();
 42        let initial_length = components.len();
 43
 44        let component_list = ListState::new(initial_length, gpui::ListAlignment::Top, px(500.0), {
 45            let this = cx.entity().downgrade();
 46            move |ix, window: &mut Window, cx: &mut App| {
 47                this.update(cx, |this, cx| {
 48                    this.render_preview(ix, window, cx).into_any_element()
 49                })
 50                .unwrap()
 51            }
 52        });
 53
 54        Self {
 55            focus_handle: cx.focus_handle(),
 56            _view_scroll_handle: ScrollHandle::new(),
 57            nav_scroll_handle: UniformListScrollHandle::new(),
 58            components,
 59            component_list,
 60            selected_index: 0,
 61        }
 62    }
 63
 64    fn scroll_to_preview(&mut self, ix: usize, cx: &mut Context<Self>) {
 65        self.component_list.scroll_to_reveal_item(ix);
 66        self.selected_index = ix;
 67        cx.notify();
 68    }
 69
 70    fn get_component(&self, ix: usize) -> ComponentMetadata {
 71        self.components[ix].clone()
 72    }
 73
 74    fn render_sidebar_entry(
 75        &self,
 76        ix: usize,
 77        selected: bool,
 78        cx: &Context<Self>,
 79    ) -> impl IntoElement {
 80        let component = self.get_component(ix);
 81
 82        ListItem::new(ix)
 83            .child(Label::new(component.name().clone()).color(Color::Default))
 84            .selectable(true)
 85            .toggle_state(selected)
 86            .inset(true)
 87            .on_click(cx.listener(move |this, _, _, cx| {
 88                this.scroll_to_preview(ix, cx);
 89            }))
 90    }
 91
 92    fn render_preview(
 93        &self,
 94        ix: usize,
 95        window: &mut Window,
 96        cx: &mut Context<Self>,
 97    ) -> impl IntoElement {
 98        let component = self.get_component(ix);
 99
100        let name = component.name();
101        let scope = component.scope();
102
103        let description = component.description();
104
105        v_flex()
106            .py_2()
107            .child(
108                v_flex()
109                    .border_1()
110                    .border_color(cx.theme().colors().border)
111                    .rounded_sm()
112                    .w_full()
113                    .gap_4()
114                    .py_4()
115                    .px_6()
116                    .flex_none()
117                    .child(
118                        v_flex()
119                            .gap_1()
120                            .child(
121                                h_flex()
122                                    .gap_1()
123                                    .text_xl()
124                                    .child(div().child(name))
125                                    .when_some(scope, |this, scope| {
126                                        this.child(div().opacity(0.5).child(format!("({})", scope)))
127                                    }),
128                            )
129                            .when_some(description, |this, description| {
130                                this.child(
131                                    div()
132                                        .text_ui_sm(cx)
133                                        .text_color(cx.theme().colors().text_muted)
134                                        .max_w(px(600.0))
135                                        .child(description),
136                                )
137                            }),
138                    )
139                    .when_some(component.preview(), |this, preview| {
140                        this.child(preview(window, cx))
141                    }),
142            )
143            .into_any_element()
144    }
145}
146
147impl Render for ComponentPreview {
148    fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
149        h_flex()
150            .id("component-preview")
151            .key_context("ComponentPreview")
152            .items_start()
153            .overflow_hidden()
154            .size_full()
155            .track_focus(&self.focus_handle)
156            .px_2()
157            .bg(cx.theme().colors().editor_background)
158            .child(
159                uniform_list(
160                    cx.entity().clone(),
161                    "component-nav",
162                    self.components.len(),
163                    move |this, range, _window, cx| {
164                        range
165                            .map(|ix| this.render_sidebar_entry(ix, ix == this.selected_index, cx))
166                            .collect()
167                    },
168                )
169                .track_scroll(self.nav_scroll_handle.clone())
170                .pt_4()
171                .w(px(240.))
172                .h_full()
173                .flex_grow(),
174            )
175            .child(
176                v_flex()
177                    .id("component-list")
178                    .px_8()
179                    .pt_4()
180                    .size_full()
181                    .child(
182                        list(self.component_list.clone())
183                            .flex_grow()
184                            .with_sizing_behavior(gpui::ListSizingBehavior::Auto),
185                    ),
186            )
187    }
188}
189
190impl EventEmitter<ItemEvent> for ComponentPreview {}
191
192impl Focusable for ComponentPreview {
193    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
194        self.focus_handle.clone()
195    }
196}
197
198impl Item for ComponentPreview {
199    type Event = ItemEvent;
200
201    fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
202        Some("Component Preview".into())
203    }
204
205    fn telemetry_event_text(&self) -> Option<&'static str> {
206        None
207    }
208
209    fn show_toolbar(&self) -> bool {
210        false
211    }
212
213    fn clone_on_split(
214        &self,
215        _workspace_id: Option<WorkspaceId>,
216        window: &mut Window,
217        cx: &mut Context<Self>,
218    ) -> Option<gpui::Entity<Self>>
219    where
220        Self: Sized,
221    {
222        Some(cx.new(|cx| Self::new(window, cx)))
223    }
224
225    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
226        f(*event)
227    }
228}
229
230// TODO: impl serializable item for component preview so it will restore with the workspace
231// ref: https://github.com/zed-industries/zed/blob/32201ac70a501e63dfa2ade9c00f85aea2d4dd94/crates/image_viewer/src/image_viewer.rs#L199
232// Use `ImageViewer` as a model for how to do it, except it'll be even simpler