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 wasm_bindgen::JsCast;
14use web_sys::*;
15use yew::prelude::*;
16
17use super::status_indicator::StatusIndicator;
18use super::style::LocalStyle;
19use crate::components::containers::select::*;
20use crate::components::status_bar_counter::StatusBarRowsCounter;
21use crate::custom_elements::copy_dropdown::*;
22use crate::custom_elements::export_dropdown::*;
23use crate::presentation::Presentation;
24use crate::renderer::*;
25use crate::session::*;
26#[cfg(test)]
27use crate::utils::WeakScope;
28use crate::utils::*;
29use crate::*;
30
31#[derive(Properties, Clone)]
32pub struct StatusBarProps {
33    pub id: String,
34    pub on_reset: Callback<bool>,
35    pub session: Session,
36    pub renderer: Renderer,
37    pub presentation: Presentation,
38
39    #[cfg(test)]
40    #[prop_or_default]
41    pub weak_link: WeakScope<StatusBar>,
42}
43
44derive_model!(Renderer, Session, Presentation for StatusBarProps);
45
46impl PartialEq for StatusBarProps {
47    fn eq(&self, other: &Self) -> bool {
48        self.id == other.id
49    }
50}
51
52pub enum StatusBarMsg {
53    Reset(bool),
54    Export,
55    Copy,
56    Noop,
57    SetThemeConfig((Vec<String>, Option<usize>)),
58    SetTheme(String),
59    // SetError(Option<String>),
60    // TableStatsChanged,
61    // SetIsUpdating(bool),
62    SetTitle(Option<String>),
63}
64
65/// A toolbar with buttons, and `Table` & `View` status information.
66pub struct StatusBar {
67    is_updating: i32,
68    theme: Option<String>,
69    themes: Vec<String>,
70    export_ref: NodeRef,
71    copy_ref: NodeRef,
72    _sub: [Subscription; 2],
73}
74
75impl Component for StatusBar {
76    type Message = StatusBarMsg;
77    type Properties = StatusBarProps;
78
79    fn create(ctx: &Context<Self>) -> Self {
80        let _sub = [
81            ctx.props()
82                .presentation
83                .theme_config_updated
84                .add_listener(ctx.link().callback(StatusBarMsg::SetThemeConfig)),
85            ctx.props()
86                .presentation
87                .title_changed
88                .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
89        ];
90
91        // Fetch initial theme
92        let presentation = ctx.props().presentation.clone();
93        let on_theme = ctx.link().callback(StatusBarMsg::SetThemeConfig);
94        ApiFuture::spawn(async move {
95            on_theme.emit(presentation.get_selected_theme_config().await?);
96            Ok(())
97        });
98
99        Self {
100            _sub,
101            theme: None,
102            themes: vec![],
103            copy_ref: NodeRef::default(),
104            export_ref: NodeRef::default(),
105            is_updating: 0,
106        }
107    }
108
109    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
110        match msg {
111            StatusBarMsg::Reset(all) => {
112                ctx.props().on_reset.emit(all);
113                false
114            },
115            StatusBarMsg::SetThemeConfig((themes, index)) => {
116                let new_theme = index.and_then(|x| themes.get(x)).cloned();
117                let should_render = new_theme != self.theme || self.themes != themes;
118                self.theme = new_theme;
119                self.themes = themes;
120                should_render
121            },
122            StatusBarMsg::SetTheme(theme_name) => {
123                clone!(
124                    ctx.props().renderer,
125                    ctx.props().session,
126                    ctx.props().presentation
127                );
128                ApiFuture::spawn(async move {
129                    presentation.set_theme_name(Some(&theme_name)).await?;
130                    let view = session.get_view().into_apierror()?;
131                    renderer.restyle_all(&view).await
132                });
133
134                false
135            },
136            StatusBarMsg::Export => {
137                let target = self.export_ref.cast::<HtmlElement>().unwrap();
138                ExportDropDownMenuElement::new_from_model(ctx.props()).open(target);
139                false
140            },
141            StatusBarMsg::Copy => {
142                let target = self.copy_ref.cast::<HtmlElement>().unwrap();
143                CopyDropDownMenuElement::new_from_model(ctx.props()).open(target);
144                false
145            },
146            StatusBarMsg::Noop => true,
147            StatusBarMsg::SetTitle(title) => {
148                ctx.props().presentation.set_title(title);
149                false
150            },
151        }
152    }
153
154    fn view(&self, ctx: &Context<Self>) -> Html {
155        let mut is_updating_class_name = classes!();
156        if self.is_updating > 0 {
157            is_updating_class_name.push("updating")
158        };
159
160        if ctx.props().presentation.get_title().is_some() {
161            is_updating_class_name.push("titled")
162        };
163
164        let reset = ctx
165            .link()
166            .callback(|event: MouseEvent| StatusBarMsg::Reset(event.shift_key()));
167
168        let export = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Export);
169        let copy = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Copy);
170        let theme_button = match &self.theme {
171            None => html! {},
172            Some(selected) => {
173                let ontheme = ctx.link().callback(StatusBarMsg::SetTheme);
174                let values = self
175                    .themes
176                    .iter()
177                    .cloned()
178                    .map(SelectItem::Option)
179                    .collect::<Vec<_>>();
180
181                html! {
182                    if values.len() > 1 {
183                        <span class="hover-target">
184                            <span id="theme" class="button">
185                                <Select<String>
186                                    id="theme_selector"
187                                    {values}
188                                    selected={selected.to_owned()}
189                                    on_select={ontheme}
190                                />
191                            </span>
192                        </span>
193                    }
194                }
195            },
196        };
197
198        let oninput = ctx.link().callback({
199            move |input: InputEvent| {
200                let title = input
201                    .target()
202                    .unwrap()
203                    .unchecked_into::<HtmlInputElement>()
204                    .value();
205
206                let title = if title.trim().is_empty() {
207                    None
208                } else {
209                    Some(title)
210                };
211
212                StatusBarMsg::SetTitle(title)
213            }
214        });
215
216        let is_menu = ctx.props().session.has_table()
217            && (ctx.props().presentation.is_settings_open()
218                || ctx.props().presentation.get_title().is_some());
219
220        if !ctx.props().session.has_table() {
221            is_updating_class_name.push("updating");
222        }
223
224        html! {
225            <>
226                <LocalStyle href={css!("status-bar")} />
227                <div id={ctx.props().id.clone()} class={is_updating_class_name}>
228                    <StatusIndicator
229                        session={&ctx.props().session}
230                        renderer={&ctx.props().renderer}
231                    />
232                    if is_menu {
233                        <label
234                            class="input-sizer"
235                            data-value={ctx.props().presentation.get_title().unwrap_or_default()}
236                        >
237                            <input
238                                placeholder=" "
239                                value={ctx.props().presentation.get_title()}
240                                size="10"
241                                {oninput}
242                            />
243                            <span id="status-bar-placeholder" />
244                        </label>
245                    }
246                    <StatusBarRowsCounter session={&ctx.props().session} />
247                    <div id="spacer" />
248                    if is_menu {
249                        <div id="menu-bar" class="section">
250                            { theme_button }
251                            <div id="plugin-settings"><slot name="plugin-settings" /></div>
252                            <span class="hover-target">
253                                <span id="reset" class="button" onmousedown={reset}><span /></span>
254                            </span>
255                            <span class="hover-target" ref={&self.export_ref} onmousedown={export}>
256                                <span id="export" class="button"><span /></span>
257                            </span>
258                            <span class="hover-target" ref={&self.copy_ref} onmousedown={copy}>
259                                <span id="copy" class="button"><span /></span>
260                            </span>
261                        </div>
262                    }
263                </div>
264            </>
265        }
266    }
267}