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