1use std::rc::Rc;
14
15use futures::channel::oneshot::*;
16use perspective_client::ColumnType;
17use wasm_bindgen::prelude::*;
18use yew::prelude::*;
19
20use super::column_selector::ColumnSelector;
21use super::containers::split_panel::SplitPanel;
22use super::font_loader::{FontLoader, FontLoaderProps, FontLoaderStatus};
23use super::form::debug::DebugPanel;
24use super::plugin_selector::PluginSelector;
25use super::render_warning::RenderWarning;
26use super::status_bar::StatusBar;
27use super::style::{LocalStyle, StyleProvider};
28use crate::components::column_settings_sidebar::ColumnSettingsSidebar;
29use crate::components::containers::sidebar::SidebarCloseButton;
30use crate::config::*;
31use crate::custom_events::CustomEvents;
32use crate::dragdrop::*;
33use crate::model::*;
34use crate::presentation::Presentation;
35use crate::renderer::*;
36use crate::session::*;
37use crate::utils::*;
38use crate::*;
39
40#[derive(Clone, Debug, PartialEq)]
44pub enum ColumnLocator {
45 Table(String),
46 Expression(String),
47 NewExpression,
48}
49impl ColumnLocator {
50 pub fn name(&self) -> Option<&String> {
54 match self {
55 Self::Table(s) | Self::Expression(s) => Some(s),
56 Self::NewExpression => None,
57 }
58 }
59
60 pub fn name_or_default(&self, session: &Session) -> String {
61 match self {
62 Self::Table(s) | Self::Expression(s) => s.clone(),
63 Self::NewExpression => session.metadata().make_new_column_name(None),
64 }
65 }
66
67 pub fn is_active(&self, session: &Session) -> bool {
68 self.name()
69 .map(|name| session.is_column_active(name))
70 .unwrap_or_default()
71 }
72
73 #[inline(always)]
74 pub fn is_saved_expr(&self) -> bool {
75 matches!(self, ColumnLocator::Expression(_))
76 }
77
78 #[inline(always)]
79 pub fn is_expr(&self) -> bool {
80 matches!(
81 self,
82 ColumnLocator::Expression(_) | ColumnLocator::NewExpression
83 )
84 }
85
86 #[inline(always)]
87 pub fn is_new_expr(&self) -> bool {
88 matches!(self, ColumnLocator::NewExpression)
89 }
90
91 pub fn view_type(&self, session: &Session) -> Option<ColumnType> {
92 let name = self.name().cloned().unwrap_or_default();
93 session.metadata().get_column_view_type(name.as_str())
94 }
95}
96
97#[derive(Properties)]
98pub struct PerspectiveViewerProps {
99 pub elem: web_sys::HtmlElement,
100 pub session: Session,
101 pub renderer: Renderer,
102 pub presentation: Presentation,
103 pub dragdrop: DragDrop,
104 pub custom_events: CustomEvents,
105
106 #[prop_or_default]
107 pub weak_link: WeakScope<PerspectiveViewer>,
108}
109
110derive_model!(Renderer, Session, Presentation for PerspectiveViewerProps);
111
112impl PartialEq for PerspectiveViewerProps {
113 fn eq(&self, _rhs: &Self) -> bool {
114 false
115 }
116}
117
118impl PerspectiveViewerProps {
119 fn is_title(&self) -> bool {
120 !self.presentation.get_is_workspace() && self.presentation.get_title().is_some()
121 }
122}
123
124#[derive(Debug)]
125pub enum PerspectiveViewerMsg {
126 Resize,
127 Reset(bool, Option<Sender<()>>),
128 ToggleSettingsInit(Option<SettingsUpdate>, Option<Sender<ApiResult<JsValue>>>),
129 ToggleSettingsComplete(SettingsUpdate, Sender<()>),
130 ToggleDebug,
131 PreloadFontsUpdate,
132 RenderLimits(Option<(usize, usize, Option<usize>, Option<usize>)>),
133 SettingsPanelSizeUpdate(Option<i32>),
134 ColumnSettingsPanelSizeUpdate(Option<i32>),
135 Error,
136 OpenColumnSettings {
137 locator: Option<ColumnLocator>,
138 sender: Option<Sender<()>>,
139 toggle: bool,
140 },
141}
142
143pub struct PerspectiveViewer {
144 dimensions: Option<(usize, usize, Option<usize>, Option<usize>)>,
145 on_rendered: Option<Sender<()>>,
146 fonts: FontLoaderProps,
147 settings_open: bool,
148 debug_open: bool,
149 selected_column: Option<ColumnLocator>,
151 selected_column_is_active: bool, on_resize: Rc<PubSub<()>>,
153 on_dimensions_reset: Rc<PubSub<()>>,
154 _subscriptions: [Subscription; 2],
155 settings_panel_width_override: Option<i32>,
156 column_settings_panel_width_override: Option<i32>,
157
158 on_close_column_settings: Callback<()>,
159}
160
161impl Component for PerspectiveViewer {
162 type Message = PerspectiveViewerMsg;
163 type Properties = PerspectiveViewerProps;
164
165 fn create(ctx: &Context<Self>) -> Self {
166 *ctx.props().weak_link.borrow_mut() = Some(ctx.link().clone());
167 let elem = ctx.props().elem.clone();
168 let callback = ctx
169 .link()
170 .callback(|()| PerspectiveViewerMsg::PreloadFontsUpdate);
171
172 let session_sub = {
173 clone!(
174 ctx.props().presentation,
175 ctx.props().session,
176 plugin_query = ctx.props().get_plugin_column_styles_query()
177 );
178 let callback = ctx.link().batch_callback(move |(update, render_limits)| {
179 if update {
180 vec![PerspectiveViewerMsg::RenderLimits(Some(render_limits))]
181 } else {
182 let locator =
183 presentation
184 .get_open_column_settings()
185 .locator
186 .filter(|locator| match &locator {
187 ColumnLocator::Table(name) => {
188 locator.is_active(&session)
189 && plugin_query
190 .can_render_column_styles(name)
191 .unwrap_or_default()
192 },
193 _ => true,
194 });
195
196 vec![
197 PerspectiveViewerMsg::RenderLimits(Some(render_limits)),
198 PerspectiveViewerMsg::OpenColumnSettings {
199 locator,
200 sender: None,
201 toggle: false,
202 },
203 ]
204 }
205 });
206 ctx.props()
207 .renderer
208 .render_limits_changed
209 .add_listener(callback)
210 };
211
212 let error_sub = ctx
213 .props()
214 .session
215 .table_errored
216 .add_listener(ctx.link().callback(|_| PerspectiveViewerMsg::Error));
217
218 let on_close_column_settings =
219 ctx.link()
220 .callback(|_| PerspectiveViewerMsg::OpenColumnSettings {
221 locator: None,
222 sender: None,
223 toggle: false,
224 });
225
226 Self {
227 dimensions: None,
228 on_rendered: None,
229 fonts: FontLoaderProps::new(&elem, callback),
230 settings_open: false,
231 debug_open: false,
232 selected_column: None,
233 selected_column_is_active: false,
234 on_resize: Default::default(),
235 on_dimensions_reset: Default::default(),
236 _subscriptions: [session_sub, error_sub],
237 settings_panel_width_override: None,
238 column_settings_panel_width_override: None,
239 on_close_column_settings,
240 }
241 }
242
243 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
244 let needs_update = self.selected_column.is_some();
245 match msg {
246 PerspectiveViewerMsg::PreloadFontsUpdate => true,
247 PerspectiveViewerMsg::Resize => {
248 self.on_resize.emit(());
249 false
250 },
251 PerspectiveViewerMsg::Error => true,
252 PerspectiveViewerMsg::Reset(all, sender) => {
253 self.selected_column = None;
254 clone!(
255 ctx.props().renderer,
256 ctx.props().session,
257 ctx.props().presentation
258 );
259
260 ApiFuture::spawn(async move {
261 session.reset(all).await?;
262 let columns_config = if all {
263 presentation.reset_columns_configs();
264 None
265 } else {
266 Some(presentation.all_columns_configs())
267 };
268
269 renderer.reset(columns_config.as_ref()).await?;
270 presentation.reset_available_themes(None).await;
271 if all {
272 presentation.reset_theme().await?;
273 }
274
275 let result = renderer.draw(session.validate().await?.create_view()).await;
276 if let Some(sender) = sender {
277 sender.send(()).unwrap();
278 }
279
280 renderer.reset_changed.emit(());
281 result
282 });
283
284 needs_update
285 },
286 PerspectiveViewerMsg::ToggleDebug => {
287 self.debug_open = !self.debug_open;
288 clone!(ctx.props().renderer, ctx.props().session);
289 ApiFuture::spawn(async move {
290 renderer.draw(session.validate().await?.create_view()).await
291 });
292
293 true
294 },
295 PerspectiveViewerMsg::ToggleSettingsInit(Some(SettingsUpdate::Missing), None) => false,
296 PerspectiveViewerMsg::ToggleSettingsInit(
297 Some(SettingsUpdate::Missing),
298 Some(resolve),
299 ) => {
300 resolve.send(Ok(JsValue::UNDEFINED)).unwrap();
301 false
302 },
303 PerspectiveViewerMsg::ToggleSettingsInit(Some(SettingsUpdate::SetDefault), resolve) => {
304 self.init_toggle_settings_task(ctx, Some(false), resolve);
305 false
306 },
307 PerspectiveViewerMsg::ToggleSettingsInit(
308 Some(SettingsUpdate::Update(force)),
309 resolve,
310 ) => {
311 self.init_toggle_settings_task(ctx, Some(force), resolve);
312 false
313 },
314 PerspectiveViewerMsg::ToggleSettingsInit(None, resolve) => {
315 self.init_toggle_settings_task(ctx, None, resolve);
316 false
317 },
318 PerspectiveViewerMsg::ToggleSettingsComplete(SettingsUpdate::SetDefault, resolve)
319 if self.settings_open =>
320 {
321 self.selected_column = None;
322 self.settings_open = false;
323 self.on_rendered = Some(resolve);
324 true
325 },
326 PerspectiveViewerMsg::ToggleSettingsComplete(
327 SettingsUpdate::Update(force),
328 resolve,
329 ) if force != self.settings_open => {
330 self.selected_column = None;
331 self.settings_open = force;
332 self.on_rendered = Some(resolve);
333 true
334 },
335 PerspectiveViewerMsg::ToggleSettingsComplete(_, resolve)
336 if matches!(self.fonts.get_status(), FontLoaderStatus::Finished) =>
337 {
338 self.selected_column = None;
339 if let Err(e) = resolve.send(()) {
340 tracing::error!("toggle settings failed {:?}", e);
341 }
342
343 false
344 },
345 PerspectiveViewerMsg::ToggleSettingsComplete(_, resolve) => {
346 self.selected_column = None;
347 self.on_rendered = Some(resolve);
348 true
349 },
350 PerspectiveViewerMsg::RenderLimits(dimensions) => {
351 if self.dimensions != dimensions {
352 self.dimensions = dimensions;
353 true
354 } else {
355 false
356 }
357 },
358 PerspectiveViewerMsg::OpenColumnSettings {
359 locator,
360 sender,
361 toggle,
362 } => {
363 let is_active = locator
364 .as_ref()
365 .map(|l| l.is_active(&ctx.props().session))
366 .unwrap_or_default();
367
368 self.selected_column_is_active = is_active;
369 if toggle && self.selected_column == locator {
370 self.selected_column = None;
371 (false, None)
372 } else {
373 self.selected_column.clone_from(&locator);
374
375 locator
376 .clone()
377 .map(|c| (true, c.name().cloned()))
378 .unwrap_or_default()
379 };
380
381 let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
382 open_column_settings
383 .locator
384 .clone_from(&self.selected_column);
385
386 ctx.props()
387 .presentation
388 .set_open_column_settings(Some(open_column_settings));
389
390 if let Some(sender) = sender {
391 sender.send(()).unwrap();
392 }
393
394 true
395 },
396 PerspectiveViewerMsg::SettingsPanelSizeUpdate(Some(x)) => {
397 self.settings_panel_width_override = Some(x);
398 false
399 },
400 PerspectiveViewerMsg::SettingsPanelSizeUpdate(None) => {
401 self.settings_panel_width_override = None;
402 false
403 },
404 PerspectiveViewerMsg::ColumnSettingsPanelSizeUpdate(Some(x)) => {
405 self.column_settings_panel_width_override = Some(x);
406 false
407 },
408 PerspectiveViewerMsg::ColumnSettingsPanelSizeUpdate(None) => {
409 self.column_settings_panel_width_override = None;
410 false
411 },
412 }
413 }
414
415 fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
419 true
420 }
421
422 fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
425 ctx.props()
426 .presentation
427 .set_settings_open(Some(self.settings_open))
428 .unwrap();
429
430 if self.on_rendered.is_some()
431 && matches!(self.fonts.get_status(), FontLoaderStatus::Finished)
432 && self.on_rendered.take().unwrap().send(()).is_err()
433 {
434 tracing::warn!("Orphan render");
435 }
436 }
437
438 fn view(&self, ctx: &Context<Self>) -> Html {
440 let settings = ctx
441 .link()
442 .callback(|_| PerspectiveViewerMsg::ToggleSettingsInit(None, None));
443
444 let on_close_settings = ctx
445 .link()
446 .callback(|()| PerspectiveViewerMsg::ToggleSettingsInit(None, None));
447
448 let on_toggle_debug = ctx.link().callback(|_| PerspectiveViewerMsg::ToggleDebug);
449 let mut class = classes!("settings-closed");
450 if ctx.props().is_title() {
451 class.push("titled");
452 }
453
454 let on_open_expr_panel =
455 ctx.link()
456 .callback(|c| PerspectiveViewerMsg::OpenColumnSettings {
457 locator: Some(c),
458 sender: None,
459 toggle: true,
460 });
461
462 let on_reset = ctx
463 .link()
464 .callback(|all| PerspectiveViewerMsg::Reset(all, None));
465
466 let on_split_panel_resize = ctx
467 .link()
468 .callback(|(x, _)| PerspectiveViewerMsg::SettingsPanelSizeUpdate(Some(x)));
469
470 let on_column_settings_panel_resize = ctx
471 .link()
472 .callback(|(x, _)| PerspectiveViewerMsg::ColumnSettingsPanelSizeUpdate(Some(x)));
473
474 let settings_panel = html! {
475 <div id="settings_panel" class="sidebar_column noselect split-panel orient-vertical">
476 if self.selected_column.is_none() {
477 <SidebarCloseButton
478 id="settings_close_button"
479 on_close_sidebar={&on_close_settings}
480 />
481 }
482 <SidebarCloseButton
483 id={if self.debug_open { "debug_close_button" } else { "debug_open_button" }}
484 on_close_sidebar={&on_toggle_debug}
485 />
486 <PluginSelector
487 session={&ctx.props().session}
488 renderer={&ctx.props().renderer}
489 presentation={&ctx.props().presentation}
490 />
491 <ColumnSelector
492 dragdrop={&ctx.props().dragdrop}
493 renderer={&ctx.props().renderer}
494 session={&ctx.props().session}
495 presentation={&ctx.props().presentation}
496 on_resize={&self.on_resize}
497 on_open_expr_panel={&on_open_expr_panel}
498 on_dimensions_reset={&self.on_dimensions_reset}
499 selected_column={self.selected_column.clone()}
500 />
501 </div>
502 };
503
504 let main_panel = html! {
505 <div id="main_column">
506 <StatusBar
507 id="status_bar"
508 session={&ctx.props().session}
509 renderer={&ctx.props().renderer}
510 presentation={&ctx.props().presentation}
511 on_reset={on_reset.clone()}
512 />
513 <div id="main_panel_container">
514 <RenderWarning
515 dimensions={self.dimensions}
516 session={&ctx.props().session}
517 renderer={&ctx.props().renderer}
518 />
519 <slot />
520 </div>
521 if let Some(selected_column) = self.selected_column.clone() {
522 <SplitPanel
523 id="modal_panel"
524 reverse=true
525 initial_size={self.column_settings_panel_width_override}
526 on_reset={ctx.link().callback(|_| PerspectiveViewerMsg::ColumnSettingsPanelSizeUpdate(None))}
527 on_resize={on_column_settings_panel_resize}
528 >
529 <ColumnSettingsSidebar
530 session={&ctx.props().session}
531 renderer={&ctx.props().renderer}
532 custom_events={&ctx.props().custom_events}
533 presentation={&ctx.props().presentation}
534 {selected_column}
535 on_close={self.on_close_column_settings.clone()}
536 width_override={self.column_settings_panel_width_override}
537 is_active={self.selected_column_is_active}
538 />
539 <></>
540 </SplitPanel>
541 }
542 </div>
543 };
544
545 html! {
546 <>
547 <StyleProvider root={ctx.props().elem.clone()}>
548 <LocalStyle href={css!("viewer")} />
549 if self.settings_open && ctx.props().session.has_table() {
550 if self.debug_open {
551 <SplitPanel
552 id="app_panel"
553 reverse=true
554 initial_size={self.settings_panel_width_override}
555 on_reset={ctx.link().callback(|_| PerspectiveViewerMsg::SettingsPanelSizeUpdate(None))}
556 on_resize={on_split_panel_resize}
557 on_resize_finished={ctx.props().render_callback()}
558 >
559 <DebugPanel
560 session={ctx.props().session()}
561 renderer={ctx.props().renderer()}
562 presentation={ctx.props().presentation()}
563 />
564 { settings_panel }
565 { main_panel }
566 </SplitPanel>
567 } else {
568 <SplitPanel
569 id="app_panel"
570 reverse=true
571 initial_size={self.settings_panel_width_override}
572 on_reset={ctx.link().callback(|_| PerspectiveViewerMsg::SettingsPanelSizeUpdate(None))}
573 on_resize={on_split_panel_resize}
574 on_resize_finished={ctx.props().resize_callback()}
575 >
576 { settings_panel }
577 { main_panel }
578 </SplitPanel>
579 }
580 } else {
581 <RenderWarning
582 dimensions={self.dimensions}
583 session={&ctx.props().session}
584 renderer={&ctx.props().renderer}
585 />
586 if ctx.props().is_title() || !ctx.props().session.has_table() || ctx.props().session.is_errored() {
587 <StatusBar
588 id="status_bar"
589 session={&ctx.props().session}
590 renderer={&ctx.props().renderer}
591 presentation={&ctx.props().presentation}
592 {on_reset}
593 />
594 }
595 <div id="main_panel_container" {class}><slot /></div>
596 if !ctx.props().presentation.get_is_workspace() {
597 <div
598 id="settings_button"
599 class={if ctx.props().is_title() { "noselect button closed titled" } else { "noselect button closed" }}
600 onmousedown={settings}
601 />
602 }
603 }
604 </StyleProvider>
605 <FontLoader ..self.fonts.clone() />
606 </>
607 }
608 }
609
610 fn destroy(&mut self, _ctx: &Context<Self>) {}
611}
612
613impl PerspectiveViewer {
614 fn init_toggle_settings_task(
628 &mut self,
629 ctx: &Context<Self>,
630 force: Option<bool>,
631 sender: Option<Sender<ApiResult<JsValue>>>,
632 ) {
633 let is_open = ctx.props().presentation.is_settings_open();
634 match force {
635 Some(force) if is_open == force => {
636 if let Some(sender) = sender {
637 sender.send(Ok(JsValue::UNDEFINED)).unwrap();
638 }
639 },
640 Some(_) | None => {
641 let force = !is_open;
642 let callback = ctx.link().callback(move |resolve| {
643 let update = SettingsUpdate::Update(force);
644 PerspectiveViewerMsg::ToggleSettingsComplete(update, resolve)
645 });
646
647 clone!(ctx.props().renderer, ctx.props().session);
648 ApiFuture::spawn(async move {
649 let result = if session.js_get_table().is_some() {
650 renderer.presize(force, callback.emit_async_safe()).await
651 } else {
652 callback.emit_async_safe().await?;
653 Ok(JsValue::UNDEFINED)
654 };
655
656 if let Some(sender) = sender {
657 let msg = result.ignore_view_delete();
658 sender
659 .send(msg.map(|x| x.unwrap_or(JsValue::UNDEFINED)))
660 .into_apierror()?;
661 };
662
663 Ok(JsValue::undefined())
664 });
665 },
666 };
667 }
668}