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::JsCast;
18use wasm_bindgen::prelude::*;
19use web_sys::{FocusEvent, KeyboardEvent};
20use yew::prelude::*;
21
22use super::containers::split_panel::SplitPanel;
23use super::font_loader::{FontLoader, FontLoaderProps, FontLoaderStatus};
24use super::style::{LocalStyle, StyleProvider};
25use crate::components::column_settings_sidebar::ColumnSettingsPanel;
26use crate::components::main_panel::MainPanel;
27use crate::components::settings_panel::{SelectedTab, SettingsPanel};
28use crate::config::*;
29use crate::css;
30use crate::js::JsPerspectiveViewerPlugin;
31use crate::presentation::{
32    ColumnLocator, ColumnSettingsTab, DragDropProps, Presentation, PresentationProps,
33};
34use crate::queries::*;
35use crate::renderer::{RendererProps, *};
36use crate::session::{SessionProps, *};
37use crate::tasks::*;
38use crate::utils::*;
39
40#[derive(Clone, Properties)]
41pub struct PerspectiveViewerProps {
42    /// The light DOM element this component will render to.
43    pub elem: web_sys::HtmlElement,
44
45    /// State
46    pub session: Session,
47    pub renderer: Renderer,
48    pub presentation: Presentation,
49}
50
51impl PartialEq for PerspectiveViewerProps {
52    fn eq(&self, _rhs: &Self) -> bool {
53        false
54    }
55}
56
57#[derive(Debug)]
58pub enum PerspectiveViewerMsg {
59    ColumnSettingsPanelSizeUpdate(Option<i32>),
60    ColumnSettingsTabChanged(ColumnSettingsTab),
61    OpenColumnSettings {
62        locator: Option<ColumnLocator>,
63        sender: Option<Sender<()>>,
64        toggle: bool,
65    },
66    PreloadFontsUpdate,
67    Reset(bool, Option<Sender<()>>),
68    Resize,
69    SettingsPanelSizeUpdate(Option<i32>),
70    SettingsPanelTabChanged(SelectedTab),
71    SettingsPanelAutoWidth(f64),
72    ToggleDebug,
73    ToggleSettingsComplete(SettingsUpdate, Sender<()>),
74    ToggleSettingsInit(Option<SettingsUpdate>, Option<Sender<ApiResult<JsValue>>>),
75    UpdateSession(Box<SessionProps>),
76    UpdateRenderer(Box<RendererProps>),
77    UpdatePresentation(Box<PresentationProps>),
78
79    /// Update only `is_settings_open` in the presentation snapshot without
80    /// touching `available_themes` (which requires async data).
81    UpdateSettingsOpen(bool),
82    UpdateIsWorkspace(bool),
83
84    /// Update only `open_column_settings` in the presentation snapshot.
85    UpdateColumnSettings(Box<crate::presentation::OpenColumnSettings>),
86    UpdateDragDrop(Box<DragDropProps>),
87
88    /// Update only stats-related fields of `session_props` without touching
89    /// `config`.  This prevents `stats_changed` events (e.g. from `reset()`)
90    /// from propagating a freshly-cleared config to the column selector.
91    UpdateSessionStats(Option<ViewStats>, Option<TableLoadState>),
92
93    /// Increment/decrement the in-flight render counter threaded to
94    /// `StatusIndicator` so it can show the "updating" spinner.
95    IncrementUpdateCount,
96    DecrementUpdateCount,
97}
98
99use PerspectiveViewerMsg::*;
100
101pub struct PerspectiveViewer {
102    _subscriptions: Vec<Subscription>,
103    column_settings_panel_width_override: Option<i32>,
104    debug_open: bool,
105    fonts: FontLoaderProps,
106    on_close_column_settings: Callback<()>,
107    on_rendered: Option<Sender<()>>,
108    on_resize: Rc<PubSub<()>>,
109    on_settings_panel_dimensions_reset: Rc<PubSub<()>>,
110    settings_open: bool,
111    settings_panel_width_override: Option<i32>,
112    settings_panel_selected_tab: SelectedTab,
113    settings_panel_auto_width: f64,
114
115    /// Value-semantic state snapshots (Step 4 scaffold).
116    /// Populated by `UpdateSession` / `UpdateRenderer` / `UpdatePresentation` /
117    /// `UpdateDragDrop` messages dispatched from async engine tasks.
118    session_props: SessionProps,
119    renderer_props: RendererProps,
120    presentation_props: PresentationProps,
121    dragdrop_props: DragDropProps,
122
123    /// Counts in-flight renders (incremented on `view_config_changed`,
124    /// decremented on `view_created`). Threaded to `StatusIndicator`.
125    update_count: u32,
126
127    /// Window listeners that toggle the `.shift-active` class on the host
128    /// element while the Shift key is held, making Shift-modified affordances
129    /// (e.g. inactive column add, active column remove, status-bar reset)
130    /// visually discoverable. Stored so the closures outlive `create`.
131    _shift_listeners: ShiftListeners,
132}
133
134struct ShiftListeners {
135    elem: web_sys::HtmlElement,
136    keydown: Closure<dyn FnMut(KeyboardEvent)>,
137    keyup: Closure<dyn FnMut(KeyboardEvent)>,
138    blur: Closure<dyn FnMut(FocusEvent)>,
139}
140
141impl Drop for ShiftListeners {
142    fn drop(&mut self) {
143        let win = global::window();
144        let _ = win
145            .remove_event_listener_with_callback("keydown", self.keydown.as_ref().unchecked_ref());
146        let _ =
147            win.remove_event_listener_with_callback("keyup", self.keyup.as_ref().unchecked_ref());
148        let _ = win.remove_event_listener_with_callback("blur", self.blur.as_ref().unchecked_ref());
149        let _ = self.elem.class_list().remove_1("shift-active");
150    }
151}
152
153fn install_shift_listeners(elem: web_sys::HtmlElement) -> ShiftListeners {
154    let keydown = {
155        let elem = elem.clone();
156        Closure::wrap(Box::new(move |event: KeyboardEvent| {
157            if event.key() == "Shift" {
158                let _ = elem.class_list().add_1("shift-active");
159            }
160        }) as Box<dyn FnMut(KeyboardEvent)>)
161    };
162
163    let keyup = {
164        let elem = elem.clone();
165        Closure::wrap(Box::new(move |event: KeyboardEvent| {
166            if event.key() == "Shift" {
167                let _ = elem.class_list().remove_1("shift-active");
168            }
169        }) as Box<dyn FnMut(KeyboardEvent)>)
170    };
171
172    let blur = {
173        let elem = elem.clone();
174        Closure::wrap(Box::new(move |_: FocusEvent| {
175            let _ = elem.class_list().remove_1("shift-active");
176        }) as Box<dyn FnMut(FocusEvent)>)
177    };
178
179    let win = global::window();
180    let _ = win.add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref());
181    let _ = win.add_event_listener_with_callback("keyup", keyup.as_ref().unchecked_ref());
182    let _ = win.add_event_listener_with_callback("blur", blur.as_ref().unchecked_ref());
183
184    ShiftListeners {
185        elem,
186        keydown,
187        keyup,
188        blur,
189    }
190}
191
192impl Component for PerspectiveViewer {
193    type Message = PerspectiveViewerMsg;
194    type Properties = PerspectiveViewerProps;
195
196    fn create(ctx: &Context<Self>) -> Self {
197        let elem = ctx.props().elem.clone();
198        let fonts = FontLoaderProps::new(&elem, ctx.link().callback(|()| PreloadFontsUpdate));
199        inject_engine_callbacks(ctx);
200        let subscriptions = create_subscriptions(ctx);
201        let session_props = ctx.props().session.to_props();
202        let renderer_props = ctx.props().renderer.to_props(None);
203        let presentation_props = ctx.props().presentation.to_props(PtrEqRc::new(vec![]));
204
205        // Memoized callback for column settings drawer
206        let on_close_column_settings = ctx.link().callback(|_| OpenColumnSettings {
207            locator: None,
208            sender: None,
209            toggle: false,
210        });
211
212        // Kick off an initial async theme fetch so that `available_themes` is
213        // populated even if `theme_config_updated` fires before the PubSub
214        // subscription is registered.
215        {
216            let presentation = ctx.props().presentation.clone();
217            let cb = ctx.link().callback(move |themes: PtrEqRc<Vec<String>>| {
218                UpdatePresentation(Box::new(presentation.to_props(themes)))
219            });
220
221            let presentation = ctx.props().presentation.clone();
222            ApiFuture::spawn(async move {
223                let themes = presentation.get_available_themes().await?;
224                cb.emit(themes);
225                Ok(())
226            });
227        }
228
229        let shift_listeners = install_shift_listeners(elem);
230
231        Self {
232            _subscriptions: subscriptions,
233            column_settings_panel_width_override: None,
234            debug_open: false,
235            fonts,
236            on_close_column_settings,
237            on_rendered: None,
238            on_resize: Default::default(),
239            on_settings_panel_dimensions_reset: Default::default(),
240            settings_open: false,
241            settings_panel_width_override: None,
242            settings_panel_selected_tab: SelectedTab::default(),
243            settings_panel_auto_width: 0.0,
244            session_props,
245            renderer_props,
246            presentation_props,
247            dragdrop_props: DragDropProps::default(),
248            update_count: 0,
249            _shift_listeners: shift_listeners,
250        }
251    }
252
253    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
254        match msg {
255            PreloadFontsUpdate => true,
256            Resize => {
257                self.on_resize.emit(());
258                false
259            },
260            Reset(all, sender) => {
261                reset_all(
262                    &ctx.props().session,
263                    &ctx.props().renderer,
264                    &ctx.props().presentation,
265                    all,
266                    sender,
267                );
268                false
269            },
270            ToggleSettingsInit(Some(SettingsUpdate::Missing), None) => false,
271            ToggleSettingsInit(Some(SettingsUpdate::Missing), Some(resolve)) => {
272                resolve.send(Ok(JsValue::UNDEFINED)).unwrap();
273                false
274            },
275            ToggleSettingsInit(Some(SettingsUpdate::SetDefault), resolve) => {
276                self.init_toggle_settings_task(ctx, Some(false), resolve);
277                false
278            },
279            ToggleSettingsInit(Some(SettingsUpdate::Update(force)), resolve) => {
280                self.init_toggle_settings_task(ctx, Some(force), resolve);
281                false
282            },
283            ToggleSettingsInit(None, resolve) => {
284                self.init_toggle_settings_task(ctx, None, resolve);
285                false
286            },
287            ToggleSettingsComplete(SettingsUpdate::SetDefault, resolve) if self.settings_open => {
288                ctx.props().presentation.set_open_column_settings(None);
289                self.settings_open = false;
290                self.on_rendered = Some(resolve);
291                true
292            },
293            ToggleSettingsComplete(SettingsUpdate::Update(force), resolve)
294                if force != self.settings_open =>
295            {
296                ctx.props().presentation.set_open_column_settings(None);
297                self.settings_open = force;
298                self.on_rendered = Some(resolve);
299                true
300            },
301            ToggleSettingsComplete(_, resolve)
302                if matches!(self.fonts.get_status(), FontLoaderStatus::Finished) =>
303            {
304                if let Err(e) = resolve.send(()) {
305                    tracing::error!("toggle settings failed {:?}", e);
306                }
307
308                false
309            },
310            ToggleSettingsComplete(_, resolve) => {
311                ctx.props().presentation.set_open_column_settings(None);
312                self.on_rendered = Some(resolve);
313                true
314            },
315            OpenColumnSettings {
316                locator,
317                sender,
318                toggle,
319            } => {
320                let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
321                if locator == open_column_settings.locator {
322                    if toggle {
323                        ctx.props().presentation.set_open_column_settings(None);
324                    }
325                } else {
326                    open_column_settings.locator.clone_from(&locator);
327                    open_column_settings.tab =
328                        if matches!(locator, Some(ColumnLocator::NewExpression)) {
329                            Some(ColumnSettingsTab::Attributes)
330                        } else {
331                            locator.as_ref().and_then(|x| {
332                                x.name().map(|x| {
333                                    if self.session_props.is_column_active(x) {
334                                        ColumnSettingsTab::Style
335                                    } else {
336                                        ColumnSettingsTab::Attributes
337                                    }
338                                })
339                            })
340                        };
341
342                    ctx.props()
343                        .presentation
344                        .set_open_column_settings(Some(open_column_settings));
345
346                    if locator.is_some() {
347                        self.settings_panel_selected_tab = SelectedTab::Query;
348                    }
349                }
350
351                if let Some(sender) = sender {
352                    sender.send(()).unwrap();
353                }
354
355                true
356            },
357            SettingsPanelSizeUpdate(Some(x)) => {
358                self.settings_panel_width_override = Some(x);
359                false
360            },
361            SettingsPanelSizeUpdate(None) => {
362                self.settings_panel_width_override = None;
363                self.settings_panel_auto_width = 0.0;
364                self.on_settings_panel_dimensions_reset.emit(());
365                true
366            },
367            SettingsPanelTabChanged(tab) => {
368                let changed = tab != self.settings_panel_selected_tab;
369                self.settings_panel_selected_tab = tab;
370                changed
371            },
372            SettingsPanelAutoWidth(w) => {
373                if w > self.settings_panel_auto_width {
374                    self.settings_panel_auto_width = w;
375                    true
376                } else {
377                    false
378                }
379            },
380            ColumnSettingsPanelSizeUpdate(Some(x)) => {
381                self.column_settings_panel_width_override = Some(x);
382                false
383            },
384            ColumnSettingsPanelSizeUpdate(None) => {
385                self.column_settings_panel_width_override = None;
386                false
387            },
388            ColumnSettingsTabChanged(tab) => {
389                let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
390                open_column_settings.tab.clone_from(&Some(tab));
391                ctx.props()
392                    .presentation
393                    .set_open_column_settings(Some(open_column_settings));
394                true
395            },
396            ToggleDebug => {
397                self.debug_open = !self.debug_open;
398                clone!(ctx.props().renderer, ctx.props().session);
399                ApiFuture::spawn(async move {
400                    renderer.draw(session.validate().await?.create_view()).await
401                });
402
403                true
404            },
405            UpdateSession(props) => {
406                let changed = *props != self.session_props;
407                self.session_props = *props;
408                changed
409            },
410            UpdateSessionStats(stats, has_table) => {
411                let changed =
412                    stats != self.session_props.stats || has_table != self.session_props.has_table;
413                self.session_props.stats = stats;
414                self.session_props.has_table = has_table;
415                changed
416            },
417            UpdateRenderer(props) => {
418                let changed = *props != self.renderer_props;
419                self.renderer_props = *props;
420                changed
421            },
422            UpdatePresentation(props) => {
423                let changed = *props != self.presentation_props;
424                self.presentation_props = *props;
425                changed
426            },
427            UpdateSettingsOpen(open) => {
428                let changed = open != self.presentation_props.is_settings_open;
429                self.presentation_props.is_settings_open = open;
430                changed
431            },
432            UpdateIsWorkspace(is_workspace) => {
433                let changed = is_workspace != self.presentation_props.is_workspace;
434                self.presentation_props.is_workspace = is_workspace;
435                changed
436            },
437            UpdateColumnSettings(ocs) => {
438                let changed = *ocs != self.presentation_props.open_column_settings;
439                self.presentation_props.open_column_settings = *ocs;
440                changed
441            },
442            UpdateDragDrop(props) => {
443                let changed = *props != self.dragdrop_props;
444                self.dragdrop_props = *props;
445                changed
446            },
447            IncrementUpdateCount => {
448                self.update_count = self.update_count.saturating_add(1);
449                true
450            },
451            DecrementUpdateCount => {
452                self.update_count = self.update_count.saturating_sub(1);
453                true
454            },
455        }
456    }
457
458    /// This top-level component is mounted to the Custom Element, so it has no
459    /// API to provide props - but for sanity if needed, just return true on
460    /// change.
461    fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
462        true
463    }
464
465    /// On rendered call notify_resize().  This also triggers any registered
466    /// async callbacks to the Custom Element API.
467    fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
468        if self.on_rendered.is_some()
469            && matches!(self.fonts.get_status(), FontLoaderStatus::Finished)
470            && self.on_rendered.take().unwrap().send(()).is_err()
471        {
472            tracing::warn!("Orphan render");
473        }
474    }
475
476    fn view(&self, ctx: &Context<Self>) -> Html {
477        let Self::Properties {
478            presentation,
479            renderer,
480            session,
481            ..
482        } = ctx.props();
483
484        let is_settings_open = self.settings_open
485            && matches!(self.session_props.has_table, Some(TableLoadState::Loaded));
486
487        let mut class = classes!();
488        if !is_settings_open {
489            class.push("settings-closed");
490        }
491
492        if self.session_props.title.is_some() {
493            class.push("titled");
494        }
495
496        let on_open_expr_panel = ctx.link().callback(|c| OpenColumnSettings {
497            locator: c,
498            sender: None,
499            toggle: true,
500        });
501
502        let on_split_panel_resize = ctx
503            .link()
504            .callback(|(x, _)| SettingsPanelSizeUpdate(Some(x)));
505
506        let on_column_settings_panel_resize = ctx
507            .link()
508            .callback(|(x, _)| ColumnSettingsPanelSizeUpdate(Some(x)));
509
510        let on_close_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
511        let on_debug = ctx.link().callback(|_| ToggleDebug);
512        let selected_column = get_current_column_locator(
513            &self.presentation_props.open_column_settings,
514            &ctx.props().renderer,
515            &self.session_props.config,
516            &self.session_props.metadata,
517        );
518
519        let selected_tab = self.presentation_props.open_column_settings.tab;
520        let plugin_name = self.renderer_props.plugin_name.clone();
521        let available_plugins = self.renderer_props.available_plugins.clone();
522        let has_table = self.session_props.has_table.clone();
523        let named_column_count = self.renderer_props.config.config_column_names.len();
524
525        let view_config = self.session_props.config.clone();
526        let drag_column = self.dragdrop_props.column.clone();
527        let metadata = self.session_props.metadata.clone();
528        let on_select_tab = ctx.link().callback(SettingsPanelTabChanged);
529        let on_auto_width = ctx.link().callback(SettingsPanelAutoWidth);
530        let settings_panel = html! {
531            if is_settings_open {
532                <SettingsPanel
533                    on_close={on_close_settings}
534                    on_resize={&self.on_resize}
535                    on_select_column={on_open_expr_panel}
536                    is_debug={self.debug_open}
537                    {on_debug}
538                    {plugin_name}
539                    {available_plugins}
540                    {has_table}
541                    {named_column_count}
542                    {view_config}
543                    plugin_config={self.renderer_props.plugin_config.clone()}
544                    {drag_column}
545                    metadata={metadata.clone()}
546                    open_column_settings={self.presentation_props.open_column_settings.clone()}
547                    selected_theme={self.presentation_props.selected_theme.clone()}
548                    selected_tab={self.settings_panel_selected_tab}
549                    auto_width={self.settings_panel_auto_width}
550                    on_dimensions_reset={&self.on_settings_panel_dimensions_reset}
551                    {on_select_tab}
552                    {on_auto_width}
553                    {presentation}
554                    {renderer}
555                    {session}
556                />
557            }
558        };
559
560        let on_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
561        let on_select_tab = ctx.link().callback(ColumnSettingsTabChanged);
562        let column_settings_panel = html! {
563            if let Some(selected_column) = selected_column {
564                <SplitPanel
565                    id="modal_panel"
566                    reverse=true
567                    initial_size={self.column_settings_panel_width_override}
568                    on_reset={ctx.link().callback(|_| ColumnSettingsPanelSizeUpdate(None))}
569                    on_resize={on_column_settings_panel_resize}
570                >
571                    <ColumnSettingsPanel
572                        {selected_column}
573                        {selected_tab}
574                        on_close={self.on_close_column_settings.clone()}
575                        width_override={self.column_settings_panel_width_override}
576                        {on_select_tab}
577                        plugin_name={self.renderer_props.plugin_name.clone()}
578                        {metadata}
579                        view_config={self.session_props.config.clone()}
580                        column_stats={self.session_props.column_stats.clone()}
581                        selected_theme={self.presentation_props.selected_theme.clone()}
582                        {presentation}
583                        {renderer}
584                        {session}
585                    />
586                    <></>
587                </SplitPanel>
588            }
589        };
590
591        let on_reset = ctx.link().callback(|all| Reset(all, None));
592        let is_settings_open = self.settings_open
593            && matches!(self.session_props.has_table, Some(TableLoadState::Loaded));
594        let main_panel = html! {
595            <MainPanel
596                {on_settings}
597                {on_reset}
598                session_props={self.session_props.clone()}
599                renderer_props={self.renderer_props.clone()}
600                presentation_props={self.presentation_props.clone()}
601                {is_settings_open}
602                update_count={self.update_count}
603                {presentation}
604                {renderer}
605                {session}
606            />
607        };
608
609        html! {
610            <StyleProvider root={ctx.props().elem.clone()}>
611                <LocalStyle href={css!("viewer")} />
612                <div id="component_container">
613                    if is_settings_open {
614                        <SplitPanel
615                            id="app_panel"
616                            reverse=true
617                            skip_empty=true
618                            initial_size={self.settings_panel_width_override}
619                            on_reset={ctx.link().callback(|_| SettingsPanelSizeUpdate(None))}
620                            on_resize={{
621                                let size_cb = on_split_panel_resize.clone();
622                                let resize_cb = resize_callback(&ctx.props().session, &ctx.props().renderer);
623                                move |x| {
624                                    size_cb.emit(x);
625                                    resize_cb.emit(());
626                                }
627                            }}
628                            on_resize_finished={resize_callback(&ctx.props().session, &ctx.props().renderer)}
629                        >
630                            { settings_panel }
631                            <div id="main_column_container">
632                                { main_panel }
633                                { column_settings_panel }
634                            </div>
635                        </SplitPanel>
636                    } else {
637                        <div id="main_column_container">
638                            { main_panel }
639                            { column_settings_panel }
640                        </div>
641                    }
642                </div>
643                <FontLoader ..self.fonts.clone() />
644            </StyleProvider>
645        }
646    }
647
648    fn destroy(&mut self, _ctx: &Context<Self>) {}
649}
650
651impl PerspectiveViewer {
652    /// Toggle the settings, or force the settings panel either open (true) or
653    /// closed (false) explicitly.  In order to reduce apparent
654    /// screen-shear, `toggle_settings()` uses a somewhat complex render
655    /// order:  it first resize the plugin's `<div>` without moving it,
656    /// using `overflow: hidden` to hide the extra draw area;  then,
657    /// after the _async_ drawing of the plugin is complete, it will send a
658    /// message to complete the toggle action and re-render the element with
659    /// the settings removed.
660    ///
661    /// # Arguments
662    /// * `force` - Whether to explicitly set the settings panel state to
663    ///   Open/Close (`Some(true)`/`Some(false)`), or to just toggle the current
664    ///   state (`None`).
665    fn init_toggle_settings_task(
666        &mut self,
667        ctx: &Context<Self>,
668        force: Option<bool>,
669        sender: Option<Sender<ApiResult<JsValue>>>,
670    ) {
671        let is_open = ctx.props().presentation.is_settings_open();
672        ctx.props().presentation.set_settings_before_open(!is_open);
673        match force {
674            Some(force) if is_open == force => {
675                if let Some(sender) = sender {
676                    sender.send(Ok(JsValue::UNDEFINED)).unwrap();
677                }
678            },
679            Some(_) | None => {
680                let force = !is_open;
681                let callback = ctx.link().callback(move |resolve| {
682                    let update = SettingsUpdate::Update(force);
683                    ToggleSettingsComplete(update, resolve)
684                });
685
686                clone!(
687                    ctx.props().renderer,
688                    ctx.props().session,
689                    ctx.props().presentation
690                );
691
692                ApiFuture::spawn(async move {
693                    let result = if session.js_get_table().is_some() {
694                        renderer
695                            .presize(force, {
696                                let (sender, receiver) = channel::<()>();
697                                async move {
698                                    callback.emit(sender);
699                                    presentation.set_settings_open(!is_open);
700                                    Ok(receiver.await?)
701                                }
702                            })
703                            .await
704                    } else {
705                        let (sender, receiver) = channel::<()>();
706                        callback.emit(sender);
707                        presentation.set_settings_open(!is_open);
708                        receiver.await?;
709                        Ok(JsValue::UNDEFINED)
710                    };
711
712                    if let Some(sender) = sender {
713                        let msg = result.ignore_view_delete();
714                        sender
715                            .send(msg.map(|x| x.unwrap_or(JsValue::UNDEFINED)))
716                            .into_apierror()?;
717                    };
718
719                    Ok(JsValue::undefined())
720                });
721            },
722        };
723    }
724}
725
726/// Subscribe to PubSub events that still have non-root subscribers and
727/// therefore cannot yet be replaced with direct callbacks.
728fn create_subscriptions(ctx: &Context<PerspectiveViewer>) -> Vec<Subscription> {
729    let session_props_sub = {
730        let session = ctx.props().session.clone();
731        let cb = ctx
732            .link()
733            .callback(move |_: ()| UpdateSession(Box::new(session.to_props())));
734
735        let s = &ctx.props().session;
736        let sub1 = s.table_loaded.add_notify_listener(&cb);
737        let sub2 = s.table_unloaded.add_notify_listener(&cb);
738        let sub3 = s.view_created.add_notify_listener(&cb);
739        let sub4 = s.view_config_changed.add_notify_listener(&cb);
740        let sub5 = s.title_changed.add_notify_listener(&cb);
741        let sub6 = s
742            .view_config_changed
743            .add_listener(ctx.link().callback(|_| IncrementUpdateCount));
744
745        let sub7 = s
746            .view_created
747            .add_listener(ctx.link().callback(|_| DecrementUpdateCount));
748
749        // Stats fetch resolution (populates session.column_stats) triggers
750        // a fresh `SessionProps` so `column_stats` reaches downstream
751        // components and the StyleTab re-queries the schema with the
752        // new value.
753        let sub8 = s.column_stats_changed.add_notify_listener(&cb);
754
755        vec![sub1, sub2, sub3, sub4, sub5, sub6, sub7, sub8]
756    };
757
758    let renderer_props_sub = {
759        let renderer = ctx.props().renderer.clone();
760        let cb_plugin = ctx.link().callback({
761            let renderer = renderer.clone();
762            move |_: JsPerspectiveViewerPlugin| UpdateRenderer(Box::new(renderer.to_props(None)))
763        });
764
765        // Re-snapshot RendererProps when the plugin_config bucket
766        // changes (in-tab edit via `send_plugin_config`, JSON paste via
767        // `restore_and_render`, full clear via `reset_all` with
768        // `all=true`). Without this, `RendererProps.plugin_config`
769        // would stay frozen at its construct-time value and `PluginTab`
770        // would render stale.
771        let cb_plugin_config = ctx.link().callback({
772            let renderer = renderer.clone();
773            move |_: serde_json::Map<String, serde_json::Value>| {
774                UpdateRenderer(Box::new(renderer.to_props(None)))
775            }
776        });
777
778        let sub1 = ctx.props().renderer.plugin_changed.add_listener(cb_plugin);
779        let sub2 = ctx
780            .props()
781            .renderer
782            .plugin_config_changed
783            .add_listener(cb_plugin_config);
784
785        vec![sub1, sub2]
786    };
787
788    let presentation_props_sub = {
789        let presentation = ctx.props().presentation.clone();
790        let cb_settings = ctx.link().callback(UpdateSettingsOpen);
791        let cb_theme = {
792            let pres = presentation.clone();
793            ctx.link()
794                .callback(move |(themes, _): (PtrEqRc<Vec<String>>, _)| {
795                    UpdatePresentation(Box::new(pres.to_props(themes)))
796                })
797        };
798
799        let cb_column_settings = {
800            let pres = presentation.clone();
801            ctx.link().callback(move |_: (bool, Option<String>)| {
802                UpdateColumnSettings(Box::new(pres.get_open_column_settings()))
803            })
804        };
805
806        let sub1 = presentation.settings_open_changed.add_listener(cb_settings);
807        let sub2 = presentation.theme_config_updated.add_listener(cb_theme);
808        let sub3 = presentation
809            .column_settings_open_changed
810            .add_listener(cb_column_settings);
811
812        vec![sub1, sub2, sub3]
813    };
814
815    let dragdrop_props_sub = {
816        let cb_clear = ctx.link().callback(|_: ()| UpdateDragDrop(Box::default()));
817        let sub1 = ctx
818            .props()
819            .presentation
820            .drop_received
821            .add_notify_listener(&cb_clear);
822
823        vec![sub1]
824    };
825
826    let mut subscriptions = Vec::new();
827    subscriptions.extend(session_props_sub);
828    subscriptions.extend(renderer_props_sub);
829    subscriptions.extend(presentation_props_sub);
830    subscriptions.extend(dragdrop_props_sub);
831    subscriptions
832}
833
834/// Inject direct callbacks into the engine handles, replacing PubSub fields
835/// that were exclusively consumed by the root component.
836fn inject_engine_callbacks(ctx: &Context<PerspectiveViewer>) {
837    // Session: on_stats_changed
838    {
839        let session = ctx.props().session.clone();
840        let cb = ctx.link().callback(move |_: ()| {
841            UpdateSessionStats(session.get_table_stats(), session.has_table())
842        });
843
844        *ctx.props().session.on_stats_changed.borrow_mut() = Some(cb);
845    }
846
847    // Session: on_table_errored
848    {
849        let session = ctx.props().session.clone();
850        let cb = ctx
851            .link()
852            .callback(move |_: ()| UpdateSession(Box::new(session.to_props())));
853
854        *ctx.props().session.on_table_errored.borrow_mut() = Some(cb);
855    }
856
857    // Renderer: on_render_limits_changed (combines UpdateRenderer + column
858    // locator recheck that were previously two separate PubSub subscriptions).
859    {
860        clone!(
861            ctx.props().presentation,
862            ctx.props().renderer,
863            ctx.props().session
864        );
865
866        let cb = ctx.link().batch_callback(move |limits: RenderLimits| {
867            let mut msgs = vec![UpdateRenderer(Box::new(renderer.to_props(Some(limits))))];
868            if !limits.is_update {
869                let locator = get_current_column_locator(
870                    &presentation.get_open_column_settings(),
871                    &renderer,
872                    &session.get_view_config(),
873                    &session.metadata(),
874                );
875
876                msgs.push(OpenColumnSettings {
877                    locator,
878                    sender: None,
879                    toggle: false,
880                });
881            }
882
883            msgs
884        });
885
886        *ctx.props().renderer.on_render_limits_changed.borrow_mut() = Some(cb);
887    }
888
889    // Presentation: on_is_workspace_changed
890    {
891        let cb = ctx.link().callback(UpdateIsWorkspace);
892        *ctx.props()
893            .presentation
894            .on_is_workspace_changed
895            .borrow_mut() = Some(cb);
896    }
897
898    // Drag/drop: on_dragstart (post-merge: lives on Presentation)
899    {
900        let presentation = ctx.props().presentation.clone();
901        let cb = ctx.link().callback(move |_: DragEffect| {
902            UpdateDragDrop(Box::new(presentation.drag_drop_props()))
903        });
904
905        *ctx.props().presentation.on_dragstart.borrow_mut() = Some(cb);
906    }
907
908    // Drag/drop: on_dragend
909    {
910        let cb = ctx.link().callback(|_: ()| UpdateDragDrop(Box::default()));
911        *ctx.props().presentation.on_dragend.borrow_mut() = Some(cb);
912    }
913}