use std::rc::Rc;
use futures::channel::oneshot::*;
use perspective_client::ColumnType;
use wasm_bindgen::prelude::*;
use yew::prelude::*;
use super::column_selector::ColumnSelector;
use super::containers::split_panel::SplitPanel;
use super::font_loader::{FontLoader, FontLoaderProps, FontLoaderStatus};
use super::form::debug::DebugPanel;
use super::plugin_selector::PluginSelector;
use super::render_warning::RenderWarning;
use super::status_bar::StatusBar;
use super::style::{LocalStyle, StyleProvider};
use crate::components::column_settings_sidebar::ColumnSettingsSidebar;
use crate::components::containers::sidebar::SidebarCloseButton;
use crate::config::*;
use crate::custom_events::CustomEvents;
use crate::dragdrop::*;
use crate::model::*;
use crate::presentation::Presentation;
use crate::renderer::*;
use crate::session::*;
use crate::utils::*;
use crate::*;
#[derive(Clone, Debug, PartialEq)]
pub enum ColumnLocator {
Table(String),
Expression(String),
NewExpression,
}
impl ColumnLocator {
pub fn name(&self) -> Option<&String> {
match self {
Self::Table(s) | Self::Expression(s) => Some(s),
Self::NewExpression => None,
}
}
pub fn name_or_default(&self, session: &Session) -> String {
match self {
Self::Table(s) | Self::Expression(s) => s.clone(),
Self::NewExpression => session.metadata().make_new_column_name(None),
}
}
pub fn is_active(&self, session: &Session) -> bool {
self.name()
.map(|name| session.is_column_active(name))
.unwrap_or_default()
}
#[inline(always)]
pub fn is_saved_expr(&self) -> bool {
matches!(self, ColumnLocator::Expression(_))
}
#[inline(always)]
pub fn is_expr(&self) -> bool {
matches!(
self,
ColumnLocator::Expression(_) | ColumnLocator::NewExpression
)
}
#[inline(always)]
pub fn is_new_expr(&self) -> bool {
matches!(self, ColumnLocator::NewExpression)
}
pub fn view_type(&self, session: &Session) -> Option<ColumnType> {
let name = self.name().cloned().unwrap_or_default();
session.metadata().get_column_view_type(name.as_str())
}
}
#[derive(Properties)]
pub struct PerspectiveViewerProps {
pub elem: web_sys::HtmlElement,
pub session: Session,
pub renderer: Renderer,
pub presentation: Presentation,
pub dragdrop: DragDrop,
pub custom_events: CustomEvents,
#[prop_or_default]
pub weak_link: WeakScope<PerspectiveViewer>,
}
derive_model!(Renderer, Session, Presentation for PerspectiveViewerProps);
impl PartialEq for PerspectiveViewerProps {
fn eq(&self, _rhs: &Self) -> bool {
false
}
}
impl PerspectiveViewerProps {
fn is_title(&self) -> bool {
!self.presentation.get_is_workspace() && self.presentation.get_title().is_some()
}
}
#[derive(Debug)]
pub enum PerspectiveViewerMsg {
Resize,
Reset(bool, Option<Sender<()>>),
ToggleSettingsInit(Option<SettingsUpdate>, Option<Sender<ApiResult<JsValue>>>),
ToggleSettingsComplete(SettingsUpdate, Sender<()>),
ToggleDebug,
PreloadFontsUpdate,
RenderLimits(Option<(usize, usize, Option<usize>, Option<usize>)>),
SettingsPanelSizeUpdate(Option<i32>),
ColumnSettingsPanelSizeUpdate(Option<i32>),
OpenColumnSettings {
locator: Option<ColumnLocator>,
sender: Option<Sender<()>>,
toggle: bool,
},
}
pub struct PerspectiveViewer {
dimensions: Option<(usize, usize, Option<usize>, Option<usize>)>,
on_rendered: Option<Sender<()>>,
fonts: FontLoaderProps,
settings_open: bool,
debug_open: bool,
selected_column: Option<ColumnLocator>,
selected_column_is_active: bool, on_resize: Rc<PubSub<()>>,
on_dimensions_reset: Rc<PubSub<()>>,
_subscriptions: [Subscription; 1],
settings_panel_width_override: Option<i32>,
column_settings_panel_width_override: Option<i32>,
on_close_column_settings: Callback<()>,
}
impl Component for PerspectiveViewer {
type Message = PerspectiveViewerMsg;
type Properties = PerspectiveViewerProps;
fn create(ctx: &Context<Self>) -> Self {
*ctx.props().weak_link.borrow_mut() = Some(ctx.link().clone());
let elem = ctx.props().elem.clone();
let callback = ctx
.link()
.callback(|()| PerspectiveViewerMsg::PreloadFontsUpdate);
let session_sub = {
clone!(
ctx.props().presentation,
ctx.props().session,
plugin_query = ctx.props().get_plugin_column_styles_query()
);
let callback = ctx.link().batch_callback(move |(update, render_limits)| {
if update {
vec![PerspectiveViewerMsg::RenderLimits(Some(render_limits))]
} else {
let locator =
presentation
.get_open_column_settings()
.locator
.filter(|locator| match locator {
ColumnLocator::Table(ref name) => {
locator.is_active(&session)
&& plugin_query
.can_render_column_styles(name)
.unwrap_or_default()
},
_ => true,
});
vec![
PerspectiveViewerMsg::RenderLimits(Some(render_limits)),
PerspectiveViewerMsg::OpenColumnSettings {
locator,
sender: None,
toggle: false,
},
]
}
});
ctx.props()
.renderer
.render_limits_changed
.add_listener(callback)
};
let on_close_column_settings =
ctx.link()
.callback(|_| PerspectiveViewerMsg::OpenColumnSettings {
locator: None,
sender: None,
toggle: false,
});
Self {
dimensions: None,
on_rendered: None,
fonts: FontLoaderProps::new(&elem, callback),
settings_open: false,
debug_open: false,
selected_column: None,
selected_column_is_active: false,
on_resize: Default::default(),
on_dimensions_reset: Default::default(),
_subscriptions: [session_sub],
settings_panel_width_override: None,
column_settings_panel_width_override: None,
on_close_column_settings,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
let needs_update = self.selected_column.is_some();
match msg {
PerspectiveViewerMsg::PreloadFontsUpdate => true,
PerspectiveViewerMsg::Resize => {
self.on_resize.emit(());
false
},
PerspectiveViewerMsg::Reset(all, sender) => {
self.selected_column = None;
clone!(
ctx.props().renderer,
ctx.props().session,
ctx.props().presentation
);
ApiFuture::spawn(async move {
session.reset(all);
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;
let result = renderer.draw(session.validate().await?.create_view()).await;
if let Some(sender) = sender {
sender.send(()).unwrap();
}
renderer.reset_changed.emit(());
result
});
needs_update
},
PerspectiveViewerMsg::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
},
PerspectiveViewerMsg::ToggleSettingsInit(Some(SettingsUpdate::Missing), None) => false,
PerspectiveViewerMsg::ToggleSettingsInit(
Some(SettingsUpdate::Missing),
Some(resolve),
) => {
resolve.send(Ok(JsValue::UNDEFINED)).unwrap();
false
},
PerspectiveViewerMsg::ToggleSettingsInit(Some(SettingsUpdate::SetDefault), resolve) => {
self.init_toggle_settings_task(ctx, Some(false), resolve);
false
},
PerspectiveViewerMsg::ToggleSettingsInit(
Some(SettingsUpdate::Update(force)),
resolve,
) => {
self.init_toggle_settings_task(ctx, Some(force), resolve);
false
},
PerspectiveViewerMsg::ToggleSettingsInit(None, resolve) => {
self.init_toggle_settings_task(ctx, None, resolve);
false
},
PerspectiveViewerMsg::ToggleSettingsComplete(SettingsUpdate::SetDefault, resolve)
if self.settings_open =>
{
self.selected_column = None;
self.settings_open = false;
self.on_rendered = Some(resolve);
true
},
PerspectiveViewerMsg::ToggleSettingsComplete(
SettingsUpdate::Update(force),
resolve,
) if force != self.settings_open => {
self.selected_column = None;
self.settings_open = force;
self.on_rendered = Some(resolve);
true
},
PerspectiveViewerMsg::ToggleSettingsComplete(_, resolve)
if matches!(self.fonts.get_status(), FontLoaderStatus::Finished) =>
{
self.selected_column = None;
resolve.send(()).expect("Orphan render");
false
},
PerspectiveViewerMsg::ToggleSettingsComplete(_, resolve) => {
self.selected_column = None;
self.on_rendered = Some(resolve);
true
},
PerspectiveViewerMsg::RenderLimits(dimensions) => {
if self.dimensions != dimensions {
self.dimensions = dimensions;
true
} else {
false
}
},
PerspectiveViewerMsg::OpenColumnSettings {
locator,
sender,
toggle,
} => {
let is_active = locator
.as_ref()
.map(|l| l.is_active(&ctx.props().session))
.unwrap_or_default();
self.selected_column_is_active = is_active;
if toggle && self.selected_column == locator {
self.selected_column = None;
(false, None)
} else {
self.selected_column.clone_from(&locator);
locator
.clone()
.map(|c| (true, c.name().cloned()))
.unwrap_or_default()
};
let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
open_column_settings
.locator
.clone_from(&self.selected_column);
ctx.props()
.presentation
.set_open_column_settings(Some(open_column_settings));
if let Some(sender) = sender {
sender.send(()).unwrap();
}
true
},
PerspectiveViewerMsg::SettingsPanelSizeUpdate(Some(x)) => {
self.settings_panel_width_override = Some(x);
false
},
PerspectiveViewerMsg::SettingsPanelSizeUpdate(None) => {
self.settings_panel_width_override = None;
false
},
PerspectiveViewerMsg::ColumnSettingsPanelSizeUpdate(Some(x)) => {
self.column_settings_panel_width_override = Some(x);
false
},
PerspectiveViewerMsg::ColumnSettingsPanelSizeUpdate(None) => {
self.column_settings_panel_width_override = None;
false
},
}
}
fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
true
}
fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
ctx.props()
.presentation
.set_settings_open(Some(self.settings_open))
.unwrap();
if self.on_rendered.is_some()
&& matches!(self.fonts.get_status(), FontLoaderStatus::Finished)
{
self.on_rendered
.take()
.unwrap()
.send(())
.expect("Orphan render");
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let settings = ctx
.link()
.callback(|_| PerspectiveViewerMsg::ToggleSettingsInit(None, None));
let on_close_settings = ctx
.link()
.callback(|()| PerspectiveViewerMsg::ToggleSettingsInit(None, None));
let on_toggle_debug = ctx.link().callback(|_| PerspectiveViewerMsg::ToggleDebug);
let mut class = classes!("settings-closed");
if ctx.props().is_title() {
class.push("titled");
}
let on_open_expr_panel =
ctx.link()
.callback(|c| PerspectiveViewerMsg::OpenColumnSettings {
locator: Some(c),
sender: None,
toggle: true,
});
let on_reset = ctx
.link()
.callback(|all| PerspectiveViewerMsg::Reset(all, None));
let on_split_panel_resize = ctx
.link()
.callback(|(x, _)| PerspectiveViewerMsg::SettingsPanelSizeUpdate(Some(x)));
let on_column_settings_panel_resize = ctx
.link()
.callback(|(x, _)| PerspectiveViewerMsg::ColumnSettingsPanelSizeUpdate(Some(x)));
let settings_panel = html! {
<div id="settings_panel" class="sidebar_column noselect split-panel orient-vertical">
if self.selected_column.is_none() {
<SidebarCloseButton
id="settings_close_button"
on_close_sidebar={&on_close_settings}
/>
}
<SidebarCloseButton
id={if self.debug_open { "debug_close_button" } else { "debug_open_button" }}
on_close_sidebar={&on_toggle_debug}
/>
<PluginSelector
session={&ctx.props().session}
renderer={&ctx.props().renderer}
presentation={&ctx.props().presentation}
/>
<ColumnSelector
dragdrop={&ctx.props().dragdrop}
renderer={&ctx.props().renderer}
session={&ctx.props().session}
presentation={&ctx.props().presentation}
on_resize={&self.on_resize}
on_open_expr_panel={&on_open_expr_panel}
on_dimensions_reset={&self.on_dimensions_reset}
selected_column={self.selected_column.clone()}
/>
</div>
};
let main_panel = html! {
<div id="main_column">
<StatusBar
id="status_bar"
session={&ctx.props().session}
renderer={&ctx.props().renderer}
presentation={&ctx.props().presentation}
on_reset={on_reset.clone()}
/>
<div id="main_panel_container">
<RenderWarning
dimensions={self.dimensions}
session={&ctx.props().session}
renderer={&ctx.props().renderer}
/>
<slot />
</div>
if let Some(selected_column) = self.selected_column.clone() {
<SplitPanel
id="modal_panel"
reverse=true
initial_size={self.column_settings_panel_width_override}
on_reset={ctx.link().callback(|_| PerspectiveViewerMsg::ColumnSettingsPanelSizeUpdate(None))}
on_resize={on_column_settings_panel_resize}
>
<ColumnSettingsSidebar
session={&ctx.props().session}
renderer={&ctx.props().renderer}
custom_events={&ctx.props().custom_events}
presentation={&ctx.props().presentation}
{selected_column}
on_close={self.on_close_column_settings.clone()}
width_override={self.column_settings_panel_width_override}
is_active={self.selected_column_is_active}
/>
<></>
</SplitPanel>
}
</div>
};
html! {
<>
<StyleProvider>
<LocalStyle href={css!("viewer")} />
if self.settings_open && ctx.props().session.has_table() {
if self.debug_open {
<SplitPanel
id="app_panel"
reverse=true
initial_size={self.settings_panel_width_override}
on_reset={ctx.link().callback(|_| PerspectiveViewerMsg::SettingsPanelSizeUpdate(None))}
on_resize={on_split_panel_resize}
on_resize_finished={ctx.props().render_callback()}
>
<DebugPanel
session={ctx.props().session()}
renderer={ctx.props().renderer()}
presentation={ctx.props().presentation()}
/>
{ settings_panel }
{ main_panel }
</SplitPanel>
} else {
<SplitPanel
id="app_panel"
reverse=true
initial_size={self.settings_panel_width_override}
on_reset={ctx.link().callback(|_| PerspectiveViewerMsg::SettingsPanelSizeUpdate(None))}
on_resize={on_split_panel_resize}
on_resize_finished={ctx.props().render_callback()}
>
{ settings_panel }
{ main_panel }
</SplitPanel>
}
} else {
<RenderWarning
dimensions={self.dimensions}
session={&ctx.props().session}
renderer={&ctx.props().renderer}
/>
if ctx.props().is_title() || !ctx.props().session.has_table() {
<StatusBar
id="status_bar"
session={&ctx.props().session}
renderer={&ctx.props().renderer}
presentation={&ctx.props().presentation}
{on_reset}
/>
}
<div id="main_panel_container" {class}><slot /></div>
if !ctx.props().presentation.get_is_workspace() {
<div
id="settings_button"
class={if ctx.props().is_title() { "noselect button closed titled" } else { "noselect button closed" }}
onmousedown={settings}
/>
}
}
</StyleProvider>
<FontLoader ..self.fonts.clone() />
</>
}
}
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();
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);
PerspectiveViewerMsg::ToggleSettingsComplete(update, resolve)
});
clone!(ctx.props().renderer, ctx.props().session);
ApiFuture::spawn(async move {
let result = if session.js_get_table().is_some() {
renderer.presize(force, callback.emit_async_safe()).await
} else {
callback.emit_async_safe().await?;
Ok(JsValue::UNDEFINED)
};
if let Some(sender) = sender {
let msg = result.clone().ignore_view_delete();
sender.send(msg).into_apierror()?;
};
result
});
},
};
}
}