perspective_viewer/components/
viewer.rs1use std::rc::Rc;
14
15use futures::channel::oneshot::*;
16use perspective_js::utils::*;
17use wasm_bindgen::prelude::*;
18use yew::prelude::*;
19
20use super::containers::split_panel::SplitPanel;
21use super::font_loader::{FontLoader, FontLoaderProps, FontLoaderStatus};
22use super::form::debug::DebugPanel;
23use super::style::{LocalStyle, StyleProvider};
24use crate::components::column_settings_sidebar::ColumnSettingsPanel;
25use crate::components::main_panel::MainPanel;
26use crate::components::settings_panel::SettingsPanel;
27use crate::config::*;
28use crate::custom_events::CustomEvents;
29use crate::dragdrop::*;
30use crate::model::*;
31use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation};
32use crate::renderer::*;
33use crate::session::*;
34use crate::utils::*;
35use crate::{PerspectiveProperties, css};
36
37#[derive(Clone, Properties, PerspectiveProperties!)]
38pub struct PerspectiveViewerProps {
39 pub elem: web_sys::HtmlElement,
41
42 pub custom_events: CustomEvents,
44 pub dragdrop: DragDrop,
45 pub session: Session,
46 pub renderer: Renderer,
47 pub presentation: Presentation,
48}
49
50impl PartialEq for PerspectiveViewerProps {
51 fn eq(&self, _rhs: &Self) -> bool {
52 false
53 }
54}
55
56impl PerspectiveViewerProps {
57 fn is_title(&self) -> bool {
58 self.session.get_title().is_some()
59 }
60}
61
62#[derive(Debug)]
63pub enum PerspectiveViewerMsg {
64 ColumnSettingsPanelSizeUpdate(Option<i32>),
65 ColumnSettingsTabChanged(ColumnSettingsTab),
66 OpenColumnSettings {
67 locator: Option<ColumnLocator>,
68 sender: Option<Sender<()>>,
69 toggle: bool,
70 },
71 PreloadFontsUpdate,
72 Reset(bool, Option<Sender<()>>),
73 Resize,
74 SettingsPanelSizeUpdate(Option<i32>),
75 ToggleDebug,
76 ToggleSettingsComplete(SettingsUpdate, Sender<()>),
77 ToggleSettingsInit(Option<SettingsUpdate>, Option<Sender<ApiResult<JsValue>>>),
78}
79
80use PerspectiveViewerMsg::*;
81
82pub struct PerspectiveViewer {
83 _subscriptions: [Subscription; 1],
84 column_settings_panel_width_override: Option<i32>,
85 debug_open: bool,
86 fonts: FontLoaderProps,
87 on_close_column_settings: Callback<()>,
88 on_rendered: Option<Sender<()>>,
89 on_resize: Rc<PubSub<()>>,
90 settings_open: bool,
91 settings_panel_width_override: Option<i32>,
92}
93
94impl Component for PerspectiveViewer {
95 type Message = PerspectiveViewerMsg;
96 type Properties = PerspectiveViewerProps;
97
98 fn create(ctx: &Context<Self>) -> Self {
99 let elem = ctx.props().elem.clone();
100 let fonts = FontLoaderProps::new(&elem, ctx.link().callback(|()| PreloadFontsUpdate));
101
102 let session_sub = {
103 let props = ctx.props().clone();
104 let callback = ctx.link().batch_callback(move |(update, _)| {
105 if update {
106 vec![]
107 } else {
108 let locator = props.get_current_column_locator();
109 vec![OpenColumnSettings {
110 locator,
111 sender: None,
112 toggle: false,
113 }]
114 }
115 });
116
117 ctx.props()
118 .renderer
119 .render_limits_changed
120 .add_listener(callback)
121 };
122
123 let on_close_column_settings = ctx.link().callback(|_| OpenColumnSettings {
124 locator: None,
125 sender: None,
126 toggle: false,
127 });
128
129 Self {
130 _subscriptions: [session_sub],
131 column_settings_panel_width_override: None,
132 debug_open: false,
133 fonts,
134 on_close_column_settings,
135 on_rendered: None,
136 on_resize: Default::default(),
137 settings_open: false,
138 settings_panel_width_override: None,
139 }
140 }
141
142 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
143 match msg {
144 PreloadFontsUpdate => true,
145 Resize => {
146 self.on_resize.emit(());
147 false
148 },
149 Reset(all, sender) => {
150 ctx.props().presentation.set_open_column_settings(None);
151 clone!(
152 ctx.props().renderer,
153 ctx.props().session,
154 ctx.props().presentation
155 );
156
157 ApiFuture::spawn(async move {
158 session
159 .reset(ResetOptions {
160 config: true,
161 expressions: all,
162 ..ResetOptions::default()
163 })
164 .await?;
165 let columns_config = if all {
166 presentation.reset_columns_configs();
167 None
168 } else {
169 Some(presentation.all_columns_configs())
170 };
171
172 renderer.reset(columns_config.as_ref()).await?;
173 presentation.reset_available_themes(None).await;
174 if all {
175 presentation.reset_theme().await?;
176 }
177
178 let result = renderer.draw(session.validate().await?.create_view()).await;
179 if let Some(sender) = sender {
180 sender.send(()).unwrap();
181 }
182
183 renderer.reset_changed.emit(());
184 result
185 });
186
187 false
188 },
189 ToggleSettingsInit(Some(SettingsUpdate::Missing), None) => false,
190 ToggleSettingsInit(Some(SettingsUpdate::Missing), Some(resolve)) => {
191 resolve.send(Ok(JsValue::UNDEFINED)).unwrap();
192 false
193 },
194 ToggleSettingsInit(Some(SettingsUpdate::SetDefault), resolve) => {
195 self.init_toggle_settings_task(ctx, Some(false), resolve);
196 false
197 },
198 ToggleSettingsInit(Some(SettingsUpdate::Update(force)), resolve) => {
199 self.init_toggle_settings_task(ctx, Some(force), resolve);
200 false
201 },
202 ToggleSettingsInit(None, resolve) => {
203 self.init_toggle_settings_task(ctx, None, resolve);
204 false
205 },
206 ToggleSettingsComplete(SettingsUpdate::SetDefault, resolve) if self.settings_open => {
207 ctx.props().presentation.set_open_column_settings(None);
208 self.settings_open = false;
209 self.on_rendered = Some(resolve);
210 true
211 },
212 ToggleSettingsComplete(SettingsUpdate::Update(force), resolve)
213 if force != self.settings_open =>
214 {
215 ctx.props().presentation.set_open_column_settings(None);
216 self.settings_open = force;
217 self.on_rendered = Some(resolve);
218 true
219 },
220 ToggleSettingsComplete(_, resolve)
221 if matches!(self.fonts.get_status(), FontLoaderStatus::Finished) =>
222 {
223 ctx.props().presentation.set_open_column_settings(None);
224 if let Err(e) = resolve.send(()) {
225 tracing::error!("toggle settings failed {:?}", e);
226 }
227
228 false
229 },
230 ToggleSettingsComplete(_, resolve) => {
231 ctx.props().presentation.set_open_column_settings(None);
232 self.on_rendered = Some(resolve);
233 true
234 },
235 OpenColumnSettings {
236 locator,
237 sender,
238 toggle,
239 } => {
240 let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
241 if locator == open_column_settings.locator {
242 if toggle {
243 ctx.props().presentation.set_open_column_settings(None);
244 }
245 } else {
246 open_column_settings.locator.clone_from(&locator);
247 open_column_settings.tab =
248 if matches!(locator, Some(ColumnLocator::NewExpression)) {
249 Some(ColumnSettingsTab::Attributes)
250 } else {
251 locator.as_ref().and_then(|x| {
252 x.name().map(|x| {
253 if ctx.props().session.is_column_active(x) {
254 ColumnSettingsTab::Style
255 } else {
256 ColumnSettingsTab::Attributes
257 }
258 })
259 })
260 };
261
262 ctx.props()
263 .presentation
264 .set_open_column_settings(Some(open_column_settings));
265 }
266
267 if let Some(sender) = sender {
268 sender.send(()).unwrap();
269 }
270
271 true
272 },
273 SettingsPanelSizeUpdate(Some(x)) => {
274 self.settings_panel_width_override = Some(x);
275 false
276 },
277 SettingsPanelSizeUpdate(None) => {
278 self.settings_panel_width_override = None;
279 false
280 },
281 ColumnSettingsPanelSizeUpdate(Some(x)) => {
282 self.column_settings_panel_width_override = Some(x);
283 false
284 },
285 ColumnSettingsPanelSizeUpdate(None) => {
286 self.column_settings_panel_width_override = None;
287 false
288 },
289 ColumnSettingsTabChanged(tab) => {
290 let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
291 open_column_settings.tab.clone_from(&Some(tab));
292 ctx.props()
293 .presentation
294 .set_open_column_settings(Some(open_column_settings));
295 true
296 },
297 ToggleDebug => {
298 self.debug_open = !self.debug_open;
299 clone!(ctx.props().renderer, ctx.props().session);
300 ApiFuture::spawn(async move {
301 renderer.draw(session.validate().await?.create_view()).await
302 });
303
304 true
305 },
306 }
307 }
308
309 fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
313 true
314 }
315
316 fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
319 if self.on_rendered.is_some()
320 && matches!(self.fonts.get_status(), FontLoaderStatus::Finished)
321 && self.on_rendered.take().unwrap().send(()).is_err()
322 {
323 tracing::warn!("Orphan render");
324 }
325 }
326
327 fn view(&self, ctx: &Context<Self>) -> Html {
328 let Self::Properties {
329 custom_events,
330 dragdrop,
331 presentation,
332 renderer,
333 session,
334 ..
335 } = ctx.props();
336
337 let is_settings_open = self.settings_open && ctx.props().session.has_table();
338 let mut class = classes!();
339 if !is_settings_open {
340 class.push("settings-closed");
341 }
342
343 if ctx.props().is_title() {
344 class.push("titled");
345 }
346
347 let on_open_expr_panel = ctx.link().callback(|c| OpenColumnSettings {
348 locator: Some(c),
349 sender: None,
350 toggle: true,
351 });
352
353 let on_split_panel_resize = ctx
354 .link()
355 .callback(|(x, _)| SettingsPanelSizeUpdate(Some(x)));
356
357 let on_column_settings_panel_resize = ctx
358 .link()
359 .callback(|(x, _)| ColumnSettingsPanelSizeUpdate(Some(x)));
360
361 let on_close_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
362 let on_debug = ctx.link().callback(|_| ToggleDebug);
363 let selected_column = ctx.props().get_current_column_locator();
364 let selected_tab = ctx.props().presentation.get_open_column_settings().tab;
365 let settings_panel = html! {
366 if is_settings_open {
367 <SettingsPanel
368 on_close={on_close_settings}
369 on_resize={&self.on_resize}
370 on_select_column={on_open_expr_panel}
371 is_debug={self.debug_open}
372 {on_debug}
373 {dragdrop}
374 {presentation}
375 {renderer}
376 {session}
377 />
378 }
379 };
380
381 let on_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
382 let on_select_tab = ctx.link().callback(ColumnSettingsTabChanged);
383 let column_settings_panel = html! {
384 if let Some(selected_column) = selected_column {
385 <SplitPanel
386 id="modal_panel"
387 reverse=true
388 initial_size={self.column_settings_panel_width_override}
389 on_reset={ctx.link().callback(|_| ColumnSettingsPanelSizeUpdate(None))}
390 on_resize={on_column_settings_panel_resize}
391 >
392 <ColumnSettingsPanel
393 {selected_column}
394 {selected_tab}
395 on_close={self.on_close_column_settings.clone()}
396 width_override={self.column_settings_panel_width_override}
397 {on_select_tab}
398 {custom_events}
399 {presentation}
400 {renderer}
401 {session}
402 />
403 <></>
404 </SplitPanel>
405 }
406 };
407
408 let main_panel = html! {
409 <MainPanel {on_settings} {custom_events} {presentation} {renderer} {session} />
410 };
411
412 let debug_panel = html! {
413 if self.debug_open { <DebugPanel {presentation} {renderer} {session} /> }
414 };
415
416 html! {
417 <StyleProvider root={ctx.props().elem.clone()}>
418 <LocalStyle href={css!("viewer")} />
419 <div id="component_container">
420 if is_settings_open {
421 <SplitPanel
422 id="app_panel"
423 reverse=true
424 skip_empty=true
425 initial_size={self.settings_panel_width_override}
426 on_reset={ctx.link().callback(|_| SettingsPanelSizeUpdate(None))}
427 on_resize={on_split_panel_resize.clone()}
428 on_resize_finished={ctx.props().render_callback()}
429 >
430 { debug_panel }
431 { settings_panel }
432 <div id="main_column_container">
433 { main_panel }
434 { column_settings_panel }
435 </div>
436 </SplitPanel>
437 } else {
438 <div id="main_column_container">
439 { main_panel }
440 { column_settings_panel }
441 </div>
442 }
443 </div>
444 <FontLoader ..self.fonts.clone() />
445 </StyleProvider>
446 }
447 }
448
449 fn destroy(&mut self, _ctx: &Context<Self>) {}
450}
451
452impl PerspectiveViewer {
453 fn init_toggle_settings_task(
467 &mut self,
468 ctx: &Context<Self>,
469 force: Option<bool>,
470 sender: Option<Sender<ApiResult<JsValue>>>,
471 ) {
472 let is_open = ctx.props().presentation.is_settings_open();
473 ctx.props().presentation.set_settings_before_open(!is_open);
474 match force {
475 Some(force) if is_open == force => {
476 if let Some(sender) = sender {
477 sender.send(Ok(JsValue::UNDEFINED)).unwrap();
478 }
479 },
480 Some(_) | None => {
481 let force = !is_open;
482 let callback = ctx.link().callback(move |resolve| {
483 let update = SettingsUpdate::Update(force);
484 ToggleSettingsComplete(update, resolve)
485 });
486
487 clone!(
488 ctx.props().renderer,
489 ctx.props().session,
490 ctx.props().presentation
491 );
492
493 ApiFuture::spawn(async move {
494 let result = if session.js_get_table().is_some() {
495 renderer
496 .presize(force, {
497 let (sender, receiver) = channel::<()>();
498 async move {
499 callback.emit(sender);
500 presentation.set_settings_open(!is_open);
501 Ok(receiver.await?)
502 }
503 })
504 .await
505 } else {
506 let (sender, receiver) = channel::<()>();
507 callback.emit(sender);
508 presentation.set_settings_open(!is_open);
509 receiver.await?;
510 Ok(JsValue::UNDEFINED)
511 };
512
513 if let Some(sender) = sender {
514 let msg = result.ignore_view_delete();
515 sender
516 .send(msg.map(|x| x.unwrap_or(JsValue::UNDEFINED)))
517 .into_apierror()?;
518 };
519
520 Ok(JsValue::undefined())
521 });
522 },
523 };
524 }
525}