use std::rc::Rc;
use futures::channel::oneshot::*;
use perspective_js::utils::*;
use wasm_bindgen::prelude::*;
use yew::prelude::*;
use super::containers::split_panel::SplitPanel;
use super::font_loader::{FontLoader, FontLoaderProps, FontLoaderStatus};
use super::form::debug::DebugPanel;
use super::style::{LocalStyle, StyleProvider};
use crate::components::column_settings_sidebar::ColumnSettingsPanel;
use crate::components::main_panel::MainPanel;
use crate::components::settings_panel::SettingsPanel;
use crate::config::*;
use crate::css;
use crate::custom_events::CustomEvents;
use crate::dragdrop::{DragDropProps, *};
use crate::js::JsPerspectiveViewerPlugin;
use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation, PresentationProps};
use crate::renderer::{RendererProps, *};
use crate::session::{SessionProps, *};
use crate::tasks::*;
use crate::utils::*;
#[derive(Clone, Properties)]
pub struct PerspectiveViewerProps {
pub elem: web_sys::HtmlElement,
pub custom_events: CustomEvents,
pub dragdrop: DragDrop,
pub session: Session,
pub renderer: Renderer,
pub presentation: Presentation,
}
impl PartialEq for PerspectiveViewerProps {
fn eq(&self, _rhs: &Self) -> bool {
false
}
}
impl HasCustomEvents for PerspectiveViewerProps {
fn custom_events(&self) -> &CustomEvents {
&self.custom_events
}
}
impl HasDragDrop for PerspectiveViewerProps {
fn dragdrop(&self) -> &DragDrop {
&self.dragdrop
}
}
impl HasPresentation for PerspectiveViewerProps {
fn presentation(&self) -> &Presentation {
&self.presentation
}
}
impl HasRenderer for PerspectiveViewerProps {
fn renderer(&self) -> &Renderer {
&self.renderer
}
}
impl HasSession for PerspectiveViewerProps {
fn session(&self) -> &Session {
&self.session
}
}
impl StateProvider for PerspectiveViewerProps {
type State = PerspectiveViewerProps;
fn clone_state(&self) -> Self::State {
self.clone()
}
}
#[derive(Debug)]
pub enum PerspectiveViewerMsg {
ColumnSettingsPanelSizeUpdate(Option<i32>),
ColumnSettingsTabChanged(ColumnSettingsTab),
OpenColumnSettings {
locator: Option<ColumnLocator>,
sender: Option<Sender<()>>,
toggle: bool,
},
PreloadFontsUpdate,
Reset(bool, Option<Sender<()>>),
Resize,
SettingsPanelSizeUpdate(Option<i32>),
ToggleDebug,
ToggleSettingsComplete(SettingsUpdate, Sender<()>),
ToggleSettingsInit(Option<SettingsUpdate>, Option<Sender<ApiResult<JsValue>>>),
UpdateSession(Box<SessionProps>),
UpdateRenderer(Box<RendererProps>),
UpdatePresentation(Box<PresentationProps>),
UpdateSettingsOpen(bool),
UpdateIsWorkspace(bool),
UpdateColumnSettings(Box<crate::presentation::OpenColumnSettings>),
UpdateDragDrop(Box<DragDropProps>),
UpdateSessionStats(Option<ViewStats>, Option<TableLoadState>),
IncrementUpdateCount,
DecrementUpdateCount,
}
use PerspectiveViewerMsg::*;
pub struct PerspectiveViewer {
_subscriptions: Vec<Subscription>,
column_settings_panel_width_override: Option<i32>,
debug_open: bool,
fonts: FontLoaderProps,
on_close_column_settings: Callback<()>,
on_rendered: Option<Sender<()>>,
on_resize: Rc<PubSub<()>>,
settings_open: bool,
settings_panel_width_override: Option<i32>,
session_props: SessionProps,
renderer_props: RendererProps,
presentation_props: PresentationProps,
dragdrop_props: DragDropProps,
update_count: u32,
}
impl Component for PerspectiveViewer {
type Message = PerspectiveViewerMsg;
type Properties = PerspectiveViewerProps;
fn create(ctx: &Context<Self>) -> Self {
let elem = ctx.props().elem.clone();
let fonts = FontLoaderProps::new(&elem, ctx.link().callback(|()| PreloadFontsUpdate));
inject_engine_callbacks(ctx);
let subscriptions = create_subscriptions(ctx);
let session_props = ctx.props().session.to_props();
let renderer_props = ctx.props().renderer.to_props(None);
let presentation_props = ctx.props().presentation.to_props(PtrEqRc::new(vec![]));
let on_close_column_settings = ctx.link().callback(|_| OpenColumnSettings {
locator: None,
sender: None,
toggle: false,
});
{
let presentation = ctx.props().presentation.clone();
let cb = ctx.link().callback(move |themes: PtrEqRc<Vec<String>>| {
UpdatePresentation(Box::new(presentation.to_props(themes)))
});
let presentation = ctx.props().presentation.clone();
ApiFuture::spawn(async move {
let themes = presentation.get_available_themes().await?;
cb.emit(themes);
Ok(())
});
}
Self {
_subscriptions: subscriptions,
column_settings_panel_width_override: None,
debug_open: false,
fonts,
on_close_column_settings,
on_rendered: None,
on_resize: Default::default(),
settings_open: false,
settings_panel_width_override: None,
session_props,
renderer_props,
presentation_props,
dragdrop_props: DragDropProps::default(),
update_count: 0,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
PreloadFontsUpdate => true,
Resize => {
self.on_resize.emit(());
false
},
Reset(all, sender) => {
ctx.props().presentation.set_open_column_settings(None);
clone!(
ctx.props().renderer,
ctx.props().session,
ctx.props().presentation
);
ApiFuture::spawn(async move {
session
.reset(ResetOptions {
config: true,
expressions: all,
..ResetOptions::default()
})
.await?;
let columns_config = if all {
presentation.reset_columns_configs();
None
} else {
Some(presentation.all_columns_configs())
};
renderer.reset(columns_config.as_ref()).await?;
presentation.reset_available_themes(None).await;
if all {
presentation.reset_theme().await?;
}
let result = renderer.draw(session.validate().await?.create_view()).await;
if let Some(sender) = sender {
sender.send(()).unwrap();
}
renderer.reset_changed.emit(());
result
});
false
},
ToggleSettingsInit(Some(SettingsUpdate::Missing), None) => false,
ToggleSettingsInit(Some(SettingsUpdate::Missing), Some(resolve)) => {
resolve.send(Ok(JsValue::UNDEFINED)).unwrap();
false
},
ToggleSettingsInit(Some(SettingsUpdate::SetDefault), resolve) => {
self.init_toggle_settings_task(ctx, Some(false), resolve);
false
},
ToggleSettingsInit(Some(SettingsUpdate::Update(force)), resolve) => {
self.init_toggle_settings_task(ctx, Some(force), resolve);
false
},
ToggleSettingsInit(None, resolve) => {
self.init_toggle_settings_task(ctx, None, resolve);
false
},
ToggleSettingsComplete(SettingsUpdate::SetDefault, resolve) if self.settings_open => {
ctx.props().presentation.set_open_column_settings(None);
self.settings_open = false;
self.on_rendered = Some(resolve);
true
},
ToggleSettingsComplete(SettingsUpdate::Update(force), resolve)
if force != self.settings_open =>
{
ctx.props().presentation.set_open_column_settings(None);
self.settings_open = force;
self.on_rendered = Some(resolve);
true
},
ToggleSettingsComplete(_, resolve)
if matches!(self.fonts.get_status(), FontLoaderStatus::Finished) =>
{
ctx.props().presentation.set_open_column_settings(None);
if let Err(e) = resolve.send(()) {
tracing::error!("toggle settings failed {:?}", e);
}
false
},
ToggleSettingsComplete(_, resolve) => {
ctx.props().presentation.set_open_column_settings(None);
self.on_rendered = Some(resolve);
true
},
OpenColumnSettings {
locator,
sender,
toggle,
} => {
let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
if locator == open_column_settings.locator {
if toggle {
ctx.props().presentation.set_open_column_settings(None);
}
} else {
open_column_settings.locator.clone_from(&locator);
open_column_settings.tab =
if matches!(locator, Some(ColumnLocator::NewExpression)) {
Some(ColumnSettingsTab::Attributes)
} else {
locator.as_ref().and_then(|x| {
x.name().map(|x| {
if self.session_props.is_column_active(x) {
ColumnSettingsTab::Style
} else {
ColumnSettingsTab::Attributes
}
})
})
};
ctx.props()
.presentation
.set_open_column_settings(Some(open_column_settings));
}
if let Some(sender) = sender {
sender.send(()).unwrap();
}
true
},
SettingsPanelSizeUpdate(Some(x)) => {
self.settings_panel_width_override = Some(x);
false
},
SettingsPanelSizeUpdate(None) => {
self.settings_panel_width_override = None;
false
},
ColumnSettingsPanelSizeUpdate(Some(x)) => {
self.column_settings_panel_width_override = Some(x);
false
},
ColumnSettingsPanelSizeUpdate(None) => {
self.column_settings_panel_width_override = None;
false
},
ColumnSettingsTabChanged(tab) => {
let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
open_column_settings.tab.clone_from(&Some(tab));
ctx.props()
.presentation
.set_open_column_settings(Some(open_column_settings));
true
},
ToggleDebug => {
self.debug_open = !self.debug_open;
clone!(ctx.props().renderer, ctx.props().session);
ApiFuture::spawn(async move {
renderer.draw(session.validate().await?.create_view()).await
});
true
},
UpdateSession(props) => {
let changed = *props != self.session_props;
self.session_props = *props;
changed
},
UpdateSessionStats(stats, has_table) => {
let changed =
stats != self.session_props.stats || has_table != self.session_props.has_table;
self.session_props.stats = stats;
self.session_props.has_table = has_table;
changed
},
UpdateRenderer(props) => {
let changed = *props != self.renderer_props;
self.renderer_props = *props;
changed
},
UpdatePresentation(props) => {
let changed = *props != self.presentation_props;
self.presentation_props = *props;
changed
},
UpdateSettingsOpen(open) => {
let changed = open != self.presentation_props.is_settings_open;
self.presentation_props.is_settings_open = open;
changed
},
UpdateIsWorkspace(is_workspace) => {
let changed = is_workspace != self.presentation_props.is_workspace;
self.presentation_props.is_workspace = is_workspace;
changed
},
UpdateColumnSettings(ocs) => {
let changed = *ocs != self.presentation_props.open_column_settings;
self.presentation_props.open_column_settings = *ocs;
changed
},
UpdateDragDrop(props) => {
let changed = *props != self.dragdrop_props;
self.dragdrop_props = *props;
changed
},
IncrementUpdateCount => {
self.update_count = self.update_count.saturating_add(1);
true
},
DecrementUpdateCount => {
self.update_count = self.update_count.saturating_sub(1);
true
},
}
}
fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
true
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
if self.on_rendered.is_some()
&& matches!(self.fonts.get_status(), FontLoaderStatus::Finished)
&& self.on_rendered.take().unwrap().send(()).is_err()
{
tracing::warn!("Orphan render");
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let Self::Properties {
custom_events,
dragdrop,
presentation,
renderer,
session,
..
} = ctx.props();
let is_settings_open = self.settings_open
&& matches!(self.session_props.has_table, Some(TableLoadState::Loaded));
let mut class = classes!();
if !is_settings_open {
class.push("settings-closed");
}
if self.session_props.title.is_some() {
class.push("titled");
}
let on_open_expr_panel = ctx.link().callback(|c| OpenColumnSettings {
locator: Some(c),
sender: None,
toggle: true,
});
let on_split_panel_resize = ctx
.link()
.callback(|(x, _)| SettingsPanelSizeUpdate(Some(x)));
let on_column_settings_panel_resize = ctx
.link()
.callback(|(x, _)| ColumnSettingsPanelSizeUpdate(Some(x)));
let on_close_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
let on_debug = ctx.link().callback(|_| ToggleDebug);
let selected_column = get_current_column_locator(
&self.presentation_props.open_column_settings,
&ctx.props().renderer,
&self.session_props.config,
&self.session_props.metadata,
);
let selected_tab = self.presentation_props.open_column_settings.tab;
let plugin_name = self.renderer_props.plugin_name.clone();
let available_plugins = self.renderer_props.available_plugins.clone();
let has_table = self.session_props.has_table.clone();
let named_column_count = self
.renderer_props
.requirements
.names
.as_ref()
.map(|n| n.len())
.unwrap_or(0);
let view_config = self.session_props.config.clone();
let drag_column = self.dragdrop_props.column.clone();
let metadata = self.session_props.metadata.clone();
let settings_panel = html! {
if is_settings_open {
<SettingsPanel
on_close={on_close_settings}
on_resize={&self.on_resize}
on_select_column={on_open_expr_panel}
is_debug={self.debug_open}
{on_debug}
{plugin_name}
{available_plugins}
{has_table}
{named_column_count}
{view_config}
{drag_column}
metadata={metadata.clone()}
open_column_settings={self.presentation_props.open_column_settings.clone()}
selected_theme={self.presentation_props.selected_theme.clone()}
{dragdrop}
{presentation}
{renderer}
{session}
/>
}
};
let on_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
let on_select_tab = ctx.link().callback(ColumnSettingsTabChanged);
let column_settings_panel = html! {
if let Some(selected_column) = selected_column {
<SplitPanel
id="modal_panel"
reverse=true
initial_size={self.column_settings_panel_width_override}
on_reset={ctx.link().callback(|_| ColumnSettingsPanelSizeUpdate(None))}
on_resize={on_column_settings_panel_resize}
>
<ColumnSettingsPanel
{selected_column}
{selected_tab}
on_close={self.on_close_column_settings.clone()}
width_override={self.column_settings_panel_width_override}
{on_select_tab}
plugin_name={self.renderer_props.plugin_name.clone()}
{metadata}
view_config={self.session_props.config.clone()}
selected_theme={self.presentation_props.selected_theme.clone()}
{custom_events}
{presentation}
{renderer}
{session}
/>
<></>
</SplitPanel>
}
};
let on_reset = ctx.link().callback(|all| Reset(all, None));
let render_limits = self.renderer_props.render_limits;
let has_table = self.session_props.has_table.clone();
let is_errored = self.session_props.error.is_some();
let stats = self.session_props.stats.clone();
let update_count = self.update_count;
let error = self.session_props.error.clone();
let is_settings_open = self.settings_open
&& matches!(self.session_props.has_table, Some(TableLoadState::Loaded));
let title = self.session_props.title.clone();
let selected_theme = self.presentation_props.selected_theme.clone();
let available_themes = self.presentation_props.available_themes.clone();
let main_panel = html! {
<MainPanel
{on_settings}
{on_reset}
{render_limits}
{has_table}
{is_errored}
{stats}
{update_count}
{error}
{is_settings_open}
{title}
{selected_theme}
{available_themes}
is_workspace={self.presentation_props.is_workspace}
{custom_events}
{presentation}
{renderer}
{session}
/>
};
let debug_panel = html! {
if self.debug_open { <DebugPanel {presentation} {renderer} {session} /> }
};
html! {
<StyleProvider root={ctx.props().elem.clone()}>
<LocalStyle href={css!("viewer")} />
<div id="component_container">
if is_settings_open {
<SplitPanel
id="app_panel"
reverse=true
skip_empty=true
initial_size={self.settings_panel_width_override}
on_reset={ctx.link().callback(|_| SettingsPanelSizeUpdate(None))}
on_resize={on_split_panel_resize.clone()}
on_resize_finished={ctx.props().render_callback()}
>
{ debug_panel }
{ settings_panel }
<div id="main_column_container">
{ main_panel }
{ column_settings_panel }
</div>
</SplitPanel>
} else {
<div id="main_column_container">
{ main_panel }
{ column_settings_panel }
</div>
}
</div>
<FontLoader ..self.fonts.clone() />
</StyleProvider>
}
}
fn destroy(&mut self, _ctx: &Context<Self>) {}
}
impl PerspectiveViewer {
fn init_toggle_settings_task(
&mut self,
ctx: &Context<Self>,
force: Option<bool>,
sender: Option<Sender<ApiResult<JsValue>>>,
) {
let is_open = ctx.props().presentation.is_settings_open();
ctx.props().presentation.set_settings_before_open(!is_open);
match force {
Some(force) if is_open == force => {
if let Some(sender) = sender {
sender.send(Ok(JsValue::UNDEFINED)).unwrap();
}
},
Some(_) | None => {
let force = !is_open;
let callback = ctx.link().callback(move |resolve| {
let update = SettingsUpdate::Update(force);
ToggleSettingsComplete(update, resolve)
});
clone!(
ctx.props().renderer,
ctx.props().session,
ctx.props().presentation
);
ApiFuture::spawn(async move {
let result = if session.js_get_table().is_some() {
renderer
.presize(force, {
let (sender, receiver) = channel::<()>();
async move {
callback.emit(sender);
presentation.set_settings_open(!is_open);
Ok(receiver.await?)
}
})
.await
} else {
let (sender, receiver) = channel::<()>();
callback.emit(sender);
presentation.set_settings_open(!is_open);
receiver.await?;
Ok(JsValue::UNDEFINED)
};
if let Some(sender) = sender {
let msg = result.ignore_view_delete();
sender
.send(msg.map(|x| x.unwrap_or(JsValue::UNDEFINED)))
.into_apierror()?;
};
Ok(JsValue::undefined())
});
},
};
}
}
fn create_subscriptions(ctx: &Context<PerspectiveViewer>) -> Vec<Subscription> {
let session_props_sub = {
let session = ctx.props().session.clone();
let cb = ctx
.link()
.callback(move |_: ()| UpdateSession(Box::new(session.to_props())));
let s = &ctx.props().session;
let sub1 = s.table_loaded.add_notify_listener(&cb);
let sub2 = s.table_unloaded.add_notify_listener(&cb);
let sub3 = s.view_created.add_notify_listener(&cb);
let sub4 = s.view_config_changed.add_notify_listener(&cb);
let sub5 = s.title_changed.add_notify_listener(&cb);
let sub6 = s
.view_config_changed
.add_listener(ctx.link().callback(|_| IncrementUpdateCount));
let sub7 = s
.view_created
.add_listener(ctx.link().callback(|_| DecrementUpdateCount));
vec![sub1, sub2, sub3, sub4, sub5, sub6, sub7]
};
let renderer_props_sub = {
let renderer = ctx.props().renderer.clone();
let cb_plugin = ctx.link().callback({
move |_: JsPerspectiveViewerPlugin| UpdateRenderer(Box::new(renderer.to_props(None)))
});
let sub1 = ctx.props().renderer.plugin_changed.add_listener(cb_plugin);
vec![sub1]
};
let presentation_props_sub = {
let presentation = ctx.props().presentation.clone();
let cb_settings = ctx.link().callback(UpdateSettingsOpen);
let cb_theme = {
let pres = presentation.clone();
ctx.link()
.callback(move |(themes, _): (PtrEqRc<Vec<String>>, _)| {
UpdatePresentation(Box::new(pres.to_props(themes)))
})
};
let cb_column_settings = {
let pres = presentation.clone();
ctx.link().callback(move |_: (bool, Option<String>)| {
UpdateColumnSettings(Box::new(pres.get_open_column_settings()))
})
};
let sub1 = presentation.settings_open_changed.add_listener(cb_settings);
let sub2 = presentation.theme_config_updated.add_listener(cb_theme);
let sub3 = presentation
.column_settings_open_changed
.add_listener(cb_column_settings);
vec![sub1, sub2, sub3]
};
let dragdrop_props_sub = {
let cb_clear = ctx.link().callback(|_: ()| UpdateDragDrop(Box::default()));
let sub1 = ctx
.props()
.dragdrop
.drop_received
.add_notify_listener(&cb_clear);
vec![sub1]
};
let mut subscriptions = Vec::new();
subscriptions.extend(session_props_sub);
subscriptions.extend(renderer_props_sub);
subscriptions.extend(presentation_props_sub);
subscriptions.extend(dragdrop_props_sub);
subscriptions
}
fn inject_engine_callbacks(ctx: &Context<PerspectiveViewer>) {
{
let session = ctx.props().session.clone();
let cb = ctx.link().callback(move |_: ()| {
UpdateSessionStats(session.get_table_stats(), session.has_table())
});
*ctx.props().session.on_stats_changed.borrow_mut() = Some(cb);
}
{
let session = ctx.props().session.clone();
let cb = ctx
.link()
.callback(move |_: ()| UpdateSession(Box::new(session.to_props())));
*ctx.props().session.on_table_errored.borrow_mut() = Some(cb);
}
{
clone!(
ctx.props().presentation,
ctx.props().renderer,
ctx.props().session
);
let cb = ctx.link().batch_callback(move |limits: RenderLimits| {
let mut msgs = vec![UpdateRenderer(Box::new(renderer.to_props(Some(limits))))];
if !limits.is_update {
let locator = get_current_column_locator(
&presentation.get_open_column_settings(),
&renderer,
&session.get_view_config(),
&session.metadata(),
);
msgs.push(OpenColumnSettings {
locator,
sender: None,
toggle: false,
});
}
msgs
});
*ctx.props().renderer.on_render_limits_changed.borrow_mut() = Some(cb);
}
{
let cb = ctx.link().callback(UpdateIsWorkspace);
*ctx.props()
.presentation
.on_is_workspace_changed
.borrow_mut() = Some(cb);
}
{
let dragdrop = ctx.props().dragdrop.clone();
let cb = ctx
.link()
.callback(move |_: DragEffect| UpdateDragDrop(Box::new(dragdrop.to_props())));
*ctx.props().dragdrop.on_dragstart.borrow_mut() = Some(cb);
}
{
let cb = ctx.link().callback(|_: ()| UpdateDragDrop(Box::default()));
*ctx.props().dragdrop.on_dragend.borrow_mut() = Some(cb);
}
}