perspective_viewer/components/
viewer.rs1use 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 OpenColumnSettings {
136 locator: Option<ColumnLocator>,
137 sender: Option<Sender<()>>,
138 toggle: bool,
139 },
140}
141
142pub struct PerspectiveViewer {
143 dimensions: Option<(usize, usize, Option<usize>, Option<usize>)>,
144 on_rendered: Option<Sender<()>>,
145 fonts: FontLoaderProps,
146 settings_open: bool,
147 debug_open: bool,
148 selected_column: Option<ColumnLocator>,
150 selected_column_is_active: bool, on_resize: Rc<PubSub<()>>,
152 on_dimensions_reset: Rc<PubSub<()>>,
153 _subscriptions: [Subscription; 1],
154 settings_panel_width_override: Option<i32>,
155 column_settings_panel_width_override: Option<i32>,
156
157 on_close_column_settings: Callback<()>,
158}
159
160impl Component for PerspectiveViewer {
161 type Message = PerspectiveViewerMsg;
162 type Properties = PerspectiveViewerProps;
163
164 fn create(ctx: &Context<Self>) -> Self {
165 *ctx.props().weak_link.borrow_mut() = Some(ctx.link().clone());
166 let elem = ctx.props().elem.clone();
167 let callback = ctx
168 .link()
169 .callback(|()| PerspectiveViewerMsg::PreloadFontsUpdate);
170
171 let session_sub = {
172 clone!(
173 ctx.props().presentation,
174 ctx.props().session,
175 plugin_query = ctx.props().get_plugin_column_styles_query()
176 );
177 let callback = ctx.link().batch_callback(move |(update, render_limits)| {
178 if update {
179 vec![PerspectiveViewerMsg::RenderLimits(Some(render_limits))]
180 } else {
181 let locator =
182 presentation
183 .get_open_column_settings()
184 .locator
185 .filter(|locator| match &locator {
186 ColumnLocator::Table(name) => {
187 locator.is_active(&session)
188 && plugin_query
189 .can_render_column_styles(name)
190 .unwrap_or_default()
191 },
192 _ => true,
193 });
194
195 vec![
196 PerspectiveViewerMsg::RenderLimits(Some(render_limits)),
197 PerspectiveViewerMsg::OpenColumnSettings {
198 locator,
199 sender: None,
200 toggle: false,
201 },
202 ]
203 }
204 });
205 ctx.props()
206 .renderer
207 .render_limits_changed
208 .add_listener(callback)
209 };
210
211 let on_close_column_settings =
212 ctx.link()
213 .callback(|_| PerspectiveViewerMsg::OpenColumnSettings {
214 locator: None,
215 sender: None,
216 toggle: false,
217 });
218
219 Self {
220 dimensions: None,
221 on_rendered: None,
222 fonts: FontLoaderProps::new(&elem, callback),
223 settings_open: false,
224 debug_open: false,
225 selected_column: None,
226 selected_column_is_active: false,
227 on_resize: Default::default(),
228 on_dimensions_reset: Default::default(),
229 _subscriptions: [session_sub],
230 settings_panel_width_override: None,
231 column_settings_panel_width_override: None,
232 on_close_column_settings,
233 }
234 }
235
236 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
237 let needs_update = self.selected_column.is_some();
238 match msg {
239 PerspectiveViewerMsg::PreloadFontsUpdate => true,
240 PerspectiveViewerMsg::Resize => {
241 self.on_resize.emit(());
242 false
243 },
244 PerspectiveViewerMsg::Reset(all, sender) => {
245 self.selected_column = None;
246 clone!(
247 ctx.props().renderer,
248 ctx.props().session,
249 ctx.props().presentation
250 );
251
252 ApiFuture::spawn(async move {
253 session.reset(all).await?;
254 let columns_config = if all {
255 presentation.reset_columns_configs();
256 None
257 } else {
258 Some(presentation.all_columns_configs())
259 };
260
261 renderer.reset(columns_config.as_ref()).await?;
262 presentation.reset_available_themes(None).await;
263 let result = renderer.draw(session.validate().await?.create_view()).await;
264 if let Some(sender) = sender {
265 sender.send(()).unwrap();
266 }
267
268 renderer.reset_changed.emit(());
269 result
270 });
271
272 needs_update
273 },
274 PerspectiveViewerMsg::ToggleDebug => {
275 self.debug_open = !self.debug_open;
276 clone!(ctx.props().renderer, ctx.props().session);
277 ApiFuture::spawn(async move {
278 renderer.draw(session.validate().await?.create_view()).await
279 });
280
281 true
282 },
283 PerspectiveViewerMsg::ToggleSettingsInit(Some(SettingsUpdate::Missing), None) => false,
284 PerspectiveViewerMsg::ToggleSettingsInit(
285 Some(SettingsUpdate::Missing),
286 Some(resolve),
287 ) => {
288 resolve.send(Ok(JsValue::UNDEFINED)).unwrap();
289 false
290 },
291 PerspectiveViewerMsg::ToggleSettingsInit(Some(SettingsUpdate::SetDefault), resolve) => {
292 self.init_toggle_settings_task(ctx, Some(false), resolve);
293 false
294 },
295 PerspectiveViewerMsg::ToggleSettingsInit(
296 Some(SettingsUpdate::Update(force)),
297 resolve,
298 ) => {
299 self.init_toggle_settings_task(ctx, Some(force), resolve);
300 false
301 },
302 PerspectiveViewerMsg::ToggleSettingsInit(None, resolve) => {
303 self.init_toggle_settings_task(ctx, None, resolve);
304 false
305 },
306 PerspectiveViewerMsg::ToggleSettingsComplete(SettingsUpdate::SetDefault, resolve)
307 if self.settings_open =>
308 {
309 self.selected_column = None;
310 self.settings_open = false;
311 self.on_rendered = Some(resolve);
312 true
313 },
314 PerspectiveViewerMsg::ToggleSettingsComplete(
315 SettingsUpdate::Update(force),
316 resolve,
317 ) if force != self.settings_open => {
318 self.selected_column = None;
319 self.settings_open = force;
320 self.on_rendered = Some(resolve);
321 true
322 },
323 PerspectiveViewerMsg::ToggleSettingsComplete(_, resolve)
324 if matches!(self.fonts.get_status(), FontLoaderStatus::Finished) =>
325 {
326 self.selected_column = None;
327 resolve.send(()).expect("Orphan render");
328 false
329 },
330 PerspectiveViewerMsg::ToggleSettingsComplete(_, resolve) => {
331 self.selected_column = None;
332 self.on_rendered = Some(resolve);
333 true
334 },
335 PerspectiveViewerMsg::RenderLimits(dimensions) => {
336 if self.dimensions != dimensions {
337 self.dimensions = dimensions;
338 true
339 } else {
340 false
341 }
342 },
343 PerspectiveViewerMsg::OpenColumnSettings {
344 locator,
345 sender,
346 toggle,
347 } => {
348 let is_active = locator
349 .as_ref()
350 .map(|l| l.is_active(&ctx.props().session))
351 .unwrap_or_default();
352
353 self.selected_column_is_active = is_active;
354 if toggle && self.selected_column == locator {
355 self.selected_column = None;
356 (false, None)
357 } else {
358 self.selected_column.clone_from(&locator);
359
360 locator
361 .clone()
362 .map(|c| (true, c.name().cloned()))
363 .unwrap_or_default()
364 };
365
366 let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
367 open_column_settings
368 .locator
369 .clone_from(&self.selected_column);
370
371 ctx.props()
372 .presentation
373 .set_open_column_settings(Some(open_column_settings));
374
375 if let Some(sender) = sender {
376 sender.send(()).unwrap();
377 }
378
379 true
380 },
381 PerspectiveViewerMsg::SettingsPanelSizeUpdate(Some(x)) => {
382 self.settings_panel_width_override = Some(x);
383 false
384 },
385 PerspectiveViewerMsg::SettingsPanelSizeUpdate(None) => {
386 self.settings_panel_width_override = None;
387 false
388 },
389 PerspectiveViewerMsg::ColumnSettingsPanelSizeUpdate(Some(x)) => {
390 self.column_settings_panel_width_override = Some(x);
391 false
392 },
393 PerspectiveViewerMsg::ColumnSettingsPanelSizeUpdate(None) => {
394 self.column_settings_panel_width_override = None;
395 false
396 },
397 }
398 }
399
400 fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
404 true
405 }
406
407 fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
410 ctx.props()
411 .presentation
412 .set_settings_open(Some(self.settings_open))
413 .unwrap();
414
415 if self.on_rendered.is_some()
416 && matches!(self.fonts.get_status(), FontLoaderStatus::Finished)
417 {
418 self.on_rendered
419 .take()
420 .unwrap()
421 .send(())
422 .expect("Orphan render");
423 }
424 }
425
426 fn view(&self, ctx: &Context<Self>) -> Html {
428 let settings = ctx
429 .link()
430 .callback(|_| PerspectiveViewerMsg::ToggleSettingsInit(None, None));
431
432 let on_close_settings = ctx
433 .link()
434 .callback(|()| PerspectiveViewerMsg::ToggleSettingsInit(None, None));
435
436 let on_toggle_debug = ctx.link().callback(|_| PerspectiveViewerMsg::ToggleDebug);
437 let mut class = classes!("settings-closed");
438 if ctx.props().is_title() {
439 class.push("titled");
440 }
441
442 let on_open_expr_panel =
443 ctx.link()
444 .callback(|c| PerspectiveViewerMsg::OpenColumnSettings {
445 locator: Some(c),
446 sender: None,
447 toggle: true,
448 });
449
450 let on_reset = ctx
451 .link()
452 .callback(|all| PerspectiveViewerMsg::Reset(all, None));
453
454 let on_split_panel_resize = ctx
455 .link()
456 .callback(|(x, _)| PerspectiveViewerMsg::SettingsPanelSizeUpdate(Some(x)));
457
458 let on_column_settings_panel_resize = ctx
459 .link()
460 .callback(|(x, _)| PerspectiveViewerMsg::ColumnSettingsPanelSizeUpdate(Some(x)));
461
462 let settings_panel = html! {
463 <div id="settings_panel" class="sidebar_column noselect split-panel orient-vertical">
464 if self.selected_column.is_none() {
465 <SidebarCloseButton
466 id="settings_close_button"
467 on_close_sidebar={&on_close_settings}
468 />
469 }
470 <SidebarCloseButton
471 id={if self.debug_open { "debug_close_button" } else { "debug_open_button" }}
472 on_close_sidebar={&on_toggle_debug}
473 />
474 <PluginSelector
475 session={&ctx.props().session}
476 renderer={&ctx.props().renderer}
477 presentation={&ctx.props().presentation}
478 />
479 <ColumnSelector
480 dragdrop={&ctx.props().dragdrop}
481 renderer={&ctx.props().renderer}
482 session={&ctx.props().session}
483 presentation={&ctx.props().presentation}
484 on_resize={&self.on_resize}
485 on_open_expr_panel={&on_open_expr_panel}
486 on_dimensions_reset={&self.on_dimensions_reset}
487 selected_column={self.selected_column.clone()}
488 />
489 </div>
490 };
491
492 let main_panel = html! {
493 <div id="main_column">
494 <StatusBar
495 id="status_bar"
496 session={&ctx.props().session}
497 renderer={&ctx.props().renderer}
498 presentation={&ctx.props().presentation}
499 on_reset={on_reset.clone()}
500 />
501 <div id="main_panel_container">
502 <RenderWarning
503 dimensions={self.dimensions}
504 session={&ctx.props().session}
505 renderer={&ctx.props().renderer}
506 />
507 <slot />
508 </div>
509 if let Some(selected_column) = self.selected_column.clone() {
510 <SplitPanel
511 id="modal_panel"
512 reverse=true
513 initial_size={self.column_settings_panel_width_override}
514 on_reset={ctx.link().callback(|_| PerspectiveViewerMsg::ColumnSettingsPanelSizeUpdate(None))}
515 on_resize={on_column_settings_panel_resize}
516 >
517 <ColumnSettingsSidebar
518 session={&ctx.props().session}
519 renderer={&ctx.props().renderer}
520 custom_events={&ctx.props().custom_events}
521 presentation={&ctx.props().presentation}
522 {selected_column}
523 on_close={self.on_close_column_settings.clone()}
524 width_override={self.column_settings_panel_width_override}
525 is_active={self.selected_column_is_active}
526 />
527 <></>
528 </SplitPanel>
529 }
530 </div>
531 };
532
533 html! {
534 <>
535 <StyleProvider>
536 <LocalStyle href={css!("viewer")} />
537 if self.settings_open && ctx.props().session.has_table() {
538 if self.debug_open {
539 <SplitPanel
540 id="app_panel"
541 reverse=true
542 initial_size={self.settings_panel_width_override}
543 on_reset={ctx.link().callback(|_| PerspectiveViewerMsg::SettingsPanelSizeUpdate(None))}
544 on_resize={on_split_panel_resize}
545 on_resize_finished={ctx.props().render_callback()}
546 >
547 <DebugPanel
548 session={ctx.props().session()}
549 renderer={ctx.props().renderer()}
550 presentation={ctx.props().presentation()}
551 />
552 { settings_panel }
553 { main_panel }
554 </SplitPanel>
555 } else {
556 <SplitPanel
557 id="app_panel"
558 reverse=true
559 initial_size={self.settings_panel_width_override}
560 on_reset={ctx.link().callback(|_| PerspectiveViewerMsg::SettingsPanelSizeUpdate(None))}
561 on_resize={on_split_panel_resize}
562 on_resize_finished={ctx.props().resize_callback()}
563 >
564 { settings_panel }
565 { main_panel }
566 </SplitPanel>
567 }
568 } else {
569 <RenderWarning
570 dimensions={self.dimensions}
571 session={&ctx.props().session}
572 renderer={&ctx.props().renderer}
573 />
574 if ctx.props().is_title() || !ctx.props().session.has_table() {
575 <StatusBar
576 id="status_bar"
577 session={&ctx.props().session}
578 renderer={&ctx.props().renderer}
579 presentation={&ctx.props().presentation}
580 {on_reset}
581 />
582 }
583 <div id="main_panel_container" {class}><slot /></div>
584 if !ctx.props().presentation.get_is_workspace() {
585 <div
586 id="settings_button"
587 class={if ctx.props().is_title() { "noselect button closed titled" } else { "noselect button closed" }}
588 onmousedown={settings}
589 />
590 }
591 }
592 </StyleProvider>
593 <FontLoader ..self.fonts.clone() />
594 </>
595 }
596 }
597
598 fn destroy(&mut self, _ctx: &Context<Self>) {}
599}
600
601impl PerspectiveViewer {
602 fn init_toggle_settings_task(
616 &mut self,
617 ctx: &Context<Self>,
618 force: Option<bool>,
619 sender: Option<Sender<ApiResult<JsValue>>>,
620 ) {
621 let is_open = ctx.props().presentation.is_settings_open();
622 match force {
623 Some(force) if is_open == force => {
624 if let Some(sender) = sender {
625 sender.send(Ok(JsValue::UNDEFINED)).unwrap();
626 }
627 },
628 Some(_) | None => {
629 let force = !is_open;
630 let callback = ctx.link().callback(move |resolve| {
631 let update = SettingsUpdate::Update(force);
632 PerspectiveViewerMsg::ToggleSettingsComplete(update, resolve)
633 });
634
635 clone!(ctx.props().renderer, ctx.props().session);
636 ApiFuture::spawn(async move {
637 let result = if session.js_get_table().is_some() {
638 renderer.presize(force, callback.emit_async_safe()).await
639 } else {
640 callback.emit_async_safe().await?;
641 Ok(JsValue::UNDEFINED)
642 };
643
644 if let Some(sender) = sender {
645 let msg = result.clone().ignore_view_delete();
646 sender.send(msg).into_apierror()?;
647 };
648
649 result
650 });
651 },
652 };
653 }
654}