perspective_viewer/components/
status_bar.rs1use std::rc::Rc;
14
15use web_sys::*;
16use yew::prelude::*;
17
18use super::status_indicator::StatusIndicator;
19use super::style::LocalStyle;
20use crate::components::containers::select::*;
21use crate::components::status_bar_counter::StatusBarRowsCounter;
22use crate::custom_elements::copy_dropdown::*;
23use crate::custom_elements::export_dropdown::*;
24use crate::custom_events::CustomEvents;
25use crate::model::*;
26use crate::presentation::Presentation;
27use crate::renderer::*;
28use crate::session::*;
29use crate::utils::*;
30use crate::*;
31
32#[derive(Properties, PerspectiveProperties!)]
33pub struct StatusBarProps {
34 pub id: String,
36
37 pub on_reset: Callback<bool>,
39
40 #[prop_or_default]
42 pub on_settings: Option<Callback<()>>,
43
44 pub custom_events: CustomEvents,
46 pub session: Session,
47 pub renderer: Renderer,
48 pub presentation: Presentation,
49}
50
51impl PartialEq for StatusBarProps {
52 fn eq(&self, other: &Self) -> bool {
53 self.id == other.id
54 }
55}
56
57pub enum StatusBarMsg {
58 Reset(MouseEvent),
59 Export,
60 Copy,
61 Noop,
62 Eject,
63 SetThemeConfig((Rc<Vec<String>>, Option<usize>)),
64 SetTheme(String),
65 ResetTheme,
66 PointerEvent(web_sys::PointerEvent),
67 TitleInputEvent,
68 TitleChangeEvent,
69}
70
71pub struct StatusBar {
73 _subscriptions: [Subscription; 5],
74 copy_ref: NodeRef,
75 export_ref: NodeRef,
76 input_ref: NodeRef,
77 statusbar_ref: NodeRef,
78 theme: Option<String>,
79 themes: Rc<Vec<String>>,
80 title: Option<String>,
81}
82
83impl Component for StatusBar {
84 type Message = StatusBarMsg;
85 type Properties = StatusBarProps;
86
87 fn create(ctx: &Context<Self>) -> Self {
88 fetch_initial_theme(ctx);
89 Self {
90 _subscriptions: register_listeners(ctx),
91 copy_ref: NodeRef::default(),
92 export_ref: NodeRef::default(),
93 input_ref: NodeRef::default(),
94 statusbar_ref: NodeRef::default(),
95 theme: None,
96 themes: vec![].into(),
97 title: ctx.props().session().get_title().clone(),
98 }
99 }
100
101 fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
102 self._subscriptions = register_listeners(ctx);
103 true
104 }
105
106 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
107 maybe_log_or_default!(Ok(match msg {
108 StatusBarMsg::Reset(event) => {
109 let all = event.shift_key();
110 ctx.props().on_reset.emit(all);
111 false
112 },
113 StatusBarMsg::ResetTheme => {
114 let state = ctx.props().clone_state();
115 ApiFuture::spawn(async move {
116 state.presentation.reset_theme().await?;
117 let view = state.session.get_view().into_apierror()?;
118 state.renderer.restyle_all(&view).await
119 });
120 true
121 },
122 StatusBarMsg::SetThemeConfig((themes, index)) => {
123 let new_theme = index.and_then(|x| themes.get(x)).cloned();
124 let should_render = new_theme != self.theme || self.themes != themes;
125 self.theme = new_theme;
126 self.themes = themes;
127 should_render
128 },
129 StatusBarMsg::SetTheme(theme_name) => {
130 let state = ctx.props().clone_state();
131 ApiFuture::spawn(async move {
132 state.presentation.set_theme_name(Some(&theme_name)).await?;
133 let view = state.session.get_view().into_apierror()?;
134 state.renderer.restyle_all(&view).await
135 });
136
137 false
138 },
139 StatusBarMsg::Export => {
140 let target = self.export_ref.cast::<HtmlElement>().into_apierror()?;
141 ExportDropDownMenuElement::new_from_model(ctx.props()).open(target);
142 false
143 },
144 StatusBarMsg::Copy => {
145 let target = self.copy_ref.cast::<HtmlElement>().into_apierror()?;
146 CopyDropDownMenuElement::new_from_model(ctx.props()).open(target);
147 false
148 },
149 StatusBarMsg::Eject => {
150 ctx.props().presentation().on_eject.emit(());
151 false
152 },
153 StatusBarMsg::Noop => {
154 self.title = ctx.props().session().get_title();
155 true
156 },
157 StatusBarMsg::TitleInputEvent => {
158 let elem = self.input_ref.cast::<HtmlInputElement>().into_apierror()?;
159 let title = elem.value();
160 let title = if title.trim().is_empty() {
161 None
162 } else {
163 Some(title)
164 };
165
166 self.title = title;
167 true
168 },
169 StatusBarMsg::TitleChangeEvent => {
170 let elem = self.input_ref.cast::<HtmlInputElement>().into_apierror()?;
171 let title = elem.value();
172 let title = if title.trim().is_empty() {
173 None
174 } else {
175 Some(title)
176 };
177
178 ctx.props().session().set_title(title);
179 false
180 },
181 StatusBarMsg::PointerEvent(event) => {
182 if event.target().map(JsValue::from)
183 == self.statusbar_ref.cast::<HtmlElement>().map(JsValue::from)
184 {
185 ctx.props()
186 .custom_events()
187 .dispatch_event(format!("statusbar-{}", event.type_()).as_str(), &event)?;
188 }
189
190 false
191 },
192 }))
193 }
194
195 fn view(&self, ctx: &Context<Self>) -> Html {
196 let Self::Properties {
197 custom_events,
198 presentation,
199 renderer,
200 session,
201 ..
202 } = ctx.props();
203
204 let mut is_updating_class_name = classes!();
205 if session.get_title().is_some() {
206 is_updating_class_name.push("titled");
207 };
208
209 if !presentation.is_settings_open() {
210 is_updating_class_name.push(["settings-closed", "titled"]);
211 };
212
213 if !session.has_table() {
214 is_updating_class_name.push("updating");
215 }
216
217 let onblur = ctx.link().callback(|_| StatusBarMsg::Noop);
219 let onclose = ctx.link().callback(|_| StatusBarMsg::Eject);
220 let onpointerdown = ctx.link().callback(StatusBarMsg::PointerEvent);
221 let onexport = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Export);
222 let oncopy = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Copy);
223 let onreset = ctx.link().callback(StatusBarMsg::Reset);
224 let onchange = ctx
225 .link()
226 .callback(|_: Event| StatusBarMsg::TitleChangeEvent);
227
228 let oninput = ctx
229 .link()
230 .callback(|_: InputEvent| StatusBarMsg::TitleInputEvent);
231
232 let is_menu = session.has_table() && ctx.props().on_settings.as_ref().is_none();
233 let is_title = is_menu
234 || presentation.get_is_workspace()
235 || session.get_title().is_some()
236 || session.is_errored()
237 || presentation.is_active(&self.input_ref.cast::<Element>());
238
239 let is_settings = session.get_title().is_some()
240 || presentation.get_is_workspace()
241 || !session.has_table()
242 || session.is_errored()
243 || presentation.is_settings_open()
244 || presentation.is_active(&self.input_ref.cast::<Element>());
245
246 if is_settings {
247 html! {
248 <>
249 <LocalStyle href={css!("status-bar")} />
250 <div
251 ref={&self.statusbar_ref}
252 id={ctx.props().id.clone()}
253 class={is_updating_class_name}
254 {onpointerdown}
255 >
256 <StatusIndicator {custom_events} {renderer} {session} />
257 if is_title {
258 <label
259 class="input-sizer"
260 data-value={self.title.clone().unwrap_or_default()}
261 >
262 <input
263 ref={&self.input_ref}
264 placeholder=""
265 value={self.title.clone().unwrap_or_default()}
266 size="10"
267 {onblur}
268 {onchange}
269 {oninput}
270 />
271 <span id="status-bar-placeholder" />
272 </label>
273 }
274 if is_title {
275 <StatusBarRowsCounter {session} />
276 }
277 <div id="spacer" />
278 if is_menu {
279 <div id="menu-bar" class="section">
280 <ThemeSelector
281 theme={self.theme.clone()}
282 themes={self.themes.clone()}
283 on_change={ctx.link().callback(StatusBarMsg::SetTheme)}
284 on_reset={ctx.link().callback(|_| StatusBarMsg::ResetTheme)}
285 />
286 <div id="plugin-settings"><slot name="statusbar-extra" /></div>
287 <span class="hover-target">
288 <span id="reset" class="button" onmousedown={&onreset}>
289 <span />
290 </span>
291 </span>
292 <span
293 ref={&self.export_ref}
294 class="hover-target"
295 onmousedown={onexport}
296 >
297 <span id="export" class="button"><span /></span>
298 </span>
299 <span
300 ref={&self.copy_ref}
301 class="hover-target"
302 onmousedown={oncopy}
303 >
304 <span id="copy" class="button"><span /></span>
305 </span>
306 </div>
307 }
308 if let Some(x) = ctx.props().on_settings.as_ref() {
309 <div
310 id="settings_button"
311 class="noselect"
312 onmousedown={x.reform(|_| ())}
313 />
314 <div id="close_button" class="noselect" onmousedown={onclose} />
315 }
316 </div>
317 </>
318 }
319 } else if let Some(x) = ctx.props().on_settings.as_ref() {
320 let class = classes!(is_updating_class_name, "floating");
321 html! {
322 <div id={ctx.props().id.clone()} {class}>
323 <div id="settings_button" class="noselect" onmousedown={x.reform(|_| ())} />
324 <div id="close_button" class="noselect" onmousedown={&onclose} />
325 </div>
326 }
327 } else {
328 html! {}
329 }
330 }
331}
332
333fn register_listeners(ctx: &Context<StatusBar>) -> [Subscription; 5] {
334 [
335 ctx.props()
336 .presentation()
337 .theme_config_updated
338 .add_listener(ctx.link().callback(StatusBarMsg::SetThemeConfig)),
339 ctx.props()
340 .presentation()
341 .visibility_changed
342 .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
343 ctx.props()
344 .session()
345 .title_changed
346 .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
347 ctx.props()
348 .session()
349 .table_loaded
350 .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
351 ctx.props()
352 .session()
353 .table_errored
354 .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
355 ]
356}
357
358fn fetch_initial_theme(ctx: &Context<StatusBar>) {
359 ApiFuture::spawn({
360 let on_theme = ctx.link().callback(StatusBarMsg::SetThemeConfig);
361 clone!(ctx.props().presentation());
362 async move {
363 on_theme.emit(presentation.get_selected_theme_config().await?);
364 Ok(())
365 }
366 });
367}
368
369#[derive(Properties, PartialEq)]
370struct ThemeSelectorProps {
371 pub theme: Option<String>,
372 pub themes: Rc<Vec<String>>,
373 pub on_reset: Callback<()>,
374 pub on_change: Callback<String>,
375}
376
377#[function_component]
378fn ThemeSelector(props: &ThemeSelectorProps) -> Html {
379 let is_first = props
380 .theme
381 .as_ref()
382 .and_then(|x| props.themes.first().map(|y| y == x))
383 .unwrap_or_default();
384
385 let values = use_memo(props.themes.clone(), |themes| {
386 themes
387 .iter()
388 .cloned()
389 .map(SelectItem::Option)
390 .collect::<Vec<_>>()
391 });
392
393 match &props.theme {
394 None => html! {},
395 Some(selected) => {
396 html! {
397 if values.len() > 1 {
398 <span class="hover-target">
399 <div
400 id="theme_icon"
401 class={if is_first {""} else {"modified"}}
402 tabindex="0"
403 onclick={props.on_reset.reform(|_| ())}
404 />
405 <span id="theme" class="button">
406 <Select<String>
407 id="theme_selector"
408 class="invert"
409 {values}
410 selected={selected.to_owned()}
411 on_select={props.on_change.clone()}
412 />
413 </span>
414 </span>
415 }
416 }
417 },
418 }
419}