div_inspector.rs

  1use anyhow::Result;
  2use editor::{Editor, EditorEvent, EditorMode, MultiBuffer};
  3use gpui::{
  4    AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement, WeakEntity,
  5    Window,
  6};
  7use language::Buffer;
  8use language::language_settings::SoftWrap;
  9use project::{Project, ProjectPath};
 10use std::path::Path;
 11use ui::{Label, LabelSize, Tooltip, prelude::*, v_flex};
 12
 13/// Path used for unsaved buffer that contains style json. To support the json language server, this
 14/// matches the name used in the generated schemas.
 15const ZED_INSPECTOR_STYLE_PATH: &str = "/zed-inspector-style.json";
 16
 17pub(crate) struct DivInspector {
 18    project: Entity<Project>,
 19    inspector_id: Option<InspectorElementId>,
 20    state: Option<DivInspectorState>,
 21    style_buffer: Option<Entity<Buffer>>,
 22    style_editor: Option<Entity<Editor>>,
 23    last_error: Option<SharedString>,
 24}
 25
 26impl DivInspector {
 27    pub fn new(
 28        project: Entity<Project>,
 29        window: &mut Window,
 30        cx: &mut Context<Self>,
 31    ) -> DivInspector {
 32        // Open the buffer once, so it can then be used for each editor.
 33        cx.spawn_in(window, {
 34            let project = project.clone();
 35            async move |this, cx| Self::open_style_buffer(project, this, cx).await
 36        })
 37        .detach();
 38
 39        DivInspector {
 40            project,
 41            inspector_id: None,
 42            state: None,
 43            style_buffer: None,
 44            style_editor: None,
 45            last_error: None,
 46        }
 47    }
 48
 49    async fn open_style_buffer(
 50        project: Entity<Project>,
 51        this: WeakEntity<DivInspector>,
 52        cx: &mut AsyncWindowContext,
 53    ) -> Result<()> {
 54        let worktree = project
 55            .update(cx, |project, cx| {
 56                project.create_worktree(ZED_INSPECTOR_STYLE_PATH, false, cx)
 57            })?
 58            .await?;
 59
 60        let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
 61            worktree_id: worktree.id(),
 62            path: Path::new("").into(),
 63        })?;
 64
 65        let style_buffer = project
 66            .update(cx, |project, cx| project.open_path(project_path, cx))?
 67            .await?
 68            .1;
 69
 70        project.update(cx, |project, cx| {
 71            project.register_buffer_with_language_servers(&style_buffer, cx)
 72        })?;
 73
 74        this.update_in(cx, |this, window, cx| {
 75            this.style_buffer = Some(style_buffer);
 76            if let Some(id) = this.inspector_id.clone() {
 77                let state =
 78                    window.with_inspector_state(Some(&id), cx, |state, _window| state.clone());
 79                if let Some(state) = state {
 80                    this.update_inspected_element(&id, state, window, cx);
 81                    cx.notify();
 82                }
 83            }
 84        })?;
 85
 86        Ok(())
 87    }
 88
 89    pub fn update_inspected_element(
 90        &mut self,
 91        id: &InspectorElementId,
 92        state: DivInspectorState,
 93        window: &mut Window,
 94        cx: &mut Context<Self>,
 95    ) {
 96        let base_style_json = serde_json::to_string_pretty(&state.base_style);
 97        self.state = Some(state);
 98
 99        if self.inspector_id.as_ref() == Some(id) {
100            return;
101        } else {
102            self.inspector_id = Some(id.clone());
103        }
104        let Some(style_buffer) = self.style_buffer.clone() else {
105            return;
106        };
107
108        let base_style_json = match base_style_json {
109            Ok(base_style_json) => base_style_json,
110            Err(err) => {
111                self.style_editor = None;
112                self.last_error =
113                    Some(format!("Failed to convert base_style to JSON: {err}").into());
114                return;
115            }
116        };
117        self.last_error = None;
118
119        style_buffer.update(cx, |style_buffer, cx| {
120            style_buffer.set_text(base_style_json, cx)
121        });
122
123        let style_editor = cx.new(|cx| {
124            let multi_buffer = cx.new(|cx| MultiBuffer::singleton(style_buffer, cx));
125            let mut editor = Editor::new(
126                EditorMode::full(),
127                multi_buffer,
128                Some(self.project.clone()),
129                window,
130                cx,
131            );
132            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
133            editor.set_show_line_numbers(false, cx);
134            editor.set_show_code_actions(false, cx);
135            editor.set_show_breakpoints(false, cx);
136            editor.set_show_git_diff_gutter(false, cx);
137            editor.set_show_runnables(false, cx);
138            editor.set_show_edit_predictions(Some(false), window, cx);
139            editor
140        });
141
142        cx.subscribe_in(&style_editor, window, {
143            let id = id.clone();
144            move |this, editor, event: &EditorEvent, window, cx| match event {
145                EditorEvent::BufferEdited => {
146                    let base_style_json = editor.read(cx).text(cx);
147                    match serde_json_lenient::from_str(&base_style_json) {
148                        Ok(new_base_style) => {
149                            window.with_inspector_state::<DivInspectorState, _>(
150                                Some(&id),
151                                cx,
152                                |state, _window| {
153                                    if let Some(state) = state.as_mut() {
154                                        *state.base_style = new_base_style;
155                                    }
156                                },
157                            );
158                            window.refresh();
159                            this.last_error = None;
160                        }
161                        Err(err) => this.last_error = Some(err.to_string().into()),
162                    }
163                }
164                _ => {}
165            }
166        })
167        .detach();
168
169        self.style_editor = Some(style_editor);
170    }
171}
172
173impl Render for DivInspector {
174    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
175        v_flex()
176            .size_full()
177            .gap_2()
178            .when_some(self.state.as_ref(), |this, state| {
179                this.child(
180                    v_flex()
181                        .child(Label::new("Layout").size(LabelSize::Large))
182                        .child(render_layout_state(state, cx)),
183                )
184            })
185            .when_some(self.style_editor.as_ref(), |this, style_editor| {
186                this.child(
187                    v_flex()
188                        .gap_2()
189                        .child(Label::new("Style").size(LabelSize::Large))
190                        .child(div().h_128().child(style_editor.clone()))
191                        .when_some(self.last_error.as_ref(), |this, last_error| {
192                            this.child(
193                                div()
194                                    .w_full()
195                                    .border_1()
196                                    .border_color(Color::Error.color(cx))
197                                    .child(Label::new(last_error)),
198                            )
199                        }),
200                )
201            })
202            .when_none(&self.style_editor, |this| {
203                this.child(Label::new("Loading..."))
204            })
205            .into_any_element()
206    }
207}
208
209fn render_layout_state(state: &DivInspectorState, cx: &App) -> Div {
210    v_flex()
211        .child(div().text_ui(cx).child(format!("Bounds: {}", state.bounds)))
212        .child(
213            div()
214                .id("content-size")
215                .text_ui(cx)
216                .tooltip(Tooltip::text("Size of the element's children"))
217                .child(if state.content_size != state.bounds.size {
218                    format!("Content size: {}", state.content_size)
219                } else {
220                    "".to_string()
221                }),
222        )
223}