Skip to main content

perspective_viewer/
presentation.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
13mod column_locator;
14mod props;
15mod sheets;
16
17use std::cell::RefCell;
18use std::collections::{HashMap, HashSet};
19use std::ops::Deref;
20use std::rc::Rc;
21
22use async_lock::Mutex;
23use perspective_js::utils::{ApiFuture, ApiResult};
24use web_sys::*;
25use yew::html::ImplicitClone;
26use yew::prelude::*;
27
28pub use self::column_locator::{ColumnLocator, ColumnSettingsTab, ColumnTab, OpenColumnSettings};
29pub use self::props::PresentationProps;
30use crate::config::{ColumnConfigUpdate, ColumnConfigValueUpdate, ColumnConfigValues};
31use crate::utils::*;
32
33pub type ColumnConfigMap = HashMap<String, ColumnConfigValues>;
34
35/// The available themes as detected in the browser environment or set
36/// explicitly when CORS prevents detection.  Detection is expensive and
37/// typically must be performed only once, when `document.styleSheets` is
38/// up-to-date.
39#[derive(Default)]
40struct ThemeData {
41    themes: Option<Vec<String>>,
42}
43
44/// Actual presentations tate struct with some fields hidden.
45pub struct PresentationHandle {
46    viewer_elem: HtmlElement,
47    theme_data: Mutex<ThemeData>,
48    is_settings_open: RefCell<bool>,
49    open_column_settings: RefCell<OpenColumnSettings>,
50    columns_config: RefCell<ColumnConfigMap>,
51    is_workspace: RefCell<Option<bool>>,
52    pub settings_open_changed: PubSub<bool>,
53
54    /// Injected callback from the root component, replacing the former
55    /// `is_workspace_changed: PubSub` field.
56    pub on_is_workspace_changed: RefCell<Option<Callback<bool>>>,
57    pub settings_before_open_changed: PubSub<bool>,
58    pub column_settings_open_changed: PubSub<(bool, Option<String>)>,
59    pub theme_config_updated: PubSub<(PtrEqRc<Vec<String>>, Option<usize>)>,
60    pub on_eject: PubSub<()>,
61}
62
63/// State object responsible for the non-persistable/gui element state,
64/// including Themes, panel open state and realtive size, title, etc.
65#[derive(Clone)]
66pub struct Presentation(Rc<PresentationHandle>);
67
68impl PartialEq for Presentation {
69    fn eq(&self, other: &Self) -> bool {
70        Rc::ptr_eq(&self.0, &other.0)
71    }
72}
73
74impl Deref for Presentation {
75    type Target = PresentationHandle;
76
77    fn deref(&self) -> &Self::Target {
78        &self.0
79    }
80}
81
82impl ImplicitClone for Presentation {}
83
84impl Presentation {
85    pub fn new(elem: &HtmlElement) -> Self {
86        let theme = Self(Rc::new(PresentationHandle {
87            viewer_elem: elem.clone(),
88            theme_data: Default::default(),
89            is_workspace: Default::default(),
90            settings_open_changed: Default::default(),
91            settings_before_open_changed: Default::default(),
92            column_settings_open_changed: Default::default(),
93            on_is_workspace_changed: Default::default(),
94            columns_config: Default::default(),
95            is_settings_open: Default::default(),
96            open_column_settings: Default::default(),
97            theme_config_updated: PubSub::default(),
98            on_eject: PubSub::default(),
99        }));
100
101        ApiFuture::spawn(theme.clone().init());
102        theme
103    }
104
105    pub fn is_visible(&self) -> bool {
106        self.viewer_elem
107            .offset_parent()
108            .map(|x| !x.is_null())
109            .unwrap_or(false)
110    }
111
112    pub fn is_active(&self, elem: &Option<Element>) -> bool {
113        elem.is_some() && &self.viewer_elem.shadow_root().unwrap().active_element() == elem
114    }
115
116    pub fn reset_attached(&self) {
117        *self.0.is_workspace.borrow_mut() = None;
118        if let Some(cb) = self.on_is_workspace_changed.borrow().as_ref() {
119            cb.emit(self.get_is_workspace());
120        }
121    }
122
123    pub fn get_is_workspace(&self) -> bool {
124        if self.is_workspace.borrow().is_none() {
125            if !self.viewer_elem.is_connected() {
126                return false;
127            }
128
129            let is_workspace = self
130                .viewer_elem
131                .parent_element()
132                .map(|x| x.tag_name() == "PERSPECTIVE-WORKSPACE")
133                .unwrap_or_default();
134
135            *self.is_workspace.borrow_mut() = Some(is_workspace);
136        }
137
138        self.is_workspace.borrow().unwrap()
139    }
140
141    pub fn set_settings_attribute(&self, opt: bool) {
142        self.viewer_elem
143            .toggle_attribute_with_force("settings", opt)
144            .unwrap();
145    }
146
147    pub fn is_settings_open(&self) -> bool {
148        *self.is_settings_open.borrow()
149    }
150
151    pub fn set_settings_before_open(&self, open: bool) {
152        if *self.is_settings_open.borrow() != open {
153            *self.is_settings_open.borrow_mut() = open;
154            self.set_settings_attribute(open);
155            self.settings_before_open_changed.emit(open);
156        }
157    }
158
159    pub fn set_settings_open(&self, open: bool) {
160        self.settings_open_changed.emit(open);
161    }
162
163    /// Sets the currently opened column settings. Emits an internal event on
164    /// change. Passing None is a shorthand for setting all fields to
165    /// None.
166    pub fn set_open_column_settings(&self, settings: Option<OpenColumnSettings>) {
167        let settings = settings.unwrap_or_default();
168        if *(self.open_column_settings.borrow()) != settings {
169            settings.clone_into(&mut *self.open_column_settings.borrow_mut());
170            self.column_settings_open_changed
171                .emit((true, settings.name()));
172        }
173    }
174
175    /// Gets a clone of the current OpenColumnSettings.
176    pub fn get_open_column_settings(&self) -> OpenColumnSettings {
177        self.open_column_settings.borrow().deref().clone()
178    }
179
180    async fn init(self) -> ApiResult<()> {
181        self.set_theme_attribute(self.get_selected_theme_name().await.as_deref())
182    }
183
184    /// Get the available theme names from the browser environment by parsing
185    /// readable stylesheets.  This method is memoized - the state can be
186    /// flushed by calling `reset()`.
187    pub async fn get_available_themes(&self) -> ApiResult<PtrEqRc<Vec<String>>> {
188        let mut data = self.0.theme_data.lock().await;
189        if data.themes.is_none() {
190            await_dom_loaded().await?;
191            let themes = sheets::get_theme_names(&self.0.viewer_elem)?;
192            data.themes = Some(themes);
193        }
194
195        Ok(data.themes.clone().unwrap().into())
196    }
197
198    /// Reset the state.  `styleSheets` will be re-parsed next time
199    /// `get_themes()` is called if the `themes` argument is `None`.
200    ///
201    /// # Returns
202    /// A `bool` indicating whether the internal state changed.
203    pub async fn reset_available_themes(&self, themes: Option<Vec<String>>) -> bool {
204        fn as_set(x: &Option<Vec<String>>) -> HashSet<&'_ String> {
205            x.as_ref()
206                .map(|x| x.iter().collect::<HashSet<_>>())
207                .unwrap_or_default()
208        }
209
210        let mut mutex = self.0.theme_data.lock().await;
211        let changed = as_set(&mutex.themes) != as_set(&themes);
212        mutex.themes = themes;
213        changed
214    }
215
216    pub async fn get_selected_theme_config(
217        &self,
218    ) -> ApiResult<(PtrEqRc<Vec<String>>, Option<usize>)> {
219        let themes = self.get_available_themes().await?;
220        let name = self.0.viewer_elem.get_attribute("theme");
221        let index = name
222            .and_then(|x| themes.iter().position(|y| y == &x))
223            .or(if !themes.is_empty() { Some(0) } else { None });
224
225        Ok((themes, index))
226    }
227
228    /// Returns the currently applied theme, or the default theme if no theme
229    /// has been set and themes are detected in the `document`, or `None` if
230    /// no themes are available.
231    pub async fn get_selected_theme_name(&self) -> Option<String> {
232        let (themes, index) = self.get_selected_theme_config().await.ok()?;
233        index.and_then(|x| themes.get(x).cloned())
234    }
235
236    fn set_theme_attribute(&self, theme: Option<&str>) -> ApiResult<()> {
237        if let Some(theme) = theme {
238            Ok(self.0.viewer_elem.set_attribute("theme", theme)?)
239        } else {
240            Ok(self.0.viewer_elem.remove_attribute("theme")?)
241        }
242    }
243
244    pub async fn reset_theme(&self) -> ApiResult<()> {
245        *self.0.is_workspace.borrow_mut() = None;
246        let themes = self.get_available_themes().await?;
247        let default_theme = themes.first().map(|x| x.as_str());
248        self.set_theme_name(default_theme).await?;
249        Ok(())
250    }
251
252    /// Set the theme by name, or `None` for the default theme.
253    ///
254    /// # Returns
255    /// A `bool` indicating whether the internal state changed.
256    pub async fn set_theme_name(&self, theme: Option<&str>) -> ApiResult<bool> {
257        let (themes, selected) = self.get_selected_theme_config().await?;
258        if let Some(x) = selected
259            && themes.get(x).map(|x| x.as_str()) == theme
260        {
261            return Ok(false);
262        }
263
264        let index = if let Some(theme) = theme {
265            self.set_theme_attribute(Some(theme))?;
266            themes.iter().position(|x| x == theme)
267        } else if !themes.is_empty() {
268            self.set_theme_attribute(themes.first().map(|x| x.as_str()))?;
269            Some(0)
270        } else {
271            self.set_theme_attribute(None)?;
272            None
273        };
274
275        self.theme_config_updated.emit((themes, index));
276        Ok(true)
277    }
278
279    /// Returns an owned copy of the curent column configuration map.
280    pub fn all_columns_configs(&self) -> ColumnConfigMap {
281        self.columns_config.borrow().clone()
282    }
283
284    pub fn reset_columns_configs(&self) {
285        *self.columns_config.borrow_mut() = ColumnConfigMap::new();
286    }
287
288    /// Gets a clone of the ColumnConfig for the given column name.
289    pub fn get_columns_config(&self, column_name: &str) -> Option<ColumnConfigValues> {
290        self.columns_config.borrow().get(column_name).cloned()
291    }
292
293    /// Updates the entire column config struct. (like from a restore() call)
294    pub fn update_columns_configs(&self, update: ColumnConfigUpdate) {
295        match update {
296            crate::config::OptionalUpdate::SetDefault => {
297                let mut config = self.columns_config.borrow_mut();
298                *config = HashMap::default()
299            },
300            crate::config::OptionalUpdate::Missing => {},
301            crate::config::OptionalUpdate::Update(update) => {
302                for (col_name, new_config) in update.into_iter() {
303                    self.columns_config
304                        .borrow_mut()
305                        .insert(col_name, new_config);
306                }
307            },
308        }
309    }
310
311    pub fn update_columns_config_value(
312        &self,
313        column_name: String,
314        update: ColumnConfigValueUpdate,
315    ) {
316        let mut config = self.columns_config.borrow_mut();
317        let value = config.remove(&column_name).unwrap_or_default();
318        let update = value.update(update);
319        if !update.is_empty() {
320            config.insert(column_name, update);
321        }
322    }
323
324    /// Snapshot the current presentation state as a [`PresentationProps`]
325    /// value suitable for passing as a Yew prop.  Called by the root component
326    /// whenever a presentation-related PubSub event fires.
327    ///
328    /// `available_themes` must be provided by the caller because theme
329    /// detection is async and therefore not available synchronously here.
330    pub fn to_props(&self, available_themes: PtrEqRc<Vec<String>>) -> PresentationProps {
331        let theme_attr = self.0.viewer_elem.get_attribute("theme");
332        let selected_theme = theme_attr.as_deref().and_then(|name| {
333            available_themes
334                .iter()
335                .find(|x| x.as_str() == name)
336                .cloned()
337        });
338
339        PresentationProps {
340            is_settings_open: self.is_settings_open(),
341            available_themes,
342            selected_theme,
343            open_column_settings: self.get_open_column_settings(),
344            is_workspace: self.get_is_workspace(),
345        }
346    }
347}