Skip to main content

gpui_devtools/
lib.rs

1use gpui::{
2    App, Context, Div, DivInspectorState, Inspector, InspectorElementId, IntoElement, KeyBinding,
3    Window, actions, div, prelude::*, rgb,
4};
5
6const DEFAULT_MACOS_KEY_BINDING: &str = "cmd-alt-i";
7const DEFAULT_OTHER_KEY_BINDING: &str = "ctrl-alt-i";
8
9actions!(gpui_devtools, [ToggleInspector]);
10
11#[derive(Clone, Debug)]
12pub struct Config {
13    pub key_binding: Option<&'static str>,
14    pub background: u32,
15    pub panel_background: u32,
16    pub border: u32,
17    pub text: u32,
18    pub muted_text: u32,
19    pub accent: u32,
20}
21
22impl Default for Config {
23    fn default() -> Self {
24        Self {
25            key_binding: Some(default_key_binding()),
26            background: 0x111318,
27            panel_background: 0x191c22,
28            border: 0x30343d,
29            text: 0xe6e9ef,
30            muted_text: 0x9299a8,
31            accent: 0x61afef,
32        }
33    }
34}
35
36impl Config {
37    pub fn key_binding(mut self, key_binding: Option<&'static str>) -> Self {
38        self.key_binding = key_binding;
39        self
40    }
41}
42
43pub fn init(cx: &mut App) {
44    init_with(Config::default(), cx);
45}
46
47pub fn init_with(config: Config, cx: &mut App) {
48    if let Some(key_binding) = config.key_binding {
49        cx.bind_keys([KeyBinding::new(key_binding, ToggleInspector, None)]);
50    }
51
52    cx.on_action(|_: &ToggleInspector, cx| toggle_active_window(cx));
53
54    let div_config = config.clone();
55    cx.register_inspector_element(move |_id, state: &DivInspectorState, _window, _cx| {
56        render_div_state(state, &div_config)
57    });
58
59    cx.set_inspector_renderer(Box::new(move |inspector, window, cx| {
60        render_inspector(inspector, window, cx, &config).into_any_element()
61    }));
62}
63
64pub fn toggle_active_window(cx: &mut App) {
65    let Some(active_window) = cx.active_window() else {
66        return;
67    };
68
69    cx.defer(move |cx| {
70        let _ = active_window.update(cx, |_, window, cx| window.toggle_inspector(cx));
71    });
72}
73
74fn render_inspector(
75    inspector: &mut Inspector,
76    window: &mut Window,
77    cx: &mut Context<Inspector>,
78    config: &Config,
79) -> Div {
80    let active_element = inspector.active_element_id().cloned();
81
82    div()
83        .size_full()
84        .flex()
85        .flex_col()
86        .bg(rgb(config.background))
87        .text_color(rgb(config.text))
88        .border_l_1()
89        .border_color(rgb(config.border))
90        .child(
91            div()
92                .h_12()
93                .px_3()
94                .flex()
95                .items_center()
96                .justify_between()
97                .border_b_1()
98                .border_color(rgb(config.border))
99                .child(
100                    div()
101                        .font_weight(gpui::FontWeight::SEMIBOLD)
102                        .child("GPUI DevTools"),
103                )
104                .child(
105                    div()
106                        .id("gpui-devtools-pick")
107                        .px_2()
108                        .py_1()
109                        .rounded_md()
110                        .cursor_pointer()
111                        .bg(if inspector.is_picking() {
112                            rgb(config.accent)
113                        } else {
114                            rgb(config.panel_background)
115                        })
116                        .child(if inspector.is_picking() {
117                            "Picking"
118                        } else {
119                            "Pick"
120                        })
121                        .on_click(cx.listener(|inspector, _, window, _cx| {
122                            inspector.start_picking();
123                            window.refresh();
124                        })),
125                ),
126        )
127        .child(
128            div()
129                .id("gpui-devtools-content")
130                .flex_1()
131                .overflow_y_scroll()
132                .p_3()
133                .flex()
134                .flex_col()
135                .gap_3()
136                .when_some(active_element, |panel, id| {
137                    panel.child(render_element_id(&id, config))
138                })
139                .children(inspector.render_inspector_states(window, cx)),
140        )
141}
142
143fn render_element_id(id: &InspectorElementId, config: &Config) -> Div {
144    let location = source_location(id);
145
146    section("Element", config)
147        .child(property("Source", location, config))
148        .child(property("Instance", id.instance_id.to_string(), config))
149        .child(property("Global ID", id.path.global_id.to_string(), config))
150}
151
152fn render_div_state(state: &DivInspectorState, config: &Config) -> Div {
153    section("Layout", config)
154        .child(property("Origin", state.bounds.origin.to_string(), config))
155        .child(property("Size", state.bounds.size.to_string(), config))
156        .child(property("Content", state.content_size.to_string(), config))
157        .child(property(
158            "Style refinement",
159            format!("{:#?}", state.base_style),
160            config,
161        ))
162}
163
164fn section(title: &'static str, config: &Config) -> Div {
165    div()
166        .p_3()
167        .flex()
168        .flex_col()
169        .gap_2()
170        .rounded_md()
171        .bg(rgb(config.panel_background))
172        .border_1()
173        .border_color(rgb(config.border))
174        .child(div().font_weight(gpui::FontWeight::SEMIBOLD).child(title))
175}
176
177fn property(label: &'static str, value: String, config: &Config) -> Div {
178    div()
179        .flex()
180        .flex_col()
181        .gap_1()
182        .child(
183            div()
184                .text_xs()
185                .text_color(rgb(config.muted_text))
186                .child(label),
187        )
188        .child(div().text_sm().font_family("monospace").child(value))
189}
190
191fn source_location(id: &InspectorElementId) -> String {
192    let location = id.path.source_location;
193    format!(
194        "{}:{}:{}",
195        location.file(),
196        location.line(),
197        location.column()
198    )
199}
200
201const fn default_key_binding() -> &'static str {
202    if cfg!(target_os = "macos") {
203        DEFAULT_MACOS_KEY_BINDING
204    } else {
205        DEFAULT_OTHER_KEY_BINDING
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn config_can_disable_the_default_key_binding() {
215        assert_eq!(Config::default().key_binding(None).key_binding, None);
216    }
217
218    #[test]
219    fn default_key_binding_matches_the_platform() {
220        let expected = if cfg!(target_os = "macos") {
221            "cmd-alt-i"
222        } else {
223            "ctrl-alt-i"
224        };
225        assert_eq!(default_key_binding(), expected);
226    }
227}