Skip to main content

perspective_viewer/components/
viewer.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 futures::channel::oneshot::*;
16use perspective_js::utils::*;
17use wasm_bindgen::prelude::*;
18use yew::prelude::*;
19
20use super::containers::split_panel::SplitPanel;
21use super::font_loader::{FontLoader, FontLoaderProps, FontLoaderStatus};
22use super::form::debug::DebugPanel;
23use super::style::{LocalStyle, StyleProvider};
24use crate::components::column_settings_sidebar::ColumnSettingsPanel;
25use crate::components::main_panel::MainPanel;
26use crate::components::settings_panel::SettingsPanel;
27use crate::config::*;
28use crate::custom_events::CustomEvents;
29use crate::dragdrop::*;
30use crate::model::*;
31use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation};
32use crate::renderer::*;
33use crate::session::*;
34use crate::utils::*;
35use crate::{PerspectiveProperties, css};
36
37#[derive(Clone, Properties, PerspectiveProperties!)]
38pub struct PerspectiveViewerProps {
39    /// The light DOM element this component will render to.
40    pub elem: web_sys::HtmlElement,
41
42    /// State
43    pub custom_events: CustomEvents,
44    pub dragdrop: DragDrop,
45    pub session: Session,
46    pub renderer: Renderer,
47    pub presentation: Presentation,
48}
49
50impl PartialEq for PerspectiveViewerProps {
51    fn eq(&self, _rhs: &Self) -> bool {
52        false
53    }
54}
55
56impl PerspectiveViewerProps {
57    fn is_title(&self) -> bool {
58        self.session.get_title().is_some()
59    }
60}
61
62#[derive(Debug)]
63pub enum PerspectiveViewerMsg {
64    ColumnSettingsPanelSizeUpdate(Option<i32>),
65    ColumnSettingsTabChanged(ColumnSettingsTab),
66    OpenColumnSettings {
67        locator: Option<ColumnLocator>,
68        sender: Option<Sender<()>>,
69        toggle: bool,
70    },
71    PreloadFontsUpdate,
72    Reset(bool, Option<Sender<()>>),
73    Resize,
74    SettingsPanelSizeUpdate(Option<i32>),
75    ToggleDebug,
76    ToggleSettingsComplete(SettingsUpdate, Sender<()>),
77    ToggleSettingsInit(Option<SettingsUpdate>, Option<Sender<ApiResult<JsValue>>>),
78}
79
80use PerspectiveViewerMsg::*;
81
82pub struct PerspectiveViewer {
83    _subscriptions: [Subscription; 1],
84    column_settings_panel_width_override: Option<i32>,
85    debug_open: bool,
86    fonts: FontLoaderProps,
87    on_close_column_settings: Callback<()>,
88    on_rendered: Option<Sender<()>>,
89    on_resize: Rc<PubSub<()>>,
90    settings_open: bool,
91    settings_panel_width_override: Option<i32>,
92}
93
94impl Component for PerspectiveViewer {
95    type Message = PerspectiveViewerMsg;
96    type Properties = PerspectiveViewerProps;
97
98    fn create(ctx: &Context<Self>) -> Self {
99        let elem = ctx.props().elem.clone();
100        let fonts = FontLoaderProps::new(&elem, ctx.link().callback(|()| PreloadFontsUpdate));
101
102        let session_sub = {
103            let props = ctx.props().clone();
104            let callback = ctx.link().batch_callback(move |(update, _)| {
105                if update {
106                    vec![]
107                } else {
108                    let locator = props.get_current_column_locator();
109                    vec![OpenColumnSettings {
110                        locator,
111                        sender: None,
112                        toggle: false,
113                    }]
114                }
115            });
116
117            ctx.props()
118                .renderer
119                .render_limits_changed
120                .add_listener(callback)
121        };
122
123        let on_close_column_settings = ctx.link().callback(|_| OpenColumnSettings {
124            locator: None,
125            sender: None,
126            toggle: false,
127        });
128
129        Self {
130            _subscriptions: [session_sub],
131            column_settings_panel_width_override: None,
132            debug_open: false,
133            fonts,
134            on_close_column_settings,
135            on_rendered: None,
136            on_resize: Default::default(),
137            settings_open: false,
138            settings_panel_width_override: None,
139        }
140    }
141
142    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
143        match msg {
144            PreloadFontsUpdate => true,
145            Resize => {
146                self.on_resize.emit(());
147                false
148            },
149            Reset(all, sender) => {
150                ctx.props().presentation.set_open_column_settings(None);
151                clone!(
152                    ctx.props().renderer,
153                    ctx.props().session,
154                    ctx.props().presentation
155                );
156
157                ApiFuture::spawn(async move {
158                    session
159                        .reset(ResetOptions {
160                            config: true,
161                            expressions: all,
162                            ..ResetOptions::default()
163                        })
164                        .await?;
165                    let columns_config = if all {
166                        presentation.reset_columns_configs();
167                        None
168                    } else {
169                        Some(presentation.all_columns_configs())
170                    };
171
172                    renderer.reset(columns_config.as_ref()).await?;
173                    presentation.reset_available_themes(None).await;
174                    if all {
175                        presentation.reset_theme().await?;
176                    }
177
178                    let result = renderer.draw(session.validate().await?.create_view()).await;
179                    if let Some(sender) = sender {
180                        sender.send(()).unwrap();
181                    }
182
183                    renderer.reset_changed.emit(());
184                    result
185                });
186
187                false
188            },
189            ToggleSettingsInit(Some(SettingsUpdate::Missing), None) => false,
190            ToggleSettingsInit(Some(SettingsUpdate::Missing), Some(resolve)) => {
191                resolve.send(Ok(JsValue::UNDEFINED)).unwrap();
192                false
193            },
194            ToggleSettingsInit(Some(SettingsUpdate::SetDefault), resolve) => {
195                self.init_toggle_settings_task(ctx, Some(false), resolve);
196                false
197            },
198            ToggleSettingsInit(Some(SettingsUpdate::Update(force)), resolve) => {
199                self.init_toggle_settings_task(ctx, Some(force), resolve);
200                false
201            },
202            ToggleSettingsInit(None, resolve) => {
203                self.init_toggle_settings_task(ctx, None, resolve);
204                false
205            },
206            ToggleSettingsComplete(SettingsUpdate::SetDefault, resolve) if self.settings_open => {
207                ctx.props().presentation.set_open_column_settings(None);
208                self.settings_open = false;
209                self.on_rendered = Some(resolve);
210                true
211            },
212            ToggleSettingsComplete(SettingsUpdate::Update(force), resolve)
213                if force != self.settings_open =>
214            {
215                ctx.props().presentation.set_open_column_settings(None);
216                self.settings_open = force;
217                self.on_rendered = Some(resolve);
218                true
219            },
220            ToggleSettingsComplete(_, resolve)
221                if matches!(self.fonts.get_status(), FontLoaderStatus::Finished) =>
222            {
223                ctx.props().presentation.set_open_column_settings(None);
224                if let Err(e) = resolve.send(()) {
225                    tracing::error!("toggle settings failed {:?}", e);
226                }
227
228                false
229            },
230            ToggleSettingsComplete(_, resolve) => {
231                ctx.props().presentation.set_open_column_settings(None);
232                self.on_rendered = Some(resolve);
233                true
234            },
235            OpenColumnSettings {
236                locator,
237                sender,
238                toggle,
239            } => {
240                let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
241                if locator == open_column_settings.locator {
242                    if toggle {
243                        ctx.props().presentation.set_open_column_settings(None);
244                    }
245                } else {
246                    open_column_settings.locator.clone_from(&locator);
247                    open_column_settings.tab =
248                        if matches!(locator, Some(ColumnLocator::NewExpression)) {
249                            Some(ColumnSettingsTab::Attributes)
250                        } else {
251                            locator.as_ref().and_then(|x| {
252                                x.name().map(|x| {
253                                    if ctx.props().session.is_column_active(x) {
254                                        ColumnSettingsTab::Style
255                                    } else {
256                                        ColumnSettingsTab::Attributes
257                                    }
258                                })
259                            })
260                        };
261
262                    ctx.props()
263                        .presentation
264                        .set_open_column_settings(Some(open_column_settings));
265                }
266
267                if let Some(sender) = sender {
268                    sender.send(()).unwrap();
269                }
270
271                true
272            },
273            SettingsPanelSizeUpdate(Some(x)) => {
274                self.settings_panel_width_override = Some(x);
275                false
276            },
277            SettingsPanelSizeUpdate(None) => {
278                self.settings_panel_width_override = None;
279                false
280            },
281            ColumnSettingsPanelSizeUpdate(Some(x)) => {
282                self.column_settings_panel_width_override = Some(x);
283                false
284            },
285            ColumnSettingsPanelSizeUpdate(None) => {
286                self.column_settings_panel_width_override = None;
287                false
288            },
289            ColumnSettingsTabChanged(tab) => {
290                let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
291                open_column_settings.tab.clone_from(&Some(tab));
292                ctx.props()
293                    .presentation
294                    .set_open_column_settings(Some(open_column_settings));
295                true
296            },
297            ToggleDebug => {
298                self.debug_open = !self.debug_open;
299                clone!(ctx.props().renderer, ctx.props().session);
300                ApiFuture::spawn(async move {
301                    renderer.draw(session.validate().await?.create_view()).await
302                });
303
304                true
305            },
306        }
307    }
308
309    /// This top-level component is mounted to the Custom Element, so it has no
310    /// API to provide props - but for sanity if needed, just return true on
311    /// change.
312    fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
313        true
314    }
315
316    /// On rendered call notify_resize().  This also triggers any registered
317    /// async callbacks to the Custom Element API.
318    fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
319        if self.on_rendered.is_some()
320            && matches!(self.fonts.get_status(), FontLoaderStatus::Finished)
321            && self.on_rendered.take().unwrap().send(()).is_err()
322        {
323            tracing::warn!("Orphan render");
324        }
325    }
326
327    fn view(&self, ctx: &Context<Self>) -> Html {
328        let Self::Properties {
329            custom_events,
330            dragdrop,
331            presentation,
332            renderer,
333            session,
334            ..
335        } = ctx.props();
336
337        let is_settings_open = self.settings_open && ctx.props().session.has_table();
338        let mut class = classes!();
339        if !is_settings_open {
340            class.push("settings-closed");
341        }
342
343        if ctx.props().is_title() {
344            class.push("titled");
345        }
346
347        let on_open_expr_panel = ctx.link().callback(|c| OpenColumnSettings {
348            locator: Some(c),
349            sender: None,
350            toggle: true,
351        });
352
353        let on_split_panel_resize = ctx
354            .link()
355            .callback(|(x, _)| SettingsPanelSizeUpdate(Some(x)));
356
357        let on_column_settings_panel_resize = ctx
358            .link()
359            .callback(|(x, _)| ColumnSettingsPanelSizeUpdate(Some(x)));
360
361        let on_close_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
362        let on_debug = ctx.link().callback(|_| ToggleDebug);
363        let selected_column = ctx.props().get_current_column_locator();
364        let selected_tab = ctx.props().presentation.get_open_column_settings().tab;
365        let settings_panel = html! {
366            if is_settings_open {
367                <SettingsPanel
368                    on_close={on_close_settings}
369                    on_resize={&self.on_resize}
370                    on_select_column={on_open_expr_panel}
371                    is_debug={self.debug_open}
372                    {on_debug}
373                    {dragdrop}
374                    {presentation}
375                    {renderer}
376                    {session}
377                />
378            }
379        };
380
381        let on_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
382        let on_select_tab = ctx.link().callback(ColumnSettingsTabChanged);
383        let column_settings_panel = html! {
384            if let Some(selected_column) = selected_column {
385                <SplitPanel
386                    id="modal_panel"
387                    reverse=true
388                    initial_size={self.column_settings_panel_width_override}
389                    on_reset={ctx.link().callback(|_| ColumnSettingsPanelSizeUpdate(None))}
390                    on_resize={on_column_settings_panel_resize}
391                >
392                    <ColumnSettingsPanel
393                        {selected_column}
394                        {selected_tab}
395                        on_close={self.on_close_column_settings.clone()}
396                        width_override={self.column_settings_panel_width_override}
397                        {on_select_tab}
398                        {custom_events}
399                        {presentation}
400                        {renderer}
401                        {session}
402                    />
403                    <></>
404                </SplitPanel>
405            }
406        };
407
408        let main_panel = html! {
409            <MainPanel {on_settings} {custom_events} {presentation} {renderer} {session} />
410        };
411
412        let debug_panel = html! {
413            if self.debug_open { <DebugPanel {presentation} {renderer} {session} /> }
414        };
415
416        html! {
417            <StyleProvider root={ctx.props().elem.clone()}>
418                <LocalStyle href={css!("viewer")} />
419                <div id="component_container">
420                    if is_settings_open {
421                        <SplitPanel
422                            id="app_panel"
423                            reverse=true
424                            skip_empty=true
425                            initial_size={self.settings_panel_width_override}
426                            on_reset={ctx.link().callback(|_| SettingsPanelSizeUpdate(None))}
427                            on_resize={on_split_panel_resize.clone()}
428                            on_resize_finished={ctx.props().render_callback()}
429                        >
430                            { debug_panel }
431                            { settings_panel }
432                            <div id="main_column_container">
433                                { main_panel }
434                                { column_settings_panel }
435                            </div>
436                        </SplitPanel>
437                    } else {
438                        <div id="main_column_container">
439                            { main_panel }
440                            { column_settings_panel }
441                        </div>
442                    }
443                </div>
444                <FontLoader ..self.fonts.clone() />
445            </StyleProvider>
446        }
447    }
448
449    fn destroy(&mut self, _ctx: &Context<Self>) {}
450}
451
452impl PerspectiveViewer {
453    /// Toggle the settings, or force the settings panel either open (true) or
454    /// closed (false) explicitly.  In order to reduce apparent
455    /// screen-shear, `toggle_settings()` uses a somewhat complex render
456    /// order:  it first resize the plugin's `<div>` without moving it,
457    /// using `overflow: hidden` to hide the extra draw area;  then,
458    /// after the _async_ drawing of the plugin is complete, it will send a
459    /// message to complete the toggle action and re-render the element with
460    /// the settings removed.
461    ///
462    /// # Arguments
463    /// * `force` - Whether to explicitly set the settings panel state to
464    ///   Open/Close (`Some(true)`/`Some(false)`), or to just toggle the current
465    ///   state (`None`).
466    fn init_toggle_settings_task(
467        &mut self,
468        ctx: &Context<Self>,
469        force: Option<bool>,
470        sender: Option<Sender<ApiResult<JsValue>>>,
471    ) {
472        let is_open = ctx.props().presentation.is_settings_open();
473        ctx.props().presentation.set_settings_before_open(!is_open);
474        match force {
475            Some(force) if is_open == force => {
476                if let Some(sender) = sender {
477                    sender.send(Ok(JsValue::UNDEFINED)).unwrap();
478                }
479            },
480            Some(_) | None => {
481                let force = !is_open;
482                let callback = ctx.link().callback(move |resolve| {
483                    let update = SettingsUpdate::Update(force);
484                    ToggleSettingsComplete(update, resolve)
485                });
486
487                clone!(
488                    ctx.props().renderer,
489                    ctx.props().session,
490                    ctx.props().presentation
491                );
492
493                ApiFuture::spawn(async move {
494                    let result = if session.js_get_table().is_some() {
495                        renderer
496                            .presize(force, {
497                                let (sender, receiver) = channel::<()>();
498                                async move {
499                                    callback.emit(sender);
500                                    presentation.set_settings_open(!is_open);
501                                    Ok(receiver.await?)
502                                }
503                            })
504                            .await
505                    } else {
506                        let (sender, receiver) = channel::<()>();
507                        callback.emit(sender);
508                        presentation.set_settings_open(!is_open);
509                        receiver.await?;
510                        Ok(JsValue::UNDEFINED)
511                    };
512
513                    if let Some(sender) = sender {
514                        let msg = result.ignore_view_delete();
515                        sender
516                            .send(msg.map(|x| x.unwrap_or(JsValue::UNDEFINED)))
517                            .into_apierror()?;
518                    };
519
520                    Ok(JsValue::undefined())
521                });
522            },
523        };
524    }
525}