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