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                ctx.props().presentation.set_open_column_settings(None);
298                if let Err(e) = resolve.send(()) {
299                    tracing::error!("toggle settings failed {:?}", e);
300                }
301
302                false
303            },
304            ToggleSettingsComplete(_, resolve) => {
305                ctx.props().presentation.set_open_column_settings(None);
306                self.on_rendered = Some(resolve);
307                true
308            },
309            OpenColumnSettings {
310                locator,
311                sender,
312                toggle,
313            } => {
314                let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
315                if locator == open_column_settings.locator {
316                    if toggle {
317                        ctx.props().presentation.set_open_column_settings(None);
318                    }
319                } else {
320                    open_column_settings.locator.clone_from(&locator);
321                    open_column_settings.tab =
322                        if matches!(locator, Some(ColumnLocator::NewExpression)) {
323                            Some(ColumnSettingsTab::Attributes)
324                        } else {
325                            locator.as_ref().and_then(|x| {
326                                x.name().map(|x| {
327                                    if self.session_props.is_column_active(x) {
328                                        ColumnSettingsTab::Style
329                                    } else {
330                                        ColumnSettingsTab::Attributes
331                                    }
332                                })
333                            })
334                        };
335
336                    ctx.props()
337                        .presentation
338                        .set_open_column_settings(Some(open_column_settings));
339                }
340
341                if let Some(sender) = sender {
342                    sender.send(()).unwrap();
343                }
344
345                true
346            },
347            SettingsPanelSizeUpdate(Some(x)) => {
348                self.settings_panel_width_override = Some(x);
349                false
350            },
351            SettingsPanelSizeUpdate(None) => {
352                self.settings_panel_width_override = None;
353                false
354            },
355            ColumnSettingsPanelSizeUpdate(Some(x)) => {
356                self.column_settings_panel_width_override = Some(x);
357                false
358            },
359            ColumnSettingsPanelSizeUpdate(None) => {
360                self.column_settings_panel_width_override = None;
361                false
362            },
363            ColumnSettingsTabChanged(tab) => {
364                let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
365                open_column_settings.tab.clone_from(&Some(tab));
366                ctx.props()
367                    .presentation
368                    .set_open_column_settings(Some(open_column_settings));
369                true
370            },
371            ToggleDebug => {
372                self.debug_open = !self.debug_open;
373                clone!(ctx.props().renderer, ctx.props().session);
374                ApiFuture::spawn(async move {
375                    renderer.draw(session.validate().await?.create_view()).await
376                });
377
378                true
379            },
380            UpdateSession(props) => {
381                let changed = *props != self.session_props;
382                self.session_props = *props;
383                changed
384            },
385            UpdateSessionStats(stats, has_table) => {
386                let changed =
387                    stats != self.session_props.stats || has_table != self.session_props.has_table;
388                self.session_props.stats = stats;
389                self.session_props.has_table = has_table;
390                changed
391            },
392            UpdateRenderer(props) => {
393                let changed = *props != self.renderer_props;
394                self.renderer_props = *props;
395                changed
396            },
397            UpdatePresentation(props) => {
398                let changed = *props != self.presentation_props;
399                self.presentation_props = *props;
400                changed
401            },
402            UpdateSettingsOpen(open) => {
403                let changed = open != self.presentation_props.is_settings_open;
404                self.presentation_props.is_settings_open = open;
405                changed
406            },
407            UpdateIsWorkspace(is_workspace) => {
408                let changed = is_workspace != self.presentation_props.is_workspace;
409                self.presentation_props.is_workspace = is_workspace;
410                changed
411            },
412            UpdateColumnSettings(ocs) => {
413                let changed = *ocs != self.presentation_props.open_column_settings;
414                self.presentation_props.open_column_settings = *ocs;
415                changed
416            },
417            UpdateDragDrop(props) => {
418                let changed = *props != self.dragdrop_props;
419                self.dragdrop_props = *props;
420                changed
421            },
422            IncrementUpdateCount => {
423                self.update_count = self.update_count.saturating_add(1);
424                true
425            },
426            DecrementUpdateCount => {
427                self.update_count = self.update_count.saturating_sub(1);
428                true
429            },
430        }
431    }
432
433    /// This top-level component is mounted to the Custom Element, so it has no
434    /// API to provide props - but for sanity if needed, just return true on
435    /// change.
436    fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
437        true
438    }
439
440    /// On rendered call notify_resize().  This also triggers any registered
441    /// async callbacks to the Custom Element API.
442    fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
443        if self.on_rendered.is_some()
444            && matches!(self.fonts.get_status(), FontLoaderStatus::Finished)
445            && self.on_rendered.take().unwrap().send(()).is_err()
446        {
447            tracing::warn!("Orphan render");
448        }
449    }
450
451    fn view(&self, ctx: &Context<Self>) -> Html {
452        let Self::Properties {
453            custom_events,
454            dragdrop,
455            presentation,
456            renderer,
457            session,
458            ..
459        } = ctx.props();
460
461        let is_settings_open = self.settings_open
462            && matches!(self.session_props.has_table, Some(TableLoadState::Loaded));
463
464        let mut class = classes!();
465        if !is_settings_open {
466            class.push("settings-closed");
467        }
468
469        if self.session_props.title.is_some() {
470            class.push("titled");
471        }
472
473        let on_open_expr_panel = ctx.link().callback(|c| OpenColumnSettings {
474            locator: Some(c),
475            sender: None,
476            toggle: true,
477        });
478
479        let on_split_panel_resize = ctx
480            .link()
481            .callback(|(x, _)| SettingsPanelSizeUpdate(Some(x)));
482
483        let on_column_settings_panel_resize = ctx
484            .link()
485            .callback(|(x, _)| ColumnSettingsPanelSizeUpdate(Some(x)));
486
487        let on_close_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
488        let on_debug = ctx.link().callback(|_| ToggleDebug);
489        let selected_column = get_current_column_locator(
490            &self.presentation_props.open_column_settings,
491            &ctx.props().renderer,
492            &self.session_props.config,
493            &self.session_props.metadata,
494        );
495
496        let selected_tab = self.presentation_props.open_column_settings.tab;
497        let plugin_name = self.renderer_props.plugin_name.clone();
498        let available_plugins = self.renderer_props.available_plugins.clone();
499        let has_table = self.session_props.has_table.clone();
500        let named_column_count = self
501            .renderer_props
502            .requirements
503            .names
504            .as_ref()
505            .map(|n| n.len())
506            .unwrap_or(0);
507
508        let view_config = self.session_props.config.clone();
509        let drag_column = self.dragdrop_props.column.clone();
510        let metadata = self.session_props.metadata.clone();
511        let settings_panel = html! {
512            if is_settings_open {
513                <SettingsPanel
514                    on_close={on_close_settings}
515                    on_resize={&self.on_resize}
516                    on_select_column={on_open_expr_panel}
517                    is_debug={self.debug_open}
518                    {on_debug}
519                    {plugin_name}
520                    {available_plugins}
521                    {has_table}
522                    {named_column_count}
523                    {view_config}
524                    {drag_column}
525                    metadata={metadata.clone()}
526                    open_column_settings={self.presentation_props.open_column_settings.clone()}
527                    selected_theme={self.presentation_props.selected_theme.clone()}
528                    {dragdrop}
529                    {presentation}
530                    {renderer}
531                    {session}
532                />
533            }
534        };
535
536        let on_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
537        let on_select_tab = ctx.link().callback(ColumnSettingsTabChanged);
538        let column_settings_panel = html! {
539            if let Some(selected_column) = selected_column {
540                <SplitPanel
541                    id="modal_panel"
542                    reverse=true
543                    initial_size={self.column_settings_panel_width_override}
544                    on_reset={ctx.link().callback(|_| ColumnSettingsPanelSizeUpdate(None))}
545                    on_resize={on_column_settings_panel_resize}
546                >
547                    <ColumnSettingsPanel
548                        {selected_column}
549                        {selected_tab}
550                        on_close={self.on_close_column_settings.clone()}
551                        width_override={self.column_settings_panel_width_override}
552                        {on_select_tab}
553                        plugin_name={self.renderer_props.plugin_name.clone()}
554                        {metadata}
555                        view_config={self.session_props.config.clone()}
556                        selected_theme={self.presentation_props.selected_theme.clone()}
557                        {custom_events}
558                        {presentation}
559                        {renderer}
560                        {session}
561                    />
562                    <></>
563                </SplitPanel>
564            }
565        };
566
567        let on_reset = ctx.link().callback(|all| Reset(all, None));
568        let render_limits = self.renderer_props.render_limits;
569        let has_table = self.session_props.has_table.clone();
570        let is_errored = self.session_props.error.is_some();
571        let stats = self.session_props.stats.clone();
572        let update_count = self.update_count;
573        let error = self.session_props.error.clone();
574        let is_settings_open = self.settings_open
575            && matches!(self.session_props.has_table, Some(TableLoadState::Loaded));
576        let title = self.session_props.title.clone();
577        let selected_theme = self.presentation_props.selected_theme.clone();
578        let available_themes = self.presentation_props.available_themes.clone();
579        let main_panel = html! {
580            <MainPanel
581                {on_settings}
582                {on_reset}
583                {render_limits}
584                {has_table}
585                {is_errored}
586                {stats}
587                {update_count}
588                {error}
589                {is_settings_open}
590                {title}
591                {selected_theme}
592                {available_themes}
593                is_workspace={self.presentation_props.is_workspace}
594                {custom_events}
595                {presentation}
596                {renderer}
597                {session}
598            />
599        };
600
601        let debug_panel = html! {
602            if self.debug_open { <DebugPanel {presentation} {renderer} {session} /> }
603        };
604
605        html! {
606            <StyleProvider root={ctx.props().elem.clone()}>
607                <LocalStyle href={css!("viewer")} />
608                <div id="component_container">
609                    if is_settings_open {
610                        <SplitPanel
611                            id="app_panel"
612                            reverse=true
613                            skip_empty=true
614                            initial_size={self.settings_panel_width_override}
615                            on_reset={ctx.link().callback(|_| SettingsPanelSizeUpdate(None))}
616                            on_resize={on_split_panel_resize.clone()}
617                            on_resize_finished={ctx.props().render_callback()}
618                        >
619                            { debug_panel }
620                            { settings_panel }
621                            <div id="main_column_container">
622                                { main_panel }
623                                { column_settings_panel }
624                            </div>
625                        </SplitPanel>
626                    } else {
627                        <div id="main_column_container">
628                            { main_panel }
629                            { column_settings_panel }
630                        </div>
631                    }
632                </div>
633                <FontLoader ..self.fonts.clone() />
634            </StyleProvider>
635        }
636    }
637
638    fn destroy(&mut self, _ctx: &Context<Self>) {}
639}
640
641impl PerspectiveViewer {
642    /// Toggle the settings, or force the settings panel either open (true) or
643    /// closed (false) explicitly.  In order to reduce apparent
644    /// screen-shear, `toggle_settings()` uses a somewhat complex render
645    /// order:  it first resize the plugin's `<div>` without moving it,
646    /// using `overflow: hidden` to hide the extra draw area;  then,
647    /// after the _async_ drawing of the plugin is complete, it will send a
648    /// message to complete the toggle action and re-render the element with
649    /// the settings removed.
650    ///
651    /// # Arguments
652    /// * `force` - Whether to explicitly set the settings panel state to
653    ///   Open/Close (`Some(true)`/`Some(false)`), or to just toggle the current
654    ///   state (`None`).
655    fn init_toggle_settings_task(
656        &mut self,
657        ctx: &Context<Self>,
658        force: Option<bool>,
659        sender: Option<Sender<ApiResult<JsValue>>>,
660    ) {
661        let is_open = ctx.props().presentation.is_settings_open();
662        ctx.props().presentation.set_settings_before_open(!is_open);
663        match force {
664            Some(force) if is_open == force => {
665                if let Some(sender) = sender {
666                    sender.send(Ok(JsValue::UNDEFINED)).unwrap();
667                }
668            },
669            Some(_) | None => {
670                let force = !is_open;
671                let callback = ctx.link().callback(move |resolve| {
672                    let update = SettingsUpdate::Update(force);
673                    ToggleSettingsComplete(update, resolve)
674                });
675
676                clone!(
677                    ctx.props().renderer,
678                    ctx.props().session,
679                    ctx.props().presentation
680                );
681
682                ApiFuture::spawn(async move {
683                    let result = if session.js_get_table().is_some() {
684                        renderer
685                            .presize(force, {
686                                let (sender, receiver) = channel::<()>();
687                                async move {
688                                    callback.emit(sender);
689                                    presentation.set_settings_open(!is_open);
690                                    Ok(receiver.await?)
691                                }
692                            })
693                            .await
694                    } else {
695                        let (sender, receiver) = channel::<()>();
696                        callback.emit(sender);
697                        presentation.set_settings_open(!is_open);
698                        receiver.await?;
699                        Ok(JsValue::UNDEFINED)
700                    };
701
702                    if let Some(sender) = sender {
703                        let msg = result.ignore_view_delete();
704                        sender
705                            .send(msg.map(|x| x.unwrap_or(JsValue::UNDEFINED)))
706                            .into_apierror()?;
707                    };
708
709                    Ok(JsValue::undefined())
710                });
711            },
712        };
713    }
714}
715
716/// Subscribe to PubSub events that still have non-root subscribers and
717/// therefore cannot yet be replaced with direct callbacks.
718fn create_subscriptions(ctx: &Context<PerspectiveViewer>) -> Vec<Subscription> {
719    let session_props_sub = {
720        let session = ctx.props().session.clone();
721        let cb = ctx
722            .link()
723            .callback(move |_: ()| UpdateSession(Box::new(session.to_props())));
724
725        let s = &ctx.props().session;
726        let sub1 = s.table_loaded.add_notify_listener(&cb);
727        let sub2 = s.table_unloaded.add_notify_listener(&cb);
728        let sub3 = s.view_created.add_notify_listener(&cb);
729        let sub4 = s.view_config_changed.add_notify_listener(&cb);
730        let sub5 = s.title_changed.add_notify_listener(&cb);
731        let sub6 = s
732            .view_config_changed
733            .add_listener(ctx.link().callback(|_| IncrementUpdateCount));
734
735        let sub7 = s
736            .view_created
737            .add_listener(ctx.link().callback(|_| DecrementUpdateCount));
738
739        vec![sub1, sub2, sub3, sub4, sub5, sub6, sub7]
740    };
741
742    let renderer_props_sub = {
743        let renderer = ctx.props().renderer.clone();
744        let cb_plugin = ctx.link().callback({
745            move |_: JsPerspectiveViewerPlugin| UpdateRenderer(Box::new(renderer.to_props(None)))
746        });
747
748        let sub1 = ctx.props().renderer.plugin_changed.add_listener(cb_plugin);
749        vec![sub1]
750    };
751
752    let presentation_props_sub = {
753        let presentation = ctx.props().presentation.clone();
754        let cb_settings = ctx.link().callback(UpdateSettingsOpen);
755        let cb_theme = {
756            let pres = presentation.clone();
757            ctx.link()
758                .callback(move |(themes, _): (PtrEqRc<Vec<String>>, _)| {
759                    UpdatePresentation(Box::new(pres.to_props(themes)))
760                })
761        };
762
763        let cb_column_settings = {
764            let pres = presentation.clone();
765            ctx.link().callback(move |_: (bool, Option<String>)| {
766                UpdateColumnSettings(Box::new(pres.get_open_column_settings()))
767            })
768        };
769
770        let sub1 = presentation.settings_open_changed.add_listener(cb_settings);
771        let sub2 = presentation.theme_config_updated.add_listener(cb_theme);
772        let sub3 = presentation
773            .column_settings_open_changed
774            .add_listener(cb_column_settings);
775
776        vec![sub1, sub2, sub3]
777    };
778
779    let dragdrop_props_sub = {
780        let cb_clear = ctx.link().callback(|_: ()| UpdateDragDrop(Box::default()));
781        let sub1 = ctx
782            .props()
783            .dragdrop
784            .drop_received
785            .add_notify_listener(&cb_clear);
786
787        vec![sub1]
788    };
789
790    let mut subscriptions = Vec::new();
791    subscriptions.extend(session_props_sub);
792    subscriptions.extend(renderer_props_sub);
793    subscriptions.extend(presentation_props_sub);
794    subscriptions.extend(dragdrop_props_sub);
795    subscriptions
796}
797
798/// Inject direct callbacks into the engine handles, replacing PubSub fields
799/// that were exclusively consumed by the root component.
800fn inject_engine_callbacks(ctx: &Context<PerspectiveViewer>) {
801    // Session: on_stats_changed
802    {
803        let session = ctx.props().session.clone();
804        let cb = ctx.link().callback(move |_: ()| {
805            UpdateSessionStats(session.get_table_stats(), session.has_table())
806        });
807
808        *ctx.props().session.on_stats_changed.borrow_mut() = Some(cb);
809    }
810
811    // Session: on_table_errored
812    {
813        let session = ctx.props().session.clone();
814        let cb = ctx
815            .link()
816            .callback(move |_: ()| UpdateSession(Box::new(session.to_props())));
817
818        *ctx.props().session.on_table_errored.borrow_mut() = Some(cb);
819    }
820
821    // Renderer: on_render_limits_changed (combines UpdateRenderer + column
822    // locator recheck that were previously two separate PubSub subscriptions).
823    {
824        clone!(
825            ctx.props().presentation,
826            ctx.props().renderer,
827            ctx.props().session
828        );
829
830        let cb = ctx.link().batch_callback(move |limits: RenderLimits| {
831            let mut msgs = vec![UpdateRenderer(Box::new(renderer.to_props(Some(limits))))];
832            if !limits.is_update {
833                let locator = get_current_column_locator(
834                    &presentation.get_open_column_settings(),
835                    &renderer,
836                    &session.get_view_config(),
837                    &session.metadata(),
838                );
839
840                msgs.push(OpenColumnSettings {
841                    locator,
842                    sender: None,
843                    toggle: false,
844                });
845            }
846
847            msgs
848        });
849
850        *ctx.props().renderer.on_render_limits_changed.borrow_mut() = Some(cb);
851    }
852
853    // Presentation: on_is_workspace_changed
854    {
855        let cb = ctx.link().callback(UpdateIsWorkspace);
856        *ctx.props()
857            .presentation
858            .on_is_workspace_changed
859            .borrow_mut() = Some(cb);
860    }
861
862    // DragDrop: on_dragstart
863    {
864        let dragdrop = ctx.props().dragdrop.clone();
865        let cb = ctx
866            .link()
867            .callback(move |_: DragEffect| UpdateDragDrop(Box::new(dragdrop.to_props())));
868
869        *ctx.props().dragdrop.on_dragstart.borrow_mut() = Some(cb);
870    }
871
872    // DragDrop: on_dragend
873    {
874        let cb = ctx.link().callback(|_: ()| UpdateDragDrop(Box::default()));
875        *ctx.props().dragdrop.on_dragend.borrow_mut() = Some(cb);
876    }
877}