1mod filters;
23mod msg;
24mod panels;
25mod render;
26mod settings;
27mod snapshots;
28mod wiring;
29
30use std::rc::Rc;
31
32use futures::channel::oneshot::Sender;
33use perspective_client::config::Filter;
34use perspective_js::utils::*;
35use wasm_bindgen::JsCast;
36use wasm_bindgen::prelude::*;
37use yew::prelude::*;
38
39pub use self::msg::PerspectiveViewerMsg;
40use self::msg::PerspectiveViewerMsg::*;
41use self::settings::SettingsGeometry;
42use self::wiring::*;
43use super::font_loader::{FontLoaderProps, FontLoaderStatus};
44use crate::presentation::{Presentation, PresentationProps};
45use crate::renderer::{RendererProps, *};
46use crate::session::{SessionProps, *};
47use crate::tasks::*;
48use crate::utils::*;
49use crate::workspace::Workspace;
50
51#[derive(Clone, Properties)]
52pub struct PerspectiveViewerProps {
53 pub elem: web_sys::HtmlElement,
55
56 pub presentation: Presentation,
58
59 pub workspace: Workspace,
63}
64
65impl PartialEq for PerspectiveViewerProps {
66 fn eq(&self, _rhs: &Self) -> bool {
67 false
68 }
69}
70
71pub struct PerspectiveViewer {
72 _subscriptions: Vec<Subscription>,
73
74 _active_subscriptions: Vec<Subscription>,
76
77 _title_subscriptions: Vec<Subscription>,
81
82 active_session: Session,
86 active_renderer: Renderer,
87
88 empty_session: Session,
93 empty_renderer: Renderer,
94 debug_open: bool,
95 fonts: FontLoaderProps,
96 on_close_column_settings: Callback<()>,
97 on_rendered: Vec<Sender<()>>,
98 on_resize: Rc<PubSub<()>>,
99 on_settings_panel_dimensions_reset: Rc<PubSub<()>>,
100 settings_open: bool,
101
102 global_filters: Vec<Filter>,
107
108 settings_geometry: SettingsGeometry,
112
113 session_props: SessionProps,
117 renderer_props: RendererProps,
118 presentation_props: PresentationProps,
119 dragdrop_props: crate::presentation::DragDropProps,
120
121 update_count: u32,
126
127 _shift_listeners: ShiftListeners,
132
133 _global_filter_listener: Closure<dyn FnMut(web_sys::Event)>,
137 _master_click_listener: Closure<dyn FnMut(web_sys::Event)>,
138 master_listeners_attached: bool,
139}
140
141impl Component for PerspectiveViewer {
142 type Message = PerspectiveViewerMsg;
143 type Properties = PerspectiveViewerProps;
144
145 fn create(ctx: &Context<Self>) -> Self {
146 let elem = ctx.props().elem.clone();
147 let fonts = FontLoaderProps::new(&elem, ctx.link().callback(|()| PreloadFontsUpdate));
148 let empty_session = Session::new();
149 let empty_renderer = Renderer::new(&elem);
150 let active_session = ctx
151 .props()
152 .workspace
153 .active_session()
154 .unwrap_or_else(|| empty_session.clone());
155 let active_renderer = ctx
156 .props()
157 .workspace
158 .active_renderer()
159 .unwrap_or_else(|| empty_renderer.clone());
160 inject_shared_callbacks(ctx);
161 inject_active_callbacks(ctx, &active_session, &active_renderer);
162 let subscriptions = create_shared_subscriptions(ctx);
163 let active_subscriptions =
164 create_active_subscriptions(ctx, &active_session, &active_renderer);
165 let session_props = active_session.to_props();
166 let renderer_props = active_renderer.to_props(None);
167 let presentation_props = ctx.props().presentation.to_props(PtrEqRc::new(vec![]));
168 let on_close_column_settings = ctx.link().callback(|_| OpenColumnSettings {
169 locator: None,
170 sender: None,
171 toggle: false,
172 });
173
174 {
178 let presentation = ctx.props().presentation.clone();
179 let cb = ctx.link().callback(move |themes: PtrEqRc<Vec<String>>| {
180 UpdatePresentation(Box::new(presentation.to_props(themes)))
181 });
182
183 let presentation = ctx.props().presentation.clone();
184 ApiFuture::spawn(async move {
185 let themes = presentation.get_available_themes().await?;
186 cb.emit(themes);
187 Ok(())
188 });
189 }
190
191 let shift_listeners = install_shift_listeners(elem);
192 let global_filter_listener = wiring::global_filter_listener(ctx);
193 let master_click_listener = wiring::master_click_listener(ctx);
194
195 Self {
196 _subscriptions: subscriptions,
197 _global_filter_listener: global_filter_listener,
198 _master_click_listener: master_click_listener,
199 master_listeners_attached: false,
200 _active_subscriptions: active_subscriptions,
201 _title_subscriptions: subscribe_panel_titles(ctx),
202 active_session,
203 active_renderer,
204 empty_session,
205 empty_renderer,
206 debug_open: false,
207 fonts,
208 on_close_column_settings,
209 on_rendered: Vec::new(),
210 on_resize: Default::default(),
211 on_settings_panel_dimensions_reset: Default::default(),
212 settings_open: false,
213 global_filters: Vec::new(),
214 settings_geometry: Default::default(),
215 session_props,
216 renderer_props,
217 presentation_props,
218 dragdrop_props: Default::default(),
219 update_count: 0,
220 _shift_listeners: shift_listeners,
221 }
222 }
223
224 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
227 match msg {
228 PreloadFontsUpdate => true,
229 TitlesChanged => true,
230 Resize => {
231 self.on_resize.emit(());
232 false
233 },
234 Reset(all, completion) => {
235 let workspace = &ctx.props().workspace;
245 let origins = workspace.clear_global_filters();
246 clear_master_selections(workspace, origins);
247 let mut tasks = Vec::new();
248 for (id, panel) in workspace
249 .panel_ids()
250 .into_iter()
251 .filter_map(|id| workspace.panel(&id).map(|p| (id, p)))
252 {
253 stamp_global_overlay(workspace, &id, &panel.session);
254 tasks.push(reset_all(
255 &panel.session,
256 &panel.renderer,
257 &ctx.props().presentation,
258 all,
259 ));
260 }
261
262 let run = async move {
263 futures::future::join_all(tasks)
264 .await
265 .into_iter()
266 .collect::<ApiResult<Vec<_>>>()?;
267 Ok(())
268 };
269
270 match completion {
271 Some(completion) => completion.resolve_after(run),
272 None => spawn_owned("reset", run),
273 }
274
275 false
276 },
277 ResetPanel(id, all, sender) => {
278 let id = id.map(crate::workspace::PanelId::from);
287 if let Some(panel) = ctx.props().workspace.panel_or_active(id.as_ref()) {
288 let run = reset_all(
289 &panel.session,
290 &panel.renderer,
291 &ctx.props().presentation,
292 all,
293 );
294
295 match sender {
296 Some(completion) => completion.resolve_after(run),
297 None => spawn_owned("reset-panel", run),
298 }
299 }
300
301 false
302 },
303
304 LayoutChanged => self.on_layout_changed(ctx),
306 SetActivePanel(id, completion) => self.on_set_active_panel(ctx, id, completion),
307 ClosePanel(id, completion) => self.on_close_panel(ctx, id, completion),
308 CommitWorkspaceRestore(id) => self.on_commit_workspace_restore(ctx, id),
309 DuplicatePanel(id) => self.on_duplicate_panel(ctx, id),
310 NewPanel(id) => self.on_new_panel(ctx, id),
311 NewPanelFrom { client, table } => self.on_new_panel_from(ctx, client, table),
312
313 ToggleMaster(id) => self.on_toggle_master(ctx, id),
315 MasterContribution(panel, selection) => {
316 self.on_master_contribution(ctx, panel, selection)
317 },
318 RemoveGlobalFilter(index) => self.on_remove_global_filter(ctx, index),
319 ClearGlobalFilters => self.on_clear_global_filters(ctx),
320
321 ToggleSettingsInit(update, resolve) => {
323 self.on_toggle_settings_init(ctx, update, resolve)
324 },
325 ToggleSettingsComplete(update, resolve) => {
326 self.on_toggle_settings_complete(ctx, update, resolve)
327 },
328 SettingsPanelSizeUpdate(x) => self.on_settings_panel_size_update(x),
329 SettingsDividerMove(w) => self.on_settings_divider_move(ctx, w),
330 SettingsDividerPump => self.on_settings_divider_pump(ctx),
331 SettingsDividerCommit(w) => self.on_settings_divider_commit(ctx, w),
332 SettingsDividerFinish => self.on_settings_divider_finish(ctx),
333 SettingsPanelTabChanged(tab) => self.on_settings_panel_tab_changed(tab),
334 SettingsPanelAutoWidth(w) => self.on_settings_panel_auto_width(w),
335 OpenColumnSettings {
336 locator,
337 sender,
338 toggle,
339 } => self.on_open_column_settings(ctx, locator, sender, toggle),
340 ColumnSettingsPanelSizeUpdate(x) => self.on_column_settings_panel_size_update(x),
341 ColumnSettingsTabChanged(tab) => self.on_column_settings_tab_changed(ctx, tab),
342 ToggleDebug => self.on_toggle_debug(ctx),
343
344 UpdateSession(props) => self.on_update_session(*props),
346 UpdateSessionStats(stats, has_table) => self.on_update_session_stats(stats, has_table),
347 UpdateGlobalFilters => self.on_update_global_filters(ctx),
348 UpdateRenderer(props) => self.on_update_renderer(*props),
349 UpdatePresentation(props) => self.on_update_presentation(ctx, *props),
350 UpdateSettingsOpen(open) => self.on_update_settings_open(open),
351 UpdateIsWorkspace(is_workspace) => self.on_update_is_workspace(is_workspace),
352 UpdateColumnSettings(ocs) => self.on_update_column_settings(*ocs),
353 UpdateDragDrop(props) => self.on_update_dragdrop(*props),
354 UpdateInFlight(count) => self.on_update_in_flight(count),
355 }
356 }
357
358 fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
362 true
363 }
364
365 fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
368 if !self.master_listeners_attached {
371 let _ = ctx.props().elem.add_event_listener_with_callback(
372 "perspective-global-filter",
373 self._global_filter_listener.as_ref().unchecked_ref(),
374 );
375 let _ = ctx.props().elem.add_event_listener_with_callback(
376 "perspective-click",
377 self._master_click_listener.as_ref().unchecked_ref(),
378 );
379 self.master_listeners_attached = true;
380 }
381
382 if !self.on_rendered.is_empty()
383 && matches!(self.fonts.get_status(), FontLoaderStatus::Finished)
384 {
385 for resolve in self.on_rendered.drain(..) {
386 if resolve.send(()).is_err() {
387 tracing::warn!("Orphan render");
388 }
389 }
390 }
391 }
392
393 fn view(&self, ctx: &Context<Self>) -> Html {
394 self.render(ctx)
395 }
396
397 fn destroy(&mut self, _ctx: &Context<Self>) {}
398}