1mod filters;
23mod msg;
24mod panels;
25mod render;
26mod settings;
27mod snapshots;
28mod wiring;
29
30use std::collections::HashMap;
31use std::rc::Rc;
32
33use futures::channel::oneshot::Sender;
34use perspective_client::config::Filter;
35use perspective_js::utils::*;
36use wasm_bindgen::JsCast;
37use wasm_bindgen::prelude::*;
38use yew::prelude::*;
39
40pub use self::msg::PerspectiveViewerMsg;
41use self::msg::PerspectiveViewerMsg::*;
42use self::settings::SettingsGeometry;
43use self::wiring::*;
44use crate::presentation::{Presentation, PresentationProps};
45use crate::renderer::{RendererProps, *};
46use crate::session::{SessionProps, *};
47use crate::tasks::*;
48use crate::ui::{FontLoaderProps, FontLoaderStatus};
49use crate::utils::*;
50use crate::workspace::{PanelId, Workspace};
51
52#[derive(Clone, Properties)]
53pub struct PerspectiveViewerProps {
54 pub elem: web_sys::HtmlElement,
56
57 pub presentation: Presentation,
59
60 pub workspace: Workspace,
64}
65
66impl PartialEq for PerspectiveViewerProps {
67 fn eq(&self, _rhs: &Self) -> bool {
68 false
69 }
70}
71
72pub struct PerspectiveViewer {
73 _subscriptions: Vec<Subscription>,
74
75 _active_subscriptions: Vec<Subscription>,
77
78 _title_subscriptions: Vec<Subscription>,
82
83 active_session: Session,
87 active_renderer: Renderer,
88
89 empty_session: Session,
94 empty_renderer: Renderer,
95 debug_open: bool,
96 fonts: FontLoaderProps,
97 on_close_column_settings: Callback<()>,
98 on_rendered: Vec<Sender<()>>,
99 on_resize: Rc<PubSub<()>>,
100 on_settings_panel_dimensions_reset: Rc<PubSub<()>>,
101 settings_open: bool,
102
103 pending_closes: HashMap<PanelId, Completion>,
106
107 global_filters: Vec<Filter>,
112
113 settings_geometry: SettingsGeometry,
117
118 session_props: SessionProps,
122 renderer_props: RendererProps,
123 presentation_props: PresentationProps,
124 dragdrop_props: crate::presentation::DragDropProps,
125
126 update_count: u32,
131
132 _shift_listeners: ShiftListeners,
137
138 _global_filter_listener: Closure<dyn FnMut(web_sys::Event)>,
142 _master_click_listener: Closure<dyn FnMut(web_sys::Event)>,
143 master_listeners_attached: bool,
144}
145
146impl Component for PerspectiveViewer {
147 type Message = PerspectiveViewerMsg;
148 type Properties = PerspectiveViewerProps;
149
150 fn create(ctx: &Context<Self>) -> Self {
151 let elem = ctx.props().elem.clone();
152 let fonts = FontLoaderProps::new(&elem, ctx.link().callback(|()| PreloadFontsUpdate));
153 let empty_session = Session::new();
154 let empty_renderer = Renderer::new(&elem);
155 let active_session = ctx
156 .props()
157 .workspace
158 .active_session()
159 .unwrap_or_else(|| empty_session.clone());
160 let active_renderer = ctx
161 .props()
162 .workspace
163 .active_renderer()
164 .unwrap_or_else(|| empty_renderer.clone());
165 inject_shared_callbacks(ctx);
166 inject_active_callbacks(ctx, &active_session, &active_renderer);
167 let subscriptions = create_shared_subscriptions(ctx);
168 let active_subscriptions =
169 create_active_subscriptions(ctx, &active_session, &active_renderer);
170 let session_props = active_session.to_props();
171 let renderer_props = active_renderer.to_props(None);
172 let presentation_props = ctx.props().presentation.to_props(PtrEqRc::new(vec![]));
173 let on_close_column_settings = ctx.link().callback(|_| OpenColumnSettings {
174 target: None,
175 sender: None,
176 toggle: false,
177 });
178
179 {
183 let presentation = ctx.props().presentation.clone();
184 let cb = ctx.link().callback(move |themes: PtrEqRc<Vec<String>>| {
185 UpdatePresentation(Box::new(presentation.to_props(themes)))
186 });
187
188 let presentation = ctx.props().presentation.clone();
189 ApiFuture::spawn(async move {
190 let themes = presentation.get_available_themes().await?;
191 cb.emit(themes);
192 Ok(())
193 });
194 }
195
196 let shift_listeners = install_shift_listeners(elem);
197 let global_filter_listener = wiring::global_filter_listener(ctx);
198 let master_click_listener = wiring::master_click_listener(ctx);
199
200 Self {
201 _subscriptions: subscriptions,
202 _global_filter_listener: global_filter_listener,
203 _master_click_listener: master_click_listener,
204 master_listeners_attached: false,
205 _active_subscriptions: active_subscriptions,
206 _title_subscriptions: subscribe_panel_titles(ctx),
207 active_session,
208 active_renderer,
209 empty_session,
210 empty_renderer,
211 debug_open: false,
212 fonts,
213 on_close_column_settings,
214 on_rendered: Vec::new(),
215 on_resize: Default::default(),
216 on_settings_panel_dimensions_reset: Default::default(),
217 settings_open: false,
218 pending_closes: HashMap::new(),
219 global_filters: Vec::new(),
220 settings_geometry: Default::default(),
221 session_props,
222 renderer_props,
223 presentation_props,
224 dragdrop_props: Default::default(),
225 update_count: 0,
226 _shift_listeners: shift_listeners,
227 }
228 }
229
230 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
233 match msg {
234 PreloadFontsUpdate => true,
235 TitlesChanged => true,
236 Resize => {
237 self.on_resize.emit(());
238 false
239 },
240 Reset(all, completion) => {
241 let workspace = ctx.props().workspace.clone();
254 let presentation = ctx.props().presentation.clone();
255 let origins = workspace.clear_global_filters();
256 clear_master_selections(&workspace, origins);
257 let effect = workspace.effects().guard();
258 let run = async move {
259 let _effect = effect;
260 if all {
261 crate::tasks::purge_derived(&workspace, &presentation).await?;
262 }
263
264 let mut tasks = Vec::new();
265 for (id, panel) in workspace
266 .panel_ids()
267 .into_iter()
268 .filter_map(|id| workspace.panel(&id).map(|p| (id, p)))
269 {
270 let _ = id;
271 drop(broadcast_overlay(&workspace, &panel));
272 tasks.push(reset_all(
273 &panel.session,
274 &panel.renderer,
275 &presentation,
276 all,
277 ));
278 }
279
280 futures::future::join_all(tasks)
281 .await
282 .into_iter()
283 .collect::<ApiResult<Vec<_>>>()?;
284 Ok(())
285 };
286
287 match completion {
288 Some(completion) => completion.resolve_after(run),
289 None => spawn_owned("reset", run),
290 }
291
292 false
293 },
294 ResetPanel(id, all, sender) => {
295 let id = id.map(crate::workspace::PanelId::from);
307 if let Some(panel) = ctx.props().workspace.panel_or_active(id.as_ref()) {
308 let workspace = ctx.props().workspace.clone();
309 let presentation = ctx.props().presentation.clone();
310 let effect = workspace.effects().guard();
311 let run = async move {
312 let _effect = effect;
313 if all {
314 crate::tasks::purge_derived(&workspace, &presentation).await?;
315 }
316
317 reset_all(&panel.session, &panel.renderer, &presentation, all).await
318 };
319
320 match sender {
321 Some(completion) => completion.resolve_after(run),
322 None => spawn_owned("reset-panel", run),
323 }
324 }
325
326 false
327 },
328
329 LayoutChanged => self.on_layout_changed(ctx),
331 SetActivePanel(id, completion) => self.on_set_active_panel(ctx, id, completion),
332 ClosePanel(id, completion) => self.on_close_panel(ctx, id, completion),
333 PanelClosed(id) => self.on_panel_closed(ctx, id),
334 CommitWorkspaceRestore(id) => self.on_commit_workspace_restore(ctx, id),
335 DuplicatePanel(id) => self.on_duplicate_panel(ctx, id),
336 DerivePanel(id) => self.on_derive_panel(ctx, id),
337 NewPanel(id) => self.on_new_panel(ctx, id),
338 NewPanelFrom { client, table } => self.on_new_panel_from(ctx, client, table),
339
340 ToggleMaster(id) => self.on_toggle_master(ctx, id),
342 MasterContribution(panel, selection) => {
343 self.on_master_contribution(ctx, panel, selection)
344 },
345 RemoveGlobalFilter(index) => self.on_remove_global_filter(ctx, index),
346 ClearGlobalFilters => self.on_clear_global_filters(ctx),
347
348 ToggleSettingsInit(update, announce, resolve) => {
350 self.on_toggle_settings_init(ctx, update, announce, resolve)
351 },
352 ToggleSettingsComplete(update, resolve) => {
353 self.on_toggle_settings_complete(ctx, update, resolve)
354 },
355 DividerMove(divider, target) => self.on_divider_move(ctx, divider, target),
356 DividerPump(divider) => self.on_divider_pump(ctx, divider),
357 DividerCommit(divider, target) => self.on_divider_commit(ctx, divider, target),
358 DividerFinish(divider) => self.on_divider_finish(ctx, divider),
359 SettingsPanelTabChanged(tab) => self.on_settings_panel_tab_changed(tab),
360 SettingsPanelAutoWidth(w) => self.on_settings_panel_auto_width(ctx, w),
361 OpenColumnSettings {
362 target,
363 sender,
364 toggle,
365 } => self.on_open_column_settings(ctx, target, sender, toggle),
366 ColumnSettingsPanelAutoWidth(w) => self.on_column_settings_panel_auto_width(ctx, w),
367 ToggleColumnSettingsPin => self.on_toggle_column_settings_pin(ctx),
368 ToggleColumnSettingsPinComplete(resolve) => {
369 self.on_toggle_column_settings_pin_complete(resolve)
370 },
371 ColumnSettingsTabChanged(tab) => self.on_column_settings_tab_changed(ctx, tab),
372 ToggleDebug => self.on_toggle_debug(ctx),
373
374 UpdateSession(props) => self.on_update_session(ctx, *props),
376 UpdateSessionStats(stats, has_table) => self.on_update_session_stats(stats, has_table),
377 UpdateGlobalFilters => self.on_update_global_filters(ctx),
378 UpdateRenderer(props) => self.on_update_renderer(*props),
379 UpdatePresentation(props) => self.on_update_presentation(ctx, *props),
380 UpdateSettingsOpen(open) => self.on_update_settings_open(open),
381 UpdateIsWorkspace(is_workspace) => self.on_update_is_workspace(is_workspace),
382 UpdateColumnSettings(ocs) => self.on_update_column_settings(ctx, *ocs),
383 UpdateColumnSettingsCommit(resolve) => self.on_update_column_settings_commit(resolve),
384 UpdateDragDrop(props) => self.on_update_dragdrop(*props),
385 UpdateInFlight(count) => self.on_update_in_flight(count),
386 }
387 }
388
389 fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
393 true
394 }
395
396 fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
399 if !self.master_listeners_attached {
402 let _ = ctx.props().elem.add_event_listener_with_callback(
403 "perspective-global-filter",
404 self._global_filter_listener.as_ref().unchecked_ref(),
405 );
406 let _ = ctx.props().elem.add_event_listener_with_callback(
407 "perspective-click",
408 self._master_click_listener.as_ref().unchecked_ref(),
409 );
410 self.master_listeners_attached = true;
411 }
412
413 if !self.on_rendered.is_empty()
414 && matches!(self.fonts.get_status(), FontLoaderStatus::Finished)
415 {
416 for resolve in self.on_rendered.drain(..) {
417 if resolve.send(()).is_err() {
418 tracing::warn!("Orphan render");
419 }
420 }
421 }
422 }
423
424 fn view(&self, ctx: &Context<Self>) -> Html {
425 self.render(ctx)
426 }
427
428 fn destroy(&mut self, _ctx: &Context<Self>) {}
429}