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::*;
26use crate::workspace::Workspace;
27
28#[derive(Clone, PartialEq, Properties)]
29pub struct DebugPanelProps {
30    pub presentation: Presentation,
31    pub renderer: Renderer,
32    pub session: Session,
33    pub workspace: Workspace,
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            <div id="debug-panel-overflow">
170                <div id="debug-panel" class="sidebar_column" ref={sizer}>
171                    <div id="debug-panel-controls">
172                        <button disabled={!*modified} onclick={onapply}>{ "Apply" }</button>
173                        <button disabled={!*modified} onclick={onreset}>{ "Reset" }</button>
174                        <button onclick={oncopy}>{ "Copy" }</button>
175                        <button onclick={onpaste}>{ "Paste" }</button>
176                    </div>
177                    <div id="debug-panel-editor">
178                        <CodeEditor
179                            expr={&*expr}
180                            disabled=false
181                            {oninput}
182                            {onsave}
183                            select_all={select_all.subscriber()}
184                            error={(*error).clone()}
185                        />
186                    </div>
187                    <div
188                        class="scroll-panel-auto-width"
189                        style={format!("width:{}px", props.initial_width)}
190                    />
191                </div>
192            </div>
193        </>
194    }
195}
196
197impl DebugPanelProps {
198    fn set_text(&self, setter: UseStateSetter<Rc<String>>) {
199        let props = self.clone();
200        ApiFuture::spawn(async move {
201            let config = crate::queries::get_viewer_config(
202                &props.session,
203                &props.renderer,
204                &props.presentation,
205            )
206            .await?;
207            let json = JsValue::from_serde_ext(&config)?;
208            let js_string =
209                js_sys::JSON::stringify_with_replacer_and_space(&json, &JsValue::NULL, &2.into())?;
210
211            setter.set(Rc::new(js_string.as_string().unwrap()));
212            Ok(())
213        });
214    }
215
216    fn reset_callback(
217        &self,
218        text: UseStateSetter<Rc<String>>,
219        error: UseStateSetter<Option<ExprValidationError>>,
220        modified: UseStateSetter<bool>,
221    ) -> impl Fn(()) + use<> {
222        let props = self.clone();
223        move |_| {
224            error.set(None);
225            props.set_text(text.clone());
226            modified.set(false);
227        }
228    }
229
230    fn on_save(
231        &self,
232        text: &Rc<String>,
233        error: &UseStateHandle<Option<ExprValidationError>>,
234        modified: &UseStateHandle<bool>,
235    ) {
236        let props = self.clone();
237        clone!(text, error, modified);
238        ApiFuture::spawn(async move {
239            match serde_json::from_str(&text) {
240                Ok(config) => {
241                    let active =
242                        props.workspace.active_renderer().as_ref() == Some(&props.renderer);
243                    match crate::tasks::restore_panel(
244                        &props.session,
245                        &props.renderer,
246                        &props.presentation,
247                        &props.workspace,
248                        crate::tasks::RestoreMode::Existing { active },
249                        config,
250                        crate::tasks::RestoreErrors::Suppress,
251                    )
252                    .await
253                    {
254                        Ok(_) => {
255                            modified.set(false);
256                        },
257                        Err(e) => {
258                            modified.set(true);
259                            error.set(Some(ExprValidationError {
260                                error_message: JsValue::from(e).as_string().unwrap_or_else(|| {
261                                    "Failed to validate viewer config".to_owned()
262                                }),
263                                line: 0_u32,
264                                column: 0,
265                            }));
266                        },
267                    }
268                    Ok(())
269                },
270                Err(err) => {
271                    modified.set(true);
272                    error.set(Some(ExprValidationError {
273                        error_message: err.to_string(),
274                        line: err.line() as u32 - 1,
275                        column: err.column() as u32 - 1,
276                    }));
277
278                    Ok(())
279                },
280            }
281        });
282    }
283}