perspective_viewer/components/
status_bar.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::rc::Rc;
14
15use wasm_bindgen::JsCast;
16use web_sys::*;
17use yew::prelude::*;
18
19use super::status_indicator::StatusIndicator;
20use super::style::LocalStyle;
21use crate::components::containers::select::*;
22use crate::components::status_bar_counter::StatusBarRowsCounter;
23use crate::custom_elements::copy_dropdown::*;
24use crate::custom_elements::export_dropdown::*;
25use crate::presentation::Presentation;
26use crate::renderer::*;
27use crate::session::*;
28#[cfg(test)]
29use crate::utils::WeakScope;
30use crate::utils::*;
31use crate::*;
32
33#[derive(Properties, Clone)]
34pub struct StatusBarProps {
35    pub id: String,
36    pub on_reset: Callback<bool>,
37    pub session: Session,
38    pub renderer: Renderer,
39    pub presentation: Presentation,
40
41    #[cfg(test)]
42    #[prop_or_default]
43    pub weak_link: WeakScope<StatusBar>,
44}
45
46derive_model!(Renderer, Session, Presentation for StatusBarProps);
47
48impl PartialEq for StatusBarProps {
49    fn eq(&self, other: &Self) -> bool {
50        self.id == other.id
51    }
52}
53
54pub enum StatusBarMsg {
55    Reset(bool),
56    Export,
57    Copy,
58    Noop,
59    SetThemeConfig((Rc<Vec<String>>, Option<usize>)),
60    SetTheme(String),
61    ResetTheme,
62    // SetError(Option<String>),
63    // TableStatsChanged,
64    // SetIsUpdating(bool),
65    SetTitle(Option<String>),
66}
67
68/// A toolbar with buttons, and `Table` & `View` status information.
69pub struct StatusBar {
70    is_updating: i32,
71    theme: Option<String>,
72    themes: Rc<Vec<String>>,
73    export_ref: NodeRef,
74    copy_ref: NodeRef,
75    _sub: [Subscription; 2],
76}
77
78impl Component for StatusBar {
79    type Message = StatusBarMsg;
80    type Properties = StatusBarProps;
81
82    fn create(ctx: &Context<Self>) -> Self {
83        let _sub = [
84            ctx.props()
85                .presentation
86                .theme_config_updated
87                .add_listener(ctx.link().callback(StatusBarMsg::SetThemeConfig)),
88            ctx.props()
89                .presentation
90                .title_changed
91                .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
92        ];
93
94        // Fetch initial theme
95        let presentation = ctx.props().presentation.clone();
96        let on_theme = ctx.link().callback(StatusBarMsg::SetThemeConfig);
97        ApiFuture::spawn(async move {
98            on_theme.emit(presentation.get_selected_theme_config().await?);
99            Ok(())
100        });
101
102        Self {
103            _sub,
104            theme: None,
105            themes: vec![].into(),
106            copy_ref: NodeRef::default(),
107            export_ref: NodeRef::default(),
108            is_updating: 0,
109        }
110    }
111
112    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
113        match msg {
114            StatusBarMsg::Reset(all) => {
115                ctx.props().on_reset.emit(all);
116                false
117            },
118            StatusBarMsg::ResetTheme => {
119                clone!(
120                    ctx.props().renderer,
121                    ctx.props().session,
122                    ctx.props().presentation
123                );
124
125                ApiFuture::spawn(async move {
126                    presentation.reset_theme().await?;
127                    let view = session.get_view().into_apierror()?;
128                    renderer.restyle_all(&view).await
129                });
130                true
131            },
132            StatusBarMsg::SetThemeConfig((themes, index)) => {
133                let new_theme = index.and_then(|x| themes.get(x)).cloned();
134                let should_render = new_theme != self.theme || self.themes != themes;
135                self.theme = new_theme;
136                self.themes = themes;
137                should_render
138            },
139            StatusBarMsg::SetTheme(theme_name) => {
140                clone!(
141                    ctx.props().renderer,
142                    ctx.props().session,
143                    ctx.props().presentation
144                );
145                ApiFuture::spawn(async move {
146                    presentation.set_theme_name(Some(&theme_name)).await?;
147                    let view = session.get_view().into_apierror()?;
148                    renderer.restyle_all(&view).await
149                });
150
151                false
152            },
153            StatusBarMsg::Export => {
154                let target = self.export_ref.cast::<HtmlElement>().unwrap();
155                ExportDropDownMenuElement::new_from_model(ctx.props()).open(target);
156                false
157            },
158            StatusBarMsg::Copy => {
159                let target = self.copy_ref.cast::<HtmlElement>().unwrap();
160                CopyDropDownMenuElement::new_from_model(ctx.props()).open(target);
161                false
162            },
163            StatusBarMsg::Noop => true,
164            StatusBarMsg::SetTitle(title) => {
165                ctx.props().presentation.set_title(title);
166                false
167            },
168        }
169    }
170
171    fn view(&self, ctx: &Context<Self>) -> Html {
172        let mut is_updating_class_name = classes!();
173        if self.is_updating > 0 {
174            is_updating_class_name.push("updating")
175        };
176
177        if ctx.props().presentation.get_title().is_some() {
178            is_updating_class_name.push("titled")
179        };
180
181        let reset = ctx
182            .link()
183            .callback(|event: MouseEvent| StatusBarMsg::Reset(event.shift_key()));
184
185        let export = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Export);
186        let copy = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Copy);
187
188        let onchange = ctx.link().callback({
189            move |input: Event| {
190                let title = input
191                    .target()
192                    .unwrap()
193                    .unchecked_into::<HtmlInputElement>()
194                    .value();
195
196                let title = if title.trim().is_empty() {
197                    None
198                } else {
199                    Some(title)
200                };
201
202                StatusBarMsg::SetTitle(title)
203            }
204        });
205
206        let is_menu = ctx.props().session.has_table()
207            && (ctx.props().presentation.is_settings_open()
208                || (ctx.props().presentation.get_title().is_some()
209                    && !ctx.props().session.is_errored()));
210
211        if !ctx.props().session.has_table() {
212            is_updating_class_name.push("updating");
213        }
214
215        html! {
216            <>
217                <LocalStyle href={css!("status-bar")} />
218                <div id={ctx.props().id.clone()} class={is_updating_class_name}>
219                    <StatusIndicator
220                        session={&ctx.props().session}
221                        renderer={&ctx.props().renderer}
222                    />
223                    if is_menu {
224                        <label
225                            class="input-sizer"
226                            data-value={ctx.props().presentation.get_title().unwrap_or_default()}
227                        >
228                            <input
229                                placeholder=" "
230                                value={ctx.props().presentation.get_title()}
231                                size="10"
232                                {onchange}
233                            />
234                            <span id="status-bar-placeholder" />
235                        </label>
236                    }
237                    <StatusBarRowsCounter session={&ctx.props().session} />
238                    <div id="spacer" />
239                    if is_menu {
240                        <div id="menu-bar" class="section">
241                            <ThemeSelector
242                                theme={self.theme.clone()}
243                                themes={self.themes.clone()}
244                                on_change={ctx.link().callback(StatusBarMsg::SetTheme)}
245                                on_reset={ctx.link().callback(|_| StatusBarMsg::ResetTheme)}
246                            />
247                            <div id="plugin-settings"><slot name="plugin-settings" /></div>
248                            <span class="hover-target">
249                                <span id="reset" class="button" onmousedown={reset}><span /></span>
250                            </span>
251                            <span class="hover-target" ref={&self.export_ref} onmousedown={export}>
252                                <span id="export" class="button"><span /></span>
253                            </span>
254                            <span class="hover-target" ref={&self.copy_ref} onmousedown={copy}>
255                                <span id="copy" class="button"><span /></span>
256                            </span>
257                        </div>
258                    }
259                </div>
260            </>
261        }
262    }
263}
264
265#[derive(Properties, PartialEq)]
266pub struct ThemeSelectorProps {
267    pub theme: Option<String>,
268    pub themes: Rc<Vec<String>>,
269    pub on_reset: Callback<()>,
270    pub on_change: Callback<String>,
271}
272
273#[function_component]
274pub fn ThemeSelector(props: &ThemeSelectorProps) -> Html {
275    let is_first = props
276        .theme
277        .as_ref()
278        .and_then(|x| props.themes.first().map(|y| y == x))
279        .unwrap_or_default();
280
281    let values = use_memo(props.themes.clone(), |themes| {
282        themes
283            .iter()
284            .cloned()
285            .map(SelectItem::Option)
286            .collect::<Vec<_>>()
287    });
288
289    match &props.theme {
290        None => html! {},
291        Some(selected) => {
292            html! {
293                if values.len() > 1 {
294                    <span class="hover-target">
295                        <div
296                            id="theme_icon"
297                            class={if is_first {""} else {"modified"}}
298                            tabindex="0"
299                            onclick={props.on_reset.reform(|_| ())}
300                        />
301                        <span id="theme" class="button">
302                            <Select<String>
303                                id="theme_selector"
304                                class="invert"
305                                {values}
306                                selected={selected.to_owned()}
307                                on_select={props.on_change.clone()}
308                            />
309                        </span>
310                    </span>
311                }
312            }
313        },
314    }
315}