1use 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::style::{LocalStyle, StyleProvider};
23use crate::components::column_settings_sidebar::ColumnSettingsPanel;
24use crate::components::main_panel::MainPanel;
25use crate::components::settings_panel::{SelectedTab, SettingsPanel};
26use crate::config::*;
27use crate::css;
28use crate::js::JsPerspectiveViewerPlugin;
29use crate::presentation::{
30 ColumnLocator, ColumnSettingsTab, DragDropProps, Presentation, PresentationProps,
31};
32use crate::queries::*;
33use crate::renderer::{RendererProps, *};
34use crate::session::{SessionProps, *};
35use crate::tasks::*;
36use crate::utils::*;
37
38#[derive(Clone, Properties)]
39pub struct PerspectiveViewerProps {
40 pub elem: web_sys::HtmlElement,
42
43 pub session: Session,
45 pub renderer: Renderer,
46 pub presentation: Presentation,
47}
48
49impl PartialEq for PerspectiveViewerProps {
50 fn eq(&self, _rhs: &Self) -> bool {
51 false
52 }
53}
54
55#[derive(Debug)]
56pub enum PerspectiveViewerMsg {
57 ColumnSettingsPanelSizeUpdate(Option<i32>),
58 ColumnSettingsTabChanged(ColumnSettingsTab),
59 OpenColumnSettings {
60 locator: Option<ColumnLocator>,
61 sender: Option<Sender<()>>,
62 toggle: bool,
63 },
64 PreloadFontsUpdate,
65 Reset(bool, Option<Sender<()>>),
66 Resize,
67 SettingsPanelSizeUpdate(Option<i32>),
68 SettingsPanelTabChanged(SelectedTab),
69 SettingsPanelAutoWidth(f64),
70 ToggleDebug,
71 ToggleSettingsComplete(SettingsUpdate, Sender<()>),
72 ToggleSettingsInit(Option<SettingsUpdate>, Option<Sender<ApiResult<JsValue>>>),
73 UpdateSession(Box<SessionProps>),
74 UpdateRenderer(Box<RendererProps>),
75 UpdatePresentation(Box<PresentationProps>),
76
77 UpdateSettingsOpen(bool),
80 UpdateIsWorkspace(bool),
81
82 UpdateColumnSettings(Box<crate::presentation::OpenColumnSettings>),
84 UpdateDragDrop(Box<DragDropProps>),
85
86 UpdateSessionStats(Option<ViewStats>, Option<TableLoadState>),
90
91 IncrementUpdateCount,
94 DecrementUpdateCount,
95}
96
97use PerspectiveViewerMsg::*;
98
99pub struct PerspectiveViewer {
100 _subscriptions: Vec<Subscription>,
101 column_settings_panel_width_override: Option<i32>,
102 debug_open: bool,
103 fonts: FontLoaderProps,
104 on_close_column_settings: Callback<()>,
105 on_rendered: Option<Sender<()>>,
106 on_resize: Rc<PubSub<()>>,
107 on_settings_panel_dimensions_reset: Rc<PubSub<()>>,
108 settings_open: bool,
109 settings_panel_width_override: Option<i32>,
110 settings_panel_selected_tab: SelectedTab,
111 settings_panel_auto_width: f64,
112
113 session_props: SessionProps,
117 renderer_props: RendererProps,
118 presentation_props: PresentationProps,
119 dragdrop_props: DragDropProps,
120
121 update_count: u32,
124}
125
126impl Component for PerspectiveViewer {
127 type Message = PerspectiveViewerMsg;
128 type Properties = PerspectiveViewerProps;
129
130 fn create(ctx: &Context<Self>) -> Self {
131 let elem = ctx.props().elem.clone();
132 let fonts = FontLoaderProps::new(&elem, ctx.link().callback(|()| PreloadFontsUpdate));
133 inject_engine_callbacks(ctx);
134 let subscriptions = create_subscriptions(ctx);
135 let session_props = ctx.props().session.to_props();
136 let renderer_props = ctx.props().renderer.to_props(None);
137 let presentation_props = ctx.props().presentation.to_props(PtrEqRc::new(vec![]));
138
139 let on_close_column_settings = ctx.link().callback(|_| OpenColumnSettings {
141 locator: None,
142 sender: None,
143 toggle: false,
144 });
145
146 {
150 let presentation = ctx.props().presentation.clone();
151 let cb = ctx.link().callback(move |themes: PtrEqRc<Vec<String>>| {
152 UpdatePresentation(Box::new(presentation.to_props(themes)))
153 });
154
155 let presentation = ctx.props().presentation.clone();
156 ApiFuture::spawn(async move {
157 let themes = presentation.get_available_themes().await?;
158 cb.emit(themes);
159 Ok(())
160 });
161 }
162
163 Self {
164 _subscriptions: subscriptions,
165 column_settings_panel_width_override: None,
166 debug_open: false,
167 fonts,
168 on_close_column_settings,
169 on_rendered: None,
170 on_resize: Default::default(),
171 on_settings_panel_dimensions_reset: Default::default(),
172 settings_open: false,
173 settings_panel_width_override: None,
174 settings_panel_selected_tab: SelectedTab::default(),
175 settings_panel_auto_width: 0.0,
176 session_props,
177 renderer_props,
178 presentation_props,
179 dragdrop_props: DragDropProps::default(),
180 update_count: 0,
181 }
182 }
183
184 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
185 match msg {
186 PreloadFontsUpdate => true,
187 Resize => {
188 self.on_resize.emit(());
189 false
190 },
191 Reset(all, sender) => {
192 reset_all(
193 &ctx.props().session,
194 &ctx.props().renderer,
195 &ctx.props().presentation,
196 all,
197 sender,
198 );
199 false
200 },
201 ToggleSettingsInit(Some(SettingsUpdate::Missing), None) => false,
202 ToggleSettingsInit(Some(SettingsUpdate::Missing), Some(resolve)) => {
203 resolve.send(Ok(JsValue::UNDEFINED)).unwrap();
204 false
205 },
206 ToggleSettingsInit(Some(SettingsUpdate::SetDefault), resolve) => {
207 self.init_toggle_settings_task(ctx, Some(false), resolve);
208 false
209 },
210 ToggleSettingsInit(Some(SettingsUpdate::Update(force)), resolve) => {
211 self.init_toggle_settings_task(ctx, Some(force), resolve);
212 false
213 },
214 ToggleSettingsInit(None, resolve) => {
215 self.init_toggle_settings_task(ctx, None, resolve);
216 false
217 },
218 ToggleSettingsComplete(SettingsUpdate::SetDefault, resolve) if self.settings_open => {
219 ctx.props().presentation.set_open_column_settings(None);
220 self.settings_open = false;
221 self.on_rendered = Some(resolve);
222 true
223 },
224 ToggleSettingsComplete(SettingsUpdate::Update(force), resolve)
225 if force != self.settings_open =>
226 {
227 ctx.props().presentation.set_open_column_settings(None);
228 self.settings_open = force;
229 self.on_rendered = Some(resolve);
230 true
231 },
232 ToggleSettingsComplete(_, resolve)
233 if matches!(self.fonts.get_status(), FontLoaderStatus::Finished) =>
234 {
235 if let Err(e) = resolve.send(()) {
236 tracing::error!("toggle settings failed {:?}", e);
237 }
238
239 false
240 },
241 ToggleSettingsComplete(_, resolve) => {
242 ctx.props().presentation.set_open_column_settings(None);
243 self.on_rendered = Some(resolve);
244 true
245 },
246 OpenColumnSettings {
247 locator,
248 sender,
249 toggle,
250 } => {
251 let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
252 if locator == open_column_settings.locator {
253 if toggle {
254 ctx.props().presentation.set_open_column_settings(None);
255 }
256 } else {
257 open_column_settings.locator.clone_from(&locator);
258 open_column_settings.tab =
259 if matches!(locator, Some(ColumnLocator::NewExpression)) {
260 Some(ColumnSettingsTab::Attributes)
261 } else {
262 locator.as_ref().and_then(|x| {
263 x.name().map(|x| {
264 if self.session_props.is_column_active(x) {
265 ColumnSettingsTab::Style
266 } else {
267 ColumnSettingsTab::Attributes
268 }
269 })
270 })
271 };
272
273 ctx.props()
274 .presentation
275 .set_open_column_settings(Some(open_column_settings));
276
277 if locator.is_some() {
278 self.settings_panel_selected_tab = SelectedTab::Query;
279 }
280 }
281
282 if let Some(sender) = sender {
283 sender.send(()).unwrap();
284 }
285
286 true
287 },
288 SettingsPanelSizeUpdate(Some(x)) => {
289 self.settings_panel_width_override = Some(x);
290 false
291 },
292 SettingsPanelSizeUpdate(None) => {
293 self.settings_panel_width_override = None;
294 self.settings_panel_auto_width = 0.0;
295 self.on_settings_panel_dimensions_reset.emit(());
296 true
297 },
298 SettingsPanelTabChanged(tab) => {
299 let changed = tab != self.settings_panel_selected_tab;
300 self.settings_panel_selected_tab = tab;
301 changed
302 },
303 SettingsPanelAutoWidth(w) => {
304 if w > self.settings_panel_auto_width {
305 self.settings_panel_auto_width = w;
306 true
307 } else {
308 false
309 }
310 },
311 ColumnSettingsPanelSizeUpdate(Some(x)) => {
312 self.column_settings_panel_width_override = Some(x);
313 false
314 },
315 ColumnSettingsPanelSizeUpdate(None) => {
316 self.column_settings_panel_width_override = None;
317 false
318 },
319 ColumnSettingsTabChanged(tab) => {
320 let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
321 open_column_settings.tab.clone_from(&Some(tab));
322 ctx.props()
323 .presentation
324 .set_open_column_settings(Some(open_column_settings));
325 true
326 },
327 ToggleDebug => {
328 self.debug_open = !self.debug_open;
329 clone!(ctx.props().renderer, ctx.props().session);
330 ApiFuture::spawn(async move {
331 renderer.draw(session.validate().await?.create_view()).await
332 });
333
334 true
335 },
336 UpdateSession(props) => {
337 let changed = *props != self.session_props;
338 self.session_props = *props;
339 changed
340 },
341 UpdateSessionStats(stats, has_table) => {
342 let changed =
343 stats != self.session_props.stats || has_table != self.session_props.has_table;
344 self.session_props.stats = stats;
345 self.session_props.has_table = has_table;
346 changed
347 },
348 UpdateRenderer(props) => {
349 let changed = *props != self.renderer_props;
350 self.renderer_props = *props;
351 changed
352 },
353 UpdatePresentation(props) => {
354 let changed = *props != self.presentation_props;
355 self.presentation_props = *props;
356 changed
357 },
358 UpdateSettingsOpen(open) => {
359 let changed = open != self.presentation_props.is_settings_open;
360 self.presentation_props.is_settings_open = open;
361 changed
362 },
363 UpdateIsWorkspace(is_workspace) => {
364 let changed = is_workspace != self.presentation_props.is_workspace;
365 self.presentation_props.is_workspace = is_workspace;
366 changed
367 },
368 UpdateColumnSettings(ocs) => {
369 let changed = *ocs != self.presentation_props.open_column_settings;
370 self.presentation_props.open_column_settings = *ocs;
371 changed
372 },
373 UpdateDragDrop(props) => {
374 let changed = *props != self.dragdrop_props;
375 self.dragdrop_props = *props;
376 changed
377 },
378 IncrementUpdateCount => {
379 self.update_count = self.update_count.saturating_add(1);
380 true
381 },
382 DecrementUpdateCount => {
383 self.update_count = self.update_count.saturating_sub(1);
384 true
385 },
386 }
387 }
388
389 fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
393 true
394 }
395
396 fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
399 if self.on_rendered.is_some()
400 && matches!(self.fonts.get_status(), FontLoaderStatus::Finished)
401 && self.on_rendered.take().unwrap().send(()).is_err()
402 {
403 tracing::warn!("Orphan render");
404 }
405 }
406
407 fn view(&self, ctx: &Context<Self>) -> Html {
408 let Self::Properties {
409 presentation,
410 renderer,
411 session,
412 ..
413 } = ctx.props();
414
415 let is_settings_open = self.settings_open
416 && matches!(self.session_props.has_table, Some(TableLoadState::Loaded));
417
418 let mut class = classes!();
419 if !is_settings_open {
420 class.push("settings-closed");
421 }
422
423 if self.session_props.title.is_some() {
424 class.push("titled");
425 }
426
427 let on_open_expr_panel = ctx.link().callback(|c| OpenColumnSettings {
428 locator: c,
429 sender: None,
430 toggle: true,
431 });
432
433 let on_split_panel_resize = ctx
434 .link()
435 .callback(|(x, _)| SettingsPanelSizeUpdate(Some(x)));
436
437 let on_column_settings_panel_resize = ctx
438 .link()
439 .callback(|(x, _)| ColumnSettingsPanelSizeUpdate(Some(x)));
440
441 let on_close_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
442 let on_debug = ctx.link().callback(|_| ToggleDebug);
443 let selected_column = get_current_column_locator(
444 &self.presentation_props.open_column_settings,
445 &ctx.props().renderer,
446 &self.session_props.config,
447 &self.session_props.metadata,
448 );
449
450 let selected_tab = self.presentation_props.open_column_settings.tab;
451 let plugin_name = self.renderer_props.plugin_name.clone();
452 let available_plugins = self.renderer_props.available_plugins.clone();
453 let has_table = self.session_props.has_table.clone();
454 let named_column_count = self.renderer_props.config.config_column_names.len();
455
456 let view_config = self.session_props.config.clone();
457 let drag_column = self.dragdrop_props.column.clone();
458 let metadata = self.session_props.metadata.clone();
459 let on_select_tab = ctx.link().callback(SettingsPanelTabChanged);
460 let on_auto_width = ctx.link().callback(SettingsPanelAutoWidth);
461 let settings_panel = html! {
462 if is_settings_open {
463 <SettingsPanel
464 on_close={on_close_settings}
465 on_resize={&self.on_resize}
466 on_select_column={on_open_expr_panel}
467 is_debug={self.debug_open}
468 {on_debug}
469 {plugin_name}
470 {available_plugins}
471 {has_table}
472 {named_column_count}
473 {view_config}
474 plugin_config={self.renderer_props.plugin_config.clone()}
475 {drag_column}
476 metadata={metadata.clone()}
477 open_column_settings={self.presentation_props.open_column_settings.clone()}
478 selected_theme={self.presentation_props.selected_theme.clone()}
479 selected_tab={self.settings_panel_selected_tab}
480 auto_width={self.settings_panel_auto_width}
481 on_dimensions_reset={&self.on_settings_panel_dimensions_reset}
482 {on_select_tab}
483 {on_auto_width}
484 {presentation}
485 {renderer}
486 {session}
487 />
488 }
489 };
490
491 let on_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
492 let on_select_tab = ctx.link().callback(ColumnSettingsTabChanged);
493 let column_settings_panel = html! {
494 if let Some(selected_column) = selected_column {
495 <SplitPanel
496 id="modal_panel"
497 reverse=true
498 initial_size={self.column_settings_panel_width_override}
499 on_reset={ctx.link().callback(|_| ColumnSettingsPanelSizeUpdate(None))}
500 on_resize={on_column_settings_panel_resize}
501 >
502 <ColumnSettingsPanel
503 {selected_column}
504 {selected_tab}
505 on_close={self.on_close_column_settings.clone()}
506 width_override={self.column_settings_panel_width_override}
507 {on_select_tab}
508 plugin_name={self.renderer_props.plugin_name.clone()}
509 {metadata}
510 view_config={self.session_props.config.clone()}
511 column_stats={self.session_props.column_stats.clone()}
512 selected_theme={self.presentation_props.selected_theme.clone()}
513 {presentation}
514 {renderer}
515 {session}
516 />
517 <></>
518 </SplitPanel>
519 }
520 };
521
522 let on_reset = ctx.link().callback(|all| Reset(all, None));
523 let is_settings_open = self.settings_open
524 && matches!(self.session_props.has_table, Some(TableLoadState::Loaded));
525 let main_panel = html! {
526 <MainPanel
527 {on_settings}
528 {on_reset}
529 session_props={self.session_props.clone()}
530 renderer_props={self.renderer_props.clone()}
531 presentation_props={self.presentation_props.clone()}
532 {is_settings_open}
533 update_count={self.update_count}
534 {presentation}
535 {renderer}
536 {session}
537 />
538 };
539
540 html! {
541 <StyleProvider root={ctx.props().elem.clone()}>
542 <LocalStyle href={css!("viewer")} />
543 <div id="component_container">
544 if is_settings_open {
545 <SplitPanel
546 id="app_panel"
547 reverse=true
548 skip_empty=true
549 initial_size={self.settings_panel_width_override}
550 on_reset={ctx.link().callback(|_| SettingsPanelSizeUpdate(None))}
551 on_resize={on_split_panel_resize.clone()}
552 on_resize_finished={render_callback(&ctx.props().session, &ctx.props().renderer)}
553 >
554 { settings_panel }
555 <div id="main_column_container">
556 { main_panel }
557 { column_settings_panel }
558 </div>
559 </SplitPanel>
560 } else {
561 <div id="main_column_container">
562 { main_panel }
563 { column_settings_panel }
564 </div>
565 }
566 </div>
567 <FontLoader ..self.fonts.clone() />
568 </StyleProvider>
569 }
570 }
571
572 fn destroy(&mut self, _ctx: &Context<Self>) {}
573}
574
575impl PerspectiveViewer {
576 fn init_toggle_settings_task(
590 &mut self,
591 ctx: &Context<Self>,
592 force: Option<bool>,
593 sender: Option<Sender<ApiResult<JsValue>>>,
594 ) {
595 let is_open = ctx.props().presentation.is_settings_open();
596 ctx.props().presentation.set_settings_before_open(!is_open);
597 match force {
598 Some(force) if is_open == force => {
599 if let Some(sender) = sender {
600 sender.send(Ok(JsValue::UNDEFINED)).unwrap();
601 }
602 },
603 Some(_) | None => {
604 let force = !is_open;
605 let callback = ctx.link().callback(move |resolve| {
606 let update = SettingsUpdate::Update(force);
607 ToggleSettingsComplete(update, resolve)
608 });
609
610 clone!(
611 ctx.props().renderer,
612 ctx.props().session,
613 ctx.props().presentation
614 );
615
616 ApiFuture::spawn(async move {
617 let result = if session.js_get_table().is_some() {
618 renderer
619 .presize(force, {
620 let (sender, receiver) = channel::<()>();
621 async move {
622 callback.emit(sender);
623 presentation.set_settings_open(!is_open);
624 Ok(receiver.await?)
625 }
626 })
627 .await
628 } else {
629 let (sender, receiver) = channel::<()>();
630 callback.emit(sender);
631 presentation.set_settings_open(!is_open);
632 receiver.await?;
633 Ok(JsValue::UNDEFINED)
634 };
635
636 if let Some(sender) = sender {
637 let msg = result.ignore_view_delete();
638 sender
639 .send(msg.map(|x| x.unwrap_or(JsValue::UNDEFINED)))
640 .into_apierror()?;
641 };
642
643 Ok(JsValue::undefined())
644 });
645 },
646 };
647 }
648}
649
650fn create_subscriptions(ctx: &Context<PerspectiveViewer>) -> Vec<Subscription> {
653 let session_props_sub = {
654 let session = ctx.props().session.clone();
655 let cb = ctx
656 .link()
657 .callback(move |_: ()| UpdateSession(Box::new(session.to_props())));
658
659 let s = &ctx.props().session;
660 let sub1 = s.table_loaded.add_notify_listener(&cb);
661 let sub2 = s.table_unloaded.add_notify_listener(&cb);
662 let sub3 = s.view_created.add_notify_listener(&cb);
663 let sub4 = s.view_config_changed.add_notify_listener(&cb);
664 let sub5 = s.title_changed.add_notify_listener(&cb);
665 let sub6 = s
666 .view_config_changed
667 .add_listener(ctx.link().callback(|_| IncrementUpdateCount));
668
669 let sub7 = s
670 .view_created
671 .add_listener(ctx.link().callback(|_| DecrementUpdateCount));
672
673 let sub8 = s.column_stats_changed.add_notify_listener(&cb);
678
679 vec![sub1, sub2, sub3, sub4, sub5, sub6, sub7, sub8]
680 };
681
682 let renderer_props_sub = {
683 let renderer = ctx.props().renderer.clone();
684 let cb_plugin = ctx.link().callback({
685 let renderer = renderer.clone();
686 move |_: JsPerspectiveViewerPlugin| UpdateRenderer(Box::new(renderer.to_props(None)))
687 });
688
689 let cb_plugin_config = ctx.link().callback({
696 let renderer = renderer.clone();
697 move |_: serde_json::Map<String, serde_json::Value>| {
698 UpdateRenderer(Box::new(renderer.to_props(None)))
699 }
700 });
701
702 let sub1 = ctx.props().renderer.plugin_changed.add_listener(cb_plugin);
703 let sub2 = ctx
704 .props()
705 .renderer
706 .plugin_config_changed
707 .add_listener(cb_plugin_config);
708
709 vec![sub1, sub2]
710 };
711
712 let presentation_props_sub = {
713 let presentation = ctx.props().presentation.clone();
714 let cb_settings = ctx.link().callback(UpdateSettingsOpen);
715 let cb_theme = {
716 let pres = presentation.clone();
717 ctx.link()
718 .callback(move |(themes, _): (PtrEqRc<Vec<String>>, _)| {
719 UpdatePresentation(Box::new(pres.to_props(themes)))
720 })
721 };
722
723 let cb_column_settings = {
724 let pres = presentation.clone();
725 ctx.link().callback(move |_: (bool, Option<String>)| {
726 UpdateColumnSettings(Box::new(pres.get_open_column_settings()))
727 })
728 };
729
730 let sub1 = presentation.settings_open_changed.add_listener(cb_settings);
731 let sub2 = presentation.theme_config_updated.add_listener(cb_theme);
732 let sub3 = presentation
733 .column_settings_open_changed
734 .add_listener(cb_column_settings);
735
736 vec![sub1, sub2, sub3]
737 };
738
739 let dragdrop_props_sub = {
740 let cb_clear = ctx.link().callback(|_: ()| UpdateDragDrop(Box::default()));
741 let sub1 = ctx
742 .props()
743 .presentation
744 .drop_received
745 .add_notify_listener(&cb_clear);
746
747 vec![sub1]
748 };
749
750 let mut subscriptions = Vec::new();
751 subscriptions.extend(session_props_sub);
752 subscriptions.extend(renderer_props_sub);
753 subscriptions.extend(presentation_props_sub);
754 subscriptions.extend(dragdrop_props_sub);
755 subscriptions
756}
757
758fn inject_engine_callbacks(ctx: &Context<PerspectiveViewer>) {
761 {
763 let session = ctx.props().session.clone();
764 let cb = ctx.link().callback(move |_: ()| {
765 UpdateSessionStats(session.get_table_stats(), session.has_table())
766 });
767
768 *ctx.props().session.on_stats_changed.borrow_mut() = Some(cb);
769 }
770
771 {
773 let session = ctx.props().session.clone();
774 let cb = ctx
775 .link()
776 .callback(move |_: ()| UpdateSession(Box::new(session.to_props())));
777
778 *ctx.props().session.on_table_errored.borrow_mut() = Some(cb);
779 }
780
781 {
784 clone!(
785 ctx.props().presentation,
786 ctx.props().renderer,
787 ctx.props().session
788 );
789
790 let cb = ctx.link().batch_callback(move |limits: RenderLimits| {
791 let mut msgs = vec![UpdateRenderer(Box::new(renderer.to_props(Some(limits))))];
792 if !limits.is_update {
793 let locator = get_current_column_locator(
794 &presentation.get_open_column_settings(),
795 &renderer,
796 &session.get_view_config(),
797 &session.metadata(),
798 );
799
800 msgs.push(OpenColumnSettings {
801 locator,
802 sender: None,
803 toggle: false,
804 });
805 }
806
807 msgs
808 });
809
810 *ctx.props().renderer.on_render_limits_changed.borrow_mut() = Some(cb);
811 }
812
813 {
815 let cb = ctx.link().callback(UpdateIsWorkspace);
816 *ctx.props()
817 .presentation
818 .on_is_workspace_changed
819 .borrow_mut() = Some(cb);
820 }
821
822 {
824 let presentation = ctx.props().presentation.clone();
825 let cb = ctx.link().callback(move |_: DragEffect| {
826 UpdateDragDrop(Box::new(presentation.drag_drop_props()))
827 });
828
829 *ctx.props().presentation.on_dragstart.borrow_mut() = Some(cb);
830 }
831
832 {
834 let cb = ctx.link().callback(|_: ()| UpdateDragDrop(Box::default()));
835 *ctx.props().presentation.on_dragend.borrow_mut() = Some(cb);
836 }
837}