perspective_viewer/components/
status_bar.rs1use wasm_bindgen_futures::spawn_local;
14use web_sys::*;
15use yew::prelude::*;
16
17use super::status_indicator::StatusIndicator;
18use super::style::LocalStyle;
19use crate::components::containers::select::*;
20use crate::components::copy_dropdown::CopyDropDownMenu;
21use crate::components::export_dropdown::ExportDropDownMenu;
22use crate::components::portal::PortalModal;
23use crate::components::status_bar_counter::StatusBarRowsCounter;
24use crate::config::*;
25use crate::js::*;
26use crate::presentation::{Presentation, PresentationProps};
27use crate::renderer::*;
28use crate::session::*;
29use crate::tasks::*;
30use crate::utils::*;
31use crate::*;
32
33#[derive(Clone, Properties)]
34pub struct StatusBarProps {
35 pub id: String,
37
38 pub on_reset: Callback<bool>,
40
41 #[prop_or_default]
43 pub on_settings: Option<Callback<()>>,
44
45 pub session_props: SessionProps,
49 pub presentation_props: PresentationProps,
50
51 pub is_settings_open: bool,
55
56 pub update_count: u32,
58
59 pub session: Session,
61 pub renderer: Renderer,
62 pub presentation: Presentation,
63}
64
65impl PartialEq for StatusBarProps {
66 fn eq(&self, other: &Self) -> bool {
67 self.id == other.id
68 && self.session_props == other.session_props
69 && self.presentation_props == other.presentation_props
70 && self.is_settings_open == other.is_settings_open
71 && self.update_count == other.update_count
72 }
73}
74
75pub enum StatusBarMsg {
76 Reset(MouseEvent),
77 Export,
78 Copy,
79 CloseExport,
80 CloseCopy,
81 Noop,
82 Eject,
83 SetTheme(String),
84 ResetTheme,
85 PointerEvent(web_sys::PointerEvent),
86 TitleInputEvent,
87 TitleChangeEvent,
88}
89
90pub struct StatusBar {
92 copy_ref: NodeRef,
93 export_ref: NodeRef,
94 input_ref: NodeRef,
95 statusbar_ref: NodeRef,
96 title: Option<String>,
100 copy_target: Option<HtmlElement>,
101 export_target: Option<HtmlElement>,
102}
103
104impl Component for StatusBar {
105 type Message = StatusBarMsg;
106 type Properties = StatusBarProps;
107
108 fn create(ctx: &Context<Self>) -> Self {
109 Self {
110 copy_ref: NodeRef::default(),
111 export_ref: NodeRef::default(),
112 input_ref: NodeRef::default(),
113 statusbar_ref: NodeRef::default(),
114 title: ctx.props().session_props.title.clone(),
115 copy_target: None,
116 export_target: None,
117 }
118 }
119
120 fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
121 if ctx.props().session_props.title != old_props.session_props.title
125 || ctx.props().is_settings_open != old_props.is_settings_open
126 {
127 self.title = ctx.props().session_props.title.clone();
128 }
129 true
130 }
131
132 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
133 let r: ApiResult<bool> = (|| {
134 Ok(match msg {
135 StatusBarMsg::Reset(event) => {
136 let all = event.shift_key();
137 ctx.props().on_reset.emit(all);
138 false
139 },
140 StatusBarMsg::ResetTheme => {
141 update_theme(
142 &ctx.props().session,
143 &ctx.props().renderer,
144 &ctx.props().presentation,
145 None,
146 );
147 true
148 },
149 StatusBarMsg::SetTheme(theme_name) => {
150 update_theme(
151 &ctx.props().session,
152 &ctx.props().renderer,
153 &ctx.props().presentation,
154 Some(theme_name),
155 );
156 false
157 },
158 StatusBarMsg::Export => {
159 self.export_target = self.export_ref.cast::<HtmlElement>();
160 true
161 },
162 StatusBarMsg::Copy => {
163 self.copy_target = self.copy_ref.cast::<HtmlElement>();
164 true
165 },
166 StatusBarMsg::CloseExport => {
167 self.export_target = None;
168 true
169 },
170 StatusBarMsg::CloseCopy => {
171 self.copy_target = None;
172 true
173 },
174 StatusBarMsg::Eject => {
175 ctx.props().presentation.on_eject.emit(());
176 false
177 },
178 StatusBarMsg::Noop => {
179 self.title = ctx.props().session_props.title.clone();
180 true
181 },
182 StatusBarMsg::TitleInputEvent => {
183 let elem = self.input_ref.cast::<HtmlInputElement>().into_apierror()?;
184 let title = elem.value();
185 let title = if title.trim().is_empty() {
186 None
187 } else {
188 Some(title)
189 };
190
191 self.title = title;
192 true
193 },
194 StatusBarMsg::TitleChangeEvent => {
195 let elem = self.input_ref.cast::<HtmlInputElement>().into_apierror()?;
196 let title = elem.value();
197 let title = if title.trim().is_empty() {
198 None
199 } else {
200 Some(title)
201 };
202
203 ctx.props().session.set_title(title);
204 false
205 },
206 StatusBarMsg::PointerEvent(event) => {
207 if event.target().map(JsValue::from)
208 == self.statusbar_ref.cast::<HtmlElement>().map(JsValue::from)
209 {
210 ctx.props().presentation.statusbar_pointer_event.emit(event);
211 }
212
213 false
214 },
215 })
216 })();
217 r.unwrap_or_else(|e| {
218 web_sys::console::warn_1(&e.into());
219 Default::default()
220 })
221 }
222
223 fn view(&self, ctx: &Context<Self>) -> Html {
224 let Self::Properties {
225 presentation,
226 renderer,
227 session,
228 ..
229 } = ctx.props();
230
231 let has_table = ctx.props().session_props.has_table.clone();
232 let is_errored = ctx.props().session_props.is_errored();
233 let is_settings_open = ctx.props().is_settings_open;
234 let title = &ctx.props().session_props.title;
235
236 let mut is_updating_class_name = classes!();
237 if title.is_some() {
238 is_updating_class_name.push("titled");
239 };
240
241 if !is_settings_open {
242 is_updating_class_name.push(["settings-closed", "titled"]);
243 };
244
245 if !matches!(has_table, Some(TableLoadState::Loaded)) {
246 is_updating_class_name.push("updating");
247 }
248
249 let onblur = ctx.link().callback(|_| StatusBarMsg::Noop);
251 let onclose = ctx.link().callback(|_| StatusBarMsg::Eject);
252 let onpointerdown = ctx.link().callback(StatusBarMsg::PointerEvent);
253 let onexport = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Export);
254 let oncopy = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Copy);
255 let onreset = ctx.link().callback(StatusBarMsg::Reset);
256 let onchange = ctx
257 .link()
258 .callback(|_: Event| StatusBarMsg::TitleChangeEvent);
259
260 let oninput = ctx
261 .link()
262 .callback(|_: InputEvent| StatusBarMsg::TitleInputEvent);
263
264 let is_menu = matches!(has_table, Some(TableLoadState::Loaded))
265 && ctx.props().on_settings.as_ref().is_none();
266 let is_title = is_menu
267 || ctx.props().presentation_props.is_workspace
268 || title.is_some()
269 || is_errored
270 || presentation.is_active(&self.input_ref.cast::<Element>());
271
272 let is_settings = title.is_some()
273 || ctx.props().presentation_props.is_workspace
274 || !matches!(has_table, Some(TableLoadState::Loaded))
275 || is_errored
276 || is_settings_open
277 || presentation.is_active(&self.input_ref.cast::<Element>());
278
279 let on_copy_select = {
280 let props = ctx.props().clone();
281 let link = ctx.link().clone();
282 Callback::from(move |x: ExportFile| {
283 let props = props.clone();
284 let link = link.clone();
285 spawn_local(async move {
286 let mime = x.method.mimetype(x.is_chart);
287 let task = export_method_to_blob(
288 &props.session,
289 &props.renderer,
290 &props.presentation,
291 x.method,
292 );
293 let result = copy_to_clipboard(task, mime).await;
294 let r = (|| -> ApiResult<()> {
295 result?;
296 link.send_message(StatusBarMsg::CloseCopy);
297 Ok(())
298 })();
299 if let Err(e) = r {
300 web_sys::console::warn_1(&e.into());
301 }
302 })
303 })
304 };
305
306 let on_export_select = {
307 let props = ctx.props().clone();
308 let link = ctx.link().clone();
309 Callback::from(move |x: ExportFile| {
310 if !x.name.is_empty() {
311 clone!(props, link);
312 spawn_local(async move {
313 let val = export_method_to_blob(
314 &props.session,
315 &props.renderer,
316 &props.presentation,
317 x.method,
318 )
319 .await
320 .unwrap();
321 let is_chart = props.renderer.is_chart();
322 download(&x.as_filename(is_chart), &val).unwrap();
323 link.send_message(StatusBarMsg::CloseExport);
324 })
325 }
326 })
327 };
328
329 let on_close_copy = ctx.link().callback(|_| StatusBarMsg::CloseCopy);
330 let on_close_export = ctx.link().callback(|_| StatusBarMsg::CloseExport);
331
332 if is_settings {
333 html! {
334 <>
335 <LocalStyle href={css!("status-bar")} />
336 <div
337 ref={&self.statusbar_ref}
338 id={ctx.props().id.clone()}
339 class={is_updating_class_name}
340 {onpointerdown}
341 >
342 <StatusIndicator
343 {renderer}
344 {session}
345 update_count={ctx.props().update_count}
346 session_props={ctx.props().session_props.clone()}
347 />
348 if is_title {
349 <label
350 class="input-sizer"
351 data-value={self.title.clone().unwrap_or_default()}
352 >
353 <input
354 ref={&self.input_ref}
355 placeholder=""
356 value={self.title.clone().unwrap_or_default()}
357 size="10"
358 {onblur}
359 {onchange}
360 {oninput}
361 />
362 <span id="status-bar-placeholder" />
363 </label>
364 }
365 if is_title {
366 <StatusBarRowsCounter stats={ctx.props().session_props.stats.clone()} />
367 }
368 <div id="spacer" />
369 if is_menu {
370 <div id="menu-bar" class="section">
371 <ThemeSelector
372 theme={ctx.props().presentation_props.selected_theme.clone()}
373 themes={ctx.props().presentation_props.available_themes.clone()}
374 on_change={ctx.link().callback(StatusBarMsg::SetTheme)}
375 on_reset={ctx.link().callback(|_| StatusBarMsg::ResetTheme)}
376 />
377 <div id="plugin-settings"><slot name="statusbar-extra" /></div>
378 <span class="hover-target">
379 <span id="reset" class="button" onmousedown={&onreset}>
380 <span class="icon" />
381 <span class="icon-label" />
382 </span>
383 </span>
384 <span
385 ref={&self.export_ref}
386 class="hover-target"
387 onmousedown={onexport}
388 >
389 <span id="export" class="button">
390 <span class="icon" />
391 <span class="icon-label" />
392 </span>
393 </span>
394 <span
395 ref={&self.copy_ref}
396 class="hover-target"
397 onmousedown={oncopy}
398 >
399 <span id="copy" class="button">
400 <span class="icon" />
401 <span class="icon-label" />
402 </span>
403 </span>
404 </div>
405 }
406 if let Some(x) = ctx.props().on_settings.as_ref() {
407 <div
408 id="settings_button"
409 class="noselect"
410 onmousedown={x.reform(|_| ())}
411 >
412 <span class="icon" />
413 </div>
414 <div id="close_button" class="noselect" onmousedown={onclose}>
415 <span class="icon" />
416 </div>
417 }
418 </div>
419 <PortalModal
420 tag_name="perspective-copy-menu"
421 target={self.copy_target.clone()}
422 own_focus=true
423 on_close={on_close_copy}
424 theme={ctx.props().presentation_props.selected_theme.clone().unwrap_or_default()}
425 >
426 <CopyDropDownMenu renderer={renderer.clone()} callback={on_copy_select} />
427 </PortalModal>
428 <PortalModal
429 tag_name="perspective-export-menu"
430 target={self.export_target.clone()}
431 own_focus=true
432 on_close={on_close_export}
433 theme={ctx.props().presentation_props.selected_theme.clone().unwrap_or_default()}
434 >
435 <ExportDropDownMenu
436 renderer={renderer.clone()}
437 session={session.clone()}
438 callback={on_export_select}
439 />
440 </PortalModal>
441 </>
442 }
443 } else if let Some(x) = ctx.props().on_settings.as_ref() {
444 let class = classes!(is_updating_class_name, "floating");
445 html! {
446 <div id={ctx.props().id.clone()} {class}>
447 <div id="settings_button" class="noselect" onmousedown={x.reform(|_| ())}>
448 <span class="icon" />
449 </div>
450 <div id="close_button" class="noselect" onmousedown={&onclose} />
451 </div>
452 }
453 } else {
454 html! {}
455 }
456 }
457}
458
459#[derive(Properties, PartialEq)]
460struct ThemeSelectorProps {
461 pub theme: Option<String>,
462 pub themes: PtrEqRc<Vec<String>>,
463 pub on_reset: Callback<()>,
464 pub on_change: Callback<String>,
465}
466
467#[function_component]
468fn ThemeSelector(props: &ThemeSelectorProps) -> Html {
469 let is_first = props
470 .theme
471 .as_ref()
472 .and_then(|x| props.themes.first().map(|y| y == x))
473 .unwrap_or_default();
474
475 let values = use_memo(props.themes.clone(), |themes| {
476 themes
477 .iter()
478 .cloned()
479 .map(SelectItem::Option)
480 .collect::<Vec<_>>()
481 });
482
483 match &props.theme {
484 None => html! {},
485 Some(selected) => {
486 html! {
487 if values.len() > 1 {
488 <span class="hover-target">
489 <div
490 id="theme_icon"
491 class={if is_first {""} else {"modified"}}
492 tabindex="0"
493 onclick={props.on_reset.reform(|_| ())}
494 />
495 <span id="theme" class="button">
496 <span class="icon" />
497 <Select<String>
498 id="theme_selector"
499 class="invert"
500 {values}
501 selected={selected.to_owned()}
502 on_select={props.on_change.clone()}
503 />
504 </span>
505 </span>
506 }
507 }
508 },
509 }
510}