use std::rc::Rc;
use futures::FutureExt;
use perspective_client::config::ViewConfigUpdate;
use perspective_client::{View, clone};
use perspective_js::utils::*;
use wasm_bindgen::JsValue;
use yew::prelude::*;
use super::transactional_restore::{commit_edit, prepare_edit};
use crate::presentation::Presentation;
use crate::renderer::{RenderContext, Renderer};
use crate::session::{BindDisposition, BindingEffects, Disposal, EditDelta, OpKind, Session};
use crate::utils::RenderGuard;
pub async fn bind_snapshot(
guard: &RenderGuard,
session: &Session,
renderer: &Renderer,
) -> ApiResult<(BindDisposition, Option<crate::renderer::ContextPin>)> {
let snap = session.snapshot(guard);
let validated = session.validate_snapshot(guard, snap).await?;
let disposition = session.bind_view(guard, validated).await?;
let pin = if let Some(view) = disposition.view() {
let ctx = Rc::new(build_render_context(session, renderer, view)?);
renderer.set_cached_context(ctx.clone());
Some(renderer.pin_context(guard, ctx))
} else {
None
};
Ok((disposition, pin))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RunOrigin {
Public,
Internal,
}
pub async fn dispatch_bound(
guard: &RenderGuard,
renderer: &Renderer,
disposition: BindDisposition,
plugin_state_changed: bool,
origin: RunOrigin,
) -> ApiResult<()> {
match disposition {
BindDisposition::Rebuilt(fresh) => renderer.draw_fresh(guard, fresh).await,
BindDisposition::Adopted(view) => match renderer.promote_first_paint(&view) {
Some(fresh) => renderer.draw_fresh(guard, fresh).await,
None => renderer.update_bound(guard, &view).await,
},
BindDisposition::Unchanged(view) => match renderer.promote_first_paint(&view) {
Some(fresh) => renderer.draw_fresh(guard, fresh).await,
None if plugin_state_changed || origin == RunOrigin::Public => {
renderer.update_bound(guard, &view).await
},
None => {
tracing::debug!("Render skipped, reconciled no-op (internal)");
Ok(())
},
},
BindDisposition::Deferred => {
tracing::debug!("Render skipped, no `View` attached");
Ok(())
},
}
}
fn build_render_context(
session: &Session,
renderer: &Renderer,
view: &View,
) -> ApiResult<RenderContext> {
Ok(RenderContext {
view_config: session
.get_rendered_view_config()
.ok_or("No bound `View`")?,
view: view.clone(),
table: session.get_table().ok_or("No `Table` set")?,
client: session.get_client().ok_or("No `Client` set")?,
edit_port: session.metadata().get_edit_port(),
theme: renderer.theme(),
})
}
pub fn apply_and_render(
session: &Session,
renderer: &Renderer,
update: ViewConfigUpdate,
) -> ApiResult<ApiFuture<()>> {
update_plugin_and_render(session, renderer, update, None)
}
pub fn update_plugin_and_render(
session: &Session,
renderer: &Renderer,
update: ViewConfigUpdate,
plugin_idx: Option<usize>,
) -> ApiResult<ApiFuture<()>> {
session.check_edit(&update)?;
let kind = OpKind::Edit {
delta: EditDelta::View(Box::new(update.clone())),
swaps_plugin: plugin_idx.is_some(),
};
let ticket = session.submit(kind, {
clone!(session, renderer);
move |_ctx| {
Box::pin(async move {
let prepared = prepare_edit(&session, &renderer, update, plugin_idx).await?;
let committed = commit_edit(&session, &renderer, prepared);
Ok(Some(
render_run(session, renderer, RunCommit::Done(committed)).boxed_local(),
))
})
}
});
Ok(ApiFuture::new(ticket.settle()))
}
pub fn just_render(session: &Session, renderer: &Renderer) -> ApiResult<ApiFuture<()>> {
clone!(session, renderer);
Ok(ApiFuture::new(async move {
render_run(session, renderer, RunCommit::None).await
}))
}
pub(crate) struct Committed {
pub activate: bool,
pub plugin_swapped: bool,
pub plugin_config_changed: bool,
pub columns_config_changed: bool,
}
pub(crate) enum RunCommit {
None,
Done(Committed),
Deferred(Box<dyn FnOnce() -> (Committed, BindingEffects)>),
}
pub(crate) struct RunSpec {
pub origin: RunOrigin,
pub commit: RunCommit,
pub presentation: Option<Presentation>,
}
impl Default for RunSpec {
fn default() -> Self {
Self {
origin: RunOrigin::Internal,
commit: RunCommit::None,
presentation: None,
}
}
}
pub(crate) async fn locked_run(
session: &Session,
renderer: &Renderer,
spec: RunSpec,
) -> ApiResult<()> {
clone!(session, renderer);
renderer
.clone()
.render_task(|guard| run_locked(guard, session, renderer, spec))
.await
}
pub(crate) async fn run_locked(
guard: RenderGuard,
session: Session,
renderer: Renderer,
spec: RunSpec,
) -> ApiResult<()> {
{
{
if let Some(disposal) = session.disposal() {
return match (disposal, spec.origin) {
(Disposal::Reject, RunOrigin::Public) => Err(ApiError::new("Panel disposed")),
_ => Ok(()),
};
}
let committed = match spec.commit {
RunCommit::Done(committed) => Some(committed),
RunCommit::Deferred(commit) => {
let (committed, binding) = commit();
let rebound = binding.rebound();
session.finish_binding(binding).await?;
if rebound
&& session.get_table().is_none()
&& let Some(plugin) = renderer.active_plugin()
{
plugin.clear().await?;
}
Some(committed)
},
RunCommit::None => None,
};
if !committed.as_ref().is_some_and(|c| c.activate) {
renderer.mount_active_plugin()?;
}
let plugin_swapped = match &committed {
Some(committed) => {
if committed.activate {
renderer.activate_committed_plugin()?;
}
committed.plugin_swapped
},
None => {
renderer.ensure_plugin_selected()?;
false
},
};
let plugin = renderer.active_plugin().ok_or("No Plugin")?;
renderer.stamp_theme(Some(&plugin));
let blocking = session
.get_error()
.or_else(|| renderer.failure().map(|x| x.0));
if let Some(error) = blocking {
return match spec.origin {
RunOrigin::Public => Err(error),
RunOrigin::Internal => Ok(()),
};
}
let (disposition, _pin) = if session.get_table().is_some() {
bind_snapshot(&guard, &session, &renderer).await?
} else {
(BindDisposition::Deferred, None)
};
let view_config_snapshot = session.committed_view_config().clone();
let changed = match committed {
Some(committed) => {
let changed =
committed.plugin_config_changed || committed.columns_config_changed;
if changed || plugin_swapped {
let plugin_config = renderer.committed_plugin_config();
let plugin_update =
JsValue::from_serde_ext(&plugin_config).unwrap_or(JsValue::NULL);
let columns_config = renderer
.all_columns_configs_materialized(&view_config_snapshot, &session)
.await;
plugin.restore(&plugin_update, Some(&columns_config))?;
if committed.columns_config_changed {
renderer.columns_config_changed.emit(columns_config);
}
if committed.plugin_config_changed {
renderer.plugin_config_changed.emit(plugin_config);
}
}
changed
},
None => false,
};
if spec
.presentation
.as_ref()
.map(|p| p.is_visible())
.unwrap_or(true)
{
dispatch_bound(&guard, &renderer, disposition, changed, spec.origin).await?;
}
renderer.landed();
Ok(())
}
}
}
pub(super) async fn render_run(
session: Session,
renderer: Renderer,
commit: RunCommit,
) -> ApiResult<()> {
let spec = RunSpec {
commit,
..RunSpec::default()
};
match locked_run(&session, &renderer, spec)
.await
.ignore_view_delete()
{
Err(e) => renderer.fail(e),
Ok(_) => Ok(()),
}
}
pub async fn activation_render(session: Session, renderer: Renderer) -> ApiResult<()> {
let result = {
clone!(session, renderer);
renderer
.clone()
.render_task(|guard| async move {
match session.get_view() {
Some(view) => {
let _pin = renderer
.cached_context()
.map(|ctx| renderer.pin_context(&guard, ctx));
if renderer.take_data_stale() {
renderer.update_bound(&guard, &view).await
} else {
renderer.activation_repaint(&guard).await
}
},
None => Ok(()),
}
})
.await
};
result.ignore_view_delete().map(|_| ())
}
pub fn resize_callback(session: &Session, renderer: &Renderer) -> Callback<()> {
clone!(session, renderer);
Callback::from(move |_| {
clone!(renderer, session);
crate::utils::spawn_owned("resize-callback", async move {
if !renderer.is_plugin_activated()? {
just_render(&session, &renderer)?.await
} else {
renderer.resize().await
}
});
})
}