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