mod activate;
pub mod limits;
mod plugin_store;
mod props;
mod registry;
mod render_timer;
use std::cell::{Ref, RefCell};
use std::collections::HashMap;
use std::future::Future;
use std::ops::Deref;
use std::pin::Pin;
use std::rc::Rc;
use futures::future::{join_all, select_all};
use perspective_client::utils::*;
use perspective_client::{View, ViewWindow};
use perspective_js::json;
use perspective_js::utils::ApiResult;
use wasm_bindgen::prelude::*;
use web_sys::*;
use yew::html::ImplicitClone;
use yew::prelude::*;
use self::activate::*;
pub use self::limits::RenderLimits;
use self::limits::*;
use self::plugin_store::*;
pub use self::props::RendererProps;
pub use self::registry::*;
use self::render_timer::*;
use crate::config::*;
use crate::js::plugin::*;
use crate::presentation::ColumnConfigMap;
use crate::utils::*;
pub struct RendererData {
plugin_data: RefCell<RendererMutData>,
draw_lock: DebounceMutex,
pub plugin_changed: PubSub<JsPerspectiveViewerPlugin>,
pub style_changed: PubSub<()>,
pub reset_changed: PubSub<()>,
pub on_render_limits_changed: RefCell<Option<Callback<RenderLimits>>>,
}
pub struct RendererMutData {
viewer_elem: HtmlElement,
metadata: ViewConfigRequirements,
plugin_store: PluginStore,
plugins_idx: Option<usize>,
timer: MovingWindowRenderTimer,
selection: Option<ViewWindow>,
pending_plugin: Option<usize>,
}
#[derive(Clone)]
pub struct Renderer(Rc<RendererData>);
impl Deref for Renderer {
type Target = RendererData;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PartialEq for Renderer {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
impl ImplicitClone for Renderer {}
impl Deref for RendererData {
type Target = RefCell<RendererMutData>;
fn deref(&self) -> &Self::Target {
&self.plugin_data
}
}
type TaskResult = ApiResult<JsValue>;
type TimeoutTask<'a> = Pin<Box<dyn Future<Output = Option<TaskResult>> + 'a>>;
static PRESIZE_TIMEOUT: i32 = 500;
impl Renderer {
pub fn new(viewer_elem: &HtmlElement) -> Self {
Self(Rc::new(RendererData {
plugin_data: RefCell::new(RendererMutData {
viewer_elem: viewer_elem.clone(),
metadata: ViewConfigRequirements::default(),
plugin_store: PluginStore::default(),
plugins_idx: None,
selection: None,
timer: MovingWindowRenderTimer::default(),
pending_plugin: None,
}),
draw_lock: Default::default(),
plugin_changed: Default::default(),
style_changed: Default::default(),
reset_changed: Default::default(),
on_render_limits_changed: Default::default(),
}))
}
pub async fn reset(&self, columns_config: Option<&ColumnConfigMap>) -> ApiResult<()> {
self.0.borrow_mut().plugins_idx = None;
if let Ok(plugin) = self.get_active_plugin() {
plugin.restore(&json!({}), columns_config)?;
}
Ok(())
}
pub fn delete(&self) -> ApiResult<()> {
self.get_active_plugin().map(|x| x.delete()).unwrap_or_log();
self.plugin_data.borrow().viewer_elem.set_inner_text("");
let new_state = Self::new(&self.plugin_data.borrow().viewer_elem);
std::mem::swap(
&mut *self.plugin_data.borrow_mut(),
&mut *new_state.plugin_data.borrow_mut(),
);
Ok(())
}
pub fn metadata(&self) -> Ref<'_, ViewConfigRequirements> {
Ref::map(self.borrow(), |x| &x.metadata)
}
pub fn is_chart(&self) -> bool {
let plugin = self.get_active_plugin().unwrap();
plugin.name().as_str() != "Datagrid"
}
pub fn get_all_plugins(&self) -> Vec<JsPerspectiveViewerPlugin> {
self.0.borrow_mut().plugin_store.plugins().clone()
}
pub fn get_all_plugin_categories(&self) -> HashMap<String, Vec<String>> {
self.0.borrow_mut().plugin_store.plugin_records().clone()
}
pub fn get_active_plugin(&self) -> ApiResult<JsPerspectiveViewerPlugin> {
if self.0.borrow().plugins_idx.is_none() {
let _ = self.apply_pending_plugin()?;
}
let idx = self.0.borrow().plugins_idx.unwrap_or(0);
let result = self.0.borrow_mut().plugin_store.plugins().get(idx).cloned();
Ok(result.ok_or("No Plugin")?)
}
pub fn get_plugin(&self, name: &str) -> ApiResult<JsPerspectiveViewerPlugin> {
let idx = self.find_plugin_idx(name);
let idx = idx.ok_or_else(|| JsValue::from(format!("No Plugin `{name}`")))?;
let result = self.0.borrow_mut().plugin_store.plugins().get(idx).cloned();
Ok(result.unwrap())
}
pub fn is_plugin_activated(&self) -> ApiResult<bool> {
Ok(self
.get_active_plugin()?
.unchecked_ref::<HtmlElement>()
.is_connected())
}
pub async fn restyle_all(&self, view: &perspective_client::View) -> ApiResult<JsValue> {
let plugins = self.get_all_plugins();
let tasks = plugins
.iter()
.map(|plugin| plugin.restyle(view.clone().into()));
join_all(tasks)
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.map(|_| JsValue::UNDEFINED)
}
pub fn set_throttle(&self, val: Option<f64>) {
self.0.borrow_mut().timer.set_throttle(val);
}
pub fn set_selection(&self, window: Option<ViewWindow>) {
self.borrow_mut().selection = window
}
pub fn get_selection(&self) -> Option<ViewWindow> {
self.borrow().selection.clone()
}
pub fn disable_active_plugin_render_warning(&self) {
self.borrow_mut().metadata.render_warning = false;
self.get_active_plugin().unwrap().set_render_warning(false);
}
pub fn get_next_plugin_metadata(
&self,
update: &PluginUpdate,
) -> Option<ViewConfigRequirements> {
let default_plugin_name = PLUGIN_REGISTRY.default_plugin_name();
let name = match update {
PluginUpdate::Missing => return None,
PluginUpdate::SetDefault => default_plugin_name.as_str(),
PluginUpdate::Update(plugin) => plugin,
};
let idx = self.find_plugin_idx(name).expect("f");
let changed = !matches!(
self.0.borrow().plugins_idx,
Some(selected_idx) if selected_idx == idx
);
if changed {
self.borrow_mut().pending_plugin = Some(idx);
self.get_plugin(name)
.and_then(|x| x.get_requirements())
.ok()
} else {
None
}
}
pub fn apply_pending_plugin(&self) -> ApiResult<bool> {
let xxx = self.borrow_mut().pending_plugin.take();
if let Some(idx) = xxx {
let changed = !matches!(
self.0.borrow().plugins_idx,
Some(selected_idx) if selected_idx == idx
);
if changed {
self.borrow_mut().plugins_idx = Some(idx);
let plugin: JsPerspectiveViewerPlugin = self.get_active_plugin()?;
self.borrow_mut().metadata = plugin.get_requirements()?;
self.plugin_changed.emit(plugin);
}
Ok(changed)
} else {
if self.0.borrow().plugins_idx.is_none() {
self.set_plugin(Some(&PLUGIN_REGISTRY.default_plugin_name()))?;
}
Ok(false)
}
}
fn set_plugin(&self, name: Option<&str>) -> ApiResult<bool> {
self.borrow_mut().pending_plugin = None;
let default_plugin_name = PLUGIN_REGISTRY.default_plugin_name();
let name = name.unwrap_or(default_plugin_name.as_str());
let idx = self
.find_plugin_idx(name)
.ok_or_else(|| JsValue::from(format!("Unknown plugin '{name}'")))?;
let changed = !matches!(
self.0.borrow().plugins_idx,
Some(selected_idx) if selected_idx == idx
);
if changed {
self.borrow_mut().plugins_idx = Some(idx);
let plugin: JsPerspectiveViewerPlugin = self.get_active_plugin()?;
self.borrow_mut().metadata = plugin.get_requirements()?;
self.plugin_changed.emit(plugin);
}
Ok(changed)
}
pub async fn with_lock<T>(self, task: impl Future<Output = ApiResult<T>>) -> ApiResult<T> {
let draw_mutex = self.draw_lock();
draw_mutex.lock(task).await
}
pub async fn resize(&self) -> ApiResult<()> {
let draw_mutex = self.draw_lock();
let timer = self.render_timer();
draw_mutex
.debounce(async {
set_timeout(timer.get_throttle()).await?;
let jsplugin = self.get_active_plugin()?;
jsplugin.resize().await?;
Ok(())
})
.await
}
pub async fn resize_with_dimensions(&self, width: f64, height: f64) -> ApiResult<()> {
let draw_mutex = self.draw_lock();
let timer = self.render_timer();
draw_mutex
.debounce(async {
set_timeout(timer.get_throttle()).await?;
let plugin = self.get_active_plugin()?;
let main_panel: &web_sys::HtmlElement = plugin.unchecked_ref();
let rect = main_panel.get_bounding_client_rect();
if (height - rect.height()).abs() > 0.5 || (width - rect.width()).abs() > 0.5 {
let new_width = format!("{}px", width);
let new_height = format!("{}px", height);
main_panel.style().set_property("width", &new_width)?;
main_panel.style().set_property("height", &new_height)?;
let result = plugin.resize().await;
main_panel.style().set_property("width", "")?;
main_panel.style().set_property("height", "")?;
result?;
}
Ok(())
})
.await
}
pub async fn draw(
&self,
session: impl Future<Output = ApiResult<Option<View>>>,
) -> ApiResult<()> {
self.draw_plugin(session, false).await
}
pub async fn update(&self, session: Option<View>) -> ApiResult<()> {
self.draw_plugin(async { Ok(session) }, true).await
}
async fn draw_plugin(
&self,
session: impl Future<Output = ApiResult<Option<View>>>,
is_update: bool,
) -> ApiResult<()> {
let timer = self.render_timer();
let task = async move {
if is_update {
set_timeout(timer.get_throttle()).await?;
}
if let Some(view) = session.await? {
timer.capture_time(self.draw_view(&view, is_update)).await
} else {
tracing::debug!("Render skipped, no `View` attached");
Ok(())
}
};
let draw_mutex = self.draw_lock();
if is_update {
draw_mutex.debounce(task).await
} else {
draw_mutex.lock(task).await
}
}
async fn draw_view(&self, view: &perspective_client::View, is_update: bool) -> ApiResult<()> {
let plugin = self.get_active_plugin()?;
let meta = self.metadata().clone();
let mut limits = get_row_and_col_limits(view, &meta).await?;
limits.is_update = is_update;
if let Some(cb) = self.0.on_render_limits_changed.borrow().as_ref() {
cb.emit(limits);
}
let viewer_elem = &self.0.borrow().viewer_elem.clone();
if is_update {
let task = plugin.update(view.clone().into(), limits.max_cols, limits.max_rows, false);
activate_plugin(viewer_elem, &plugin, task).await?;
} else {
let task = plugin.draw(view.clone().into(), limits.max_cols, limits.max_rows, false);
activate_plugin(viewer_elem, &plugin, task).await?;
}
remove_inactive_plugin(
viewer_elem,
&plugin,
self.plugin_data.borrow_mut().plugin_store.plugins(),
)
}
pub async fn presize(
&self,
open: bool,
panel_task: impl Future<Output = ApiResult<()>>,
) -> ApiResult<JsValue> {
let render_task = self.resize_with_timeout(open);
let result = if open {
panel_task.await?;
render_task.await
} else {
let result = render_task.await;
panel_task.await?;
result
};
match result {
Ok(x) => x,
Err(cont) => {
tracing::warn!("Presize took longer than {}ms", PRESIZE_TIMEOUT);
cont.await.unwrap()
},
}
}
async fn resize_with_timeout(&self, open: bool) -> Result<TaskResult, TimeoutTask<'_>> {
let task = async move {
if open {
self.get_active_plugin()?.resize().await
} else {
self.resize_with_explicit_dimensions().await
}
};
let draw_lock = self.draw_lock();
let tasks: [TimeoutTask<'_>; 2] = [
Box::pin(async move { Some(draw_lock.lock(task).await) }),
Box::pin(async {
set_timeout(PRESIZE_TIMEOUT).await.unwrap();
None
}),
];
let (x, _, y) = select_all(tasks.into_iter()).await;
x.ok_or_else(|| y.into_iter().next().unwrap())
}
async fn resize_with_explicit_dimensions(&self) -> TaskResult {
let plugin = self.get_active_plugin()?;
let main_panel: &web_sys::HtmlElement = plugin.unchecked_ref();
let new_width = format!("{}px", &self.0.borrow().viewer_elem.client_width());
let new_height = format!("{}px", &self.0.borrow().viewer_elem.client_height());
main_panel.style().set_property("width", &new_width)?;
main_panel.style().set_property("height", &new_height)?;
let result = plugin.resize().await;
main_panel.style().set_property("width", "")?;
main_panel.style().set_property("height", "")?;
result
}
fn draw_lock(&self) -> DebounceMutex {
self.draw_lock.clone()
}
pub fn render_timer(&self) -> MovingWindowRenderTimer {
self.0.borrow().timer.clone()
}
fn find_plugin_idx(&self, name: &str) -> Option<usize> {
let short_name = make_short_name(name);
self.0
.borrow_mut()
.plugin_store
.plugins()
.iter()
.position(|elem| make_short_name(&elem.name()).contains(&short_name))
}
}
fn make_short_name(name: &str) -> String {
name.to_lowercase()
.chars()
.filter(|x| x.is_alphabetic())
.collect()
}
impl Renderer {
pub fn to_props(&self, render_limits: Option<RenderLimits>) -> RendererProps {
let has_plugin = self.0.borrow().plugins_idx.is_some();
if has_plugin {
let plugin_name = self.get_active_plugin().ok().map(|p| p.name());
let requirements = self.metadata().clone();
let available_plugins = self
.get_all_plugins()
.into_iter()
.map(|p| p.name())
.collect::<Vec<_>>()
.into();
RendererProps {
plugin_name,
requirements,
render_limits,
available_plugins,
}
} else {
RendererProps {
plugin_name: None,
requirements: ViewConfigRequirements::default(),
render_limits,
available_plugins: PtrEqRc::new(vec![]),
}
}
}
}