Skip to main content

perspective_viewer/components/form/
debug.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::rc::Rc;
14
15use perspective_client::ExprValidationError;
16use perspective_js::utils::{ApiFuture, JsValueSerdeExt};
17use wasm_bindgen::prelude::*;
18use yew::prelude::*;
19
20use crate::components::form::code_editor::CodeEditor;
21use crate::js::{MimeType, copy_to_clipboard, paste_from_clipboard};
22use crate::presentation::*;
23use crate::renderer::*;
24use crate::session::*;
25use crate::utils::*;
26
27#[derive(Clone, PartialEq, Properties)]
28pub struct DebugPanelProps {
29    pub presentation: Presentation,
30    pub renderer: Renderer,
31    pub session: Session,
32
33    /// Trap-door width pinned by the parent `SettingsPanel` so switching
34    /// tabs doesn't shrink the panel. Threaded into the hidden sizer
35    /// `<div class="scroll-panel-auto-width">`.
36    #[prop_or_default]
37    pub initial_width: f64,
38
39    /// Fires once on mount with this panel's measured natural width.
40    /// Routed up to `SettingsPanel` which keeps the running max.
41    #[prop_or_default]
42    pub on_auto_width: Callback<f64>,
43}
44
45#[function_component(DebugPanel)]
46pub fn debug_panel(props: &DebugPanelProps) -> Html {
47    let expr = use_state_eq(|| Rc::new("".to_string()));
48    let error = use_state_eq(|| Option::<ExprValidationError>::None);
49    let select_all = use_memo((), |()| PubSub::default());
50    let modified = use_state_eq(|| false);
51
52    // Measure natural width on mount and route up to `SettingsPanel`.
53    let sizer = use_node_ref();
54    use_effect_with(expr.clone(), {
55        let sizer = sizer.clone();
56        let on_auto_width = props.on_auto_width.clone();
57        move |_| {
58            if let Some(elem) = sizer.cast::<web_sys::HtmlElement>() {
59                on_auto_width.emit(elem.get_bounding_client_rect().width());
60            }
61        }
62    });
63
64    use_effect_with((expr.setter(), props.clone()), {
65        clone!(error, modified);
66        move |(text, state)| {
67            state.set_text(text.clone());
68            error.set(None);
69            let sub1 = state
70                .renderer
71                .style_changed
72                .add_listener(state.reset_callback(
73                    text.clone(),
74                    error.setter(),
75                    modified.setter(),
76                ));
77
78            let sub2 = state
79                .renderer
80                .reset_changed
81                .add_listener(state.reset_callback(
82                    text.clone(),
83                    error.setter(),
84                    modified.setter(),
85                ));
86
87            let sub3 = state
88                .session
89                .view_config_changed
90                .add_listener(state.reset_callback(
91                    text.clone(),
92                    error.setter(),
93                    modified.setter(),
94                ));
95
96            || {
97                drop(sub1);
98                drop(sub2);
99                drop(sub3);
100            }
101        }
102    });
103
104    let oninput = use_callback(expr.setter(), {
105        clone!(modified);
106        move |x, expr| {
107            modified.set(true);
108            expr.set(x)
109        }
110    });
111
112    let onsave = use_callback((expr.clone(), error.clone(), props.clone()), {
113        clone!(modified);
114        move |_, (text, error, props)| props.on_save(text, error, &modified)
115    });
116
117    let oncopy = use_callback(
118        (expr.clone(), select_all.callback()),
119        move |_, (text, select_all)| {
120            select_all.emit(());
121            let options = web_sys::BlobPropertyBag::new();
122            options.set_type("text/plain");
123            let blob_txt = (JsValue::from((***text).clone())).clone();
124            let blob_parts = js_sys::Array::from_iter([blob_txt].iter());
125            let blob = web_sys::Blob::new_with_str_sequence_and_options(&blob_parts, &options);
126            ApiFuture::spawn(copy_to_clipboard(
127                async move { Ok(blob?) },
128                MimeType::TextPlain,
129            ));
130        },
131    );
132
133    let onapply = use_callback((expr.clone(), error.clone(), props.clone()), {
134        clone!(modified);
135        move |_, (text, error, props)| props.on_save(text, error, &modified)
136    });
137
138    let onreset = use_callback((expr.setter(), error.clone(), props.clone()), {
139        clone!(modified);
140        move |_, (text, error, props)| {
141            props.set_text(text.clone());
142            error.set(None);
143            modified.set(false);
144        }
145    });
146
147    let onpaste = use_callback((expr.clone(), error.clone(), props.clone()), {
148        clone!(modified);
149        move |_, (text, error, props)| {
150            clone!(text, error, props, modified);
151            ApiFuture::spawn(async move {
152                if let Some(x) = paste_from_clipboard().await {
153                    let x = Rc::new(x);
154                    modified.set(true);
155                    error.set(None);
156                    text.set(x.clone());
157                    props.on_save(&x, &error, &modified);
158                }
159
160                Ok(())
161            });
162        }
163    });
164
165    html! {
166        <>
167            <div id="debug-panel-overflow">
168                <div id="debug-panel" class="sidebar_column" ref={sizer}>
169                    <div id="debug-panel-controls">
170                        <button disabled={!*modified} onclick={onapply}>{ "Apply" }</button>
171                        <button disabled={!*modified} onclick={onreset}>{ "Reset" }</button>
172                        <button onclick={oncopy}>{ "Copy" }</button>
173                        <button onclick={onpaste}>{ "Paste" }</button>
174                    </div>
175                    <div id="debug-panel-editor">
176                        <CodeEditor
177                            expr={&*expr}
178                            disabled=false
179                            {oninput}
180                            {onsave}
181                            select_all={select_all.subscriber()}
182                            error={(*error).clone()}
183                        />
184                    </div>
185                    <div
186                        class="scroll-panel-auto-width"
187                        style={format!("width:{}px", props.initial_width)}
188                    />
189                </div>
190            </div>
191        </>
192    }
193}
194
195impl DebugPanelProps {
196    fn set_text(&self, setter: UseStateSetter<Rc<String>>) {
197        let props = self.clone();
198        ApiFuture::spawn(async move {
199            let config = crate::queries::get_viewer_config(
200                &props.session,
201                &props.renderer,
202                &props.presentation,
203            )
204            .await?;
205            let json = JsValue::from_serde_ext(&config)?;
206            let js_string =
207                js_sys::JSON::stringify_with_replacer_and_space(&json, &JsValue::NULL, &2.into())?;
208
209            setter.set(Rc::new(js_string.as_string().unwrap()));
210            Ok(())
211        });
212    }
213
214    fn reset_callback(
215        &self,
216        text: UseStateSetter<Rc<String>>,
217        error: UseStateSetter<Option<ExprValidationError>>,
218        modified: UseStateSetter<bool>,
219    ) -> impl Fn(()) + use<> {
220        let props = self.clone();
221        move |_| {
222            error.set(None);
223            props.set_text(text.clone());
224            modified.set(false);
225        }
226    }
227
228    fn on_save(
229        &self,
230        text: &Rc<String>,
231        error: &UseStateHandle<Option<ExprValidationError>>,
232        modified: &UseStateHandle<bool>,
233    ) {
234        let props = self.clone();
235        clone!(text, error, modified);
236        ApiFuture::spawn(async move {
237            match serde_json::from_str(&text) {
238                Ok(config) => {
239                    match crate::tasks::restore_and_render(
240                        &props.session,
241                        &props.renderer,
242                        &props.presentation,
243                        config,
244                        async { Ok(()) },
245                    )
246                    .await
247                    {
248                        Ok(_) => {
249                            modified.set(false);
250                        },
251                        Err(e) => {
252                            modified.set(true);
253                            error.set(Some(ExprValidationError {
254                                error_message: JsValue::from(e).as_string().unwrap_or_else(|| {
255                                    "Failed to validate viewer config".to_owned()
256                                }),
257                                line: 0_u32,
258                                column: 0,
259                            }));
260                        },
261                    }
262                    Ok(())
263                },
264                Err(err) => {
265                    modified.set(true);
266                    error.set(Some(ExprValidationError {
267                        error_message: err.to_string(),
268                        line: err.line() as u32 - 1,
269                        column: err.column() as u32 - 1,
270                    }));
271
272                    Ok(())
273                },
274            }
275        });
276    }
277}