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;
251 let origins = workspace.clear_global_filters();
252 clear_master_selections(workspace, origins);
253 let mut tasks = Vec::new();
254 for (id, panel) in workspace
255 .panel_ids()
256 .into_iter()
257 .filter_map(|id| workspace.panel(&id).map(|p| (id, p)))
258 {
259 stamp_global_overlay(workspace, &id, &panel.session);
260 tasks.push(reset_all(
261 &panel.session,
262 &panel.renderer,
263 &ctx.props().presentation,
264 all,
265 ));
266 }
267
268 let run = async move {
269 futures::future::join_all(tasks)
270 .await
271 .into_iter()
272 .collect::<ApiResult<Vec<_>>>()?;
273 Ok(())
274 };
275
276 match completion {
277 Some(completion) => completion.resolve_after(run),
278 None => spawn_owned("reset", run),
279 }
280
281 false
282 },
283 ResetPanel(id, all, sender) => {
284 let id = id.map(crate::workspace::PanelId::from);
293 if let Some(panel) = ctx.props().workspace.panel_or_active(id.as_ref()) {
294 let run = reset_all(
295 &panel.session,
296 &panel.renderer,
297 &ctx.props().presentation,
298 all,
299 );
300
301 match sender {
302 Some(completion) => completion.resolve_after(run),
303 None => spawn_owned("reset-panel", run),
304 }
305 }
306
307 false
308 },
309
310 LayoutChanged => self.on_layout_changed(ctx),
312 SetActivePanel(id, completion) => self.on_set_active_panel(ctx, id, completion),
313 ClosePanel(id, completion) => self.on_close_panel(ctx, id, completion),
314 PanelClosed(id) => self.on_panel_closed(ctx, id),
315 CommitWorkspaceRestore(id) => self.on_commit_workspace_restore(ctx, id),
316 DuplicatePanel(id) => self.on_duplicate_panel(ctx, id),
317 NewPanel(id) => self.on_new_panel(ctx, id),
318 NewPanelFrom { client, table } => self.on_new_panel_from(ctx, client, table),
319
320 ToggleMaster(id) => self.on_toggle_master(ctx, id),
322 MasterContribution(panel, selection) => {
323 self.on_master_contribution(ctx, panel, selection)
324 },
325 RemoveGlobalFilter(index) => self.on_remove_global_filter(ctx, index),
326 ClearGlobalFilters => self.on_clear_global_filters(ctx),
327
328 ToggleSettingsInit(update, announce, resolve) => {
330 self.on_toggle_settings_init(ctx, update, announce, resolve)
331 },
332 ToggleSettingsComplete(update, resolve) => {
333 self.on_toggle_settings_complete(ctx, update, resolve)
334 },
335 DividerMove(divider, target) => self.on_divider_move(ctx, divider, target),
336 DividerPump(divider) => self.on_divider_pump(ctx, divider),
337 DividerCommit(divider, target) => self.on_divider_commit(ctx, divider, target),
338 DividerFinish(divider) => self.on_divider_finish(ctx, divider),
339 SettingsPanelTabChanged(tab) => self.on_settings_panel_tab_changed(tab),
340 SettingsPanelAutoWidth(w) => self.on_settings_panel_auto_width(ctx, w),
341 OpenColumnSettings {
342 target,
343 sender,
344 toggle,
345 } => self.on_open_column_settings(ctx, target, sender, toggle),
346 ColumnSettingsPanelAutoWidth(w) => self.on_column_settings_panel_auto_width(ctx, w),
347 ToggleColumnSettingsPin => self.on_toggle_column_settings_pin(ctx),
348 ToggleColumnSettingsPinComplete(resolve) => {
349 self.on_toggle_column_settings_pin_complete(resolve)
350 },
351 ColumnSettingsTabChanged(tab) => self.on_column_settings_tab_changed(ctx, tab),
352 ToggleDebug => self.on_toggle_debug(ctx),
353
354 UpdateSession(props) => self.on_update_session(ctx, *props),
356 UpdateSessionStats(stats, has_table) => self.on_update_session_stats(stats, has_table),
357 UpdateGlobalFilters => self.on_update_global_filters(ctx),
358 UpdateRenderer(props) => self.on_update_renderer(*props),
359 UpdatePresentation(props) => self.on_update_presentation(ctx, *props),
360 UpdateSettingsOpen(open) => self.on_update_settings_open(open),
361 UpdateIsWorkspace(is_workspace) => self.on_update_is_workspace(is_workspace),
362 UpdateColumnSettings(ocs) => self.on_update_column_settings(ctx, *ocs),
363 UpdateColumnSettingsCommit(resolve) => self.on_update_column_settings_commit(resolve),
364 UpdateDragDrop(props) => self.on_update_dragdrop(*props),
365 UpdateInFlight(count) => self.on_update_in_flight(count),
366 }
367 }
368
369 fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
373 true
374 }
375
376 fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
379 if !self.master_listeners_attached {
382 let _ = ctx.props().elem.add_event_listener_with_callback(
383 "perspective-global-filter",
384 self._global_filter_listener.as_ref().unchecked_ref(),
385 );
386 let _ = ctx.props().elem.add_event_listener_with_callback(
387 "perspective-click",
388 self._master_click_listener.as_ref().unchecked_ref(),
389 );
390 self.master_listeners_attached = true;
391 }
392
393 if !self.on_rendered.is_empty()
394 && matches!(self.fonts.get_status(), FontLoaderStatus::Finished)
395 {
396 for resolve in self.on_rendered.drain(..) {
397 if resolve.send(()).is_err() {
398 tracing::warn!("Orphan render");
399 }
400 }
401 }
402 }
403
404 fn view(&self, ctx: &Context<Self>) -> Html {
405 self.render(ctx)
406 }
407
408 fn destroy(&mut self, _ctx: &Context<Self>) {}
409}