perspective_viewer/components/
plugin_selector.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 perspective_client::config::ViewConfigUpdate;
14use yew::prelude::*;
15
16use super::containers::select::*;
17use super::style::LocalStyle;
18use crate::config::*;
19use crate::js::*;
20use crate::model::*;
21use crate::presentation::Presentation;
22use crate::renderer::*;
23use crate::session::*;
24use crate::utils::*;
25use crate::*;
26
27#[derive(Properties, PartialEq)]
28pub struct PluginSelectorProps {
29    pub renderer: Renderer,
30    pub session: Session,
31    pub presentation: Presentation,
32
33    #[cfg(test)]
34    #[prop_or_default]
35    pub weak_link: WeakScope<PluginSelector>,
36}
37
38derive_model!(Renderer, Session, Presentation for PluginSelectorProps);
39
40#[derive(Debug)]
41pub enum PluginSelectorMsg {
42    ComponentSelectPlugin(String),
43    RendererSelectPlugin(String),
44    OpenMenu,
45}
46
47use PluginSelectorMsg::*;
48
49pub struct PluginSelector {
50    options: Vec<SelectItem<String>>,
51    is_open: bool,
52    _plugin_sub: Subscription,
53}
54
55impl Component for PluginSelector {
56    type Message = PluginSelectorMsg;
57    type Properties = PluginSelectorProps;
58
59    fn create(ctx: &Context<Self>) -> Self {
60        let options = generate_plugin_optgroups(&ctx.props().renderer);
61        let _plugin_sub = ctx.props().renderer.plugin_changed.add_listener({
62            let link = ctx.link().clone();
63            move |plugin: JsPerspectiveViewerPlugin| {
64                let name = plugin.name();
65                link.send_message(PluginSelectorMsg::RendererSelectPlugin(name))
66            }
67        });
68
69        Self {
70            options,
71            is_open: false,
72            _plugin_sub,
73        }
74    }
75
76    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
77        match msg {
78            RendererSelectPlugin(_plugin_name) => true,
79            ComponentSelectPlugin(plugin_name) => {
80                if !ctx.props().session.is_errored() {
81                    let metadata = ctx
82                        .props()
83                        .renderer
84                        .get_next_plugin_metadata(&PluginUpdate::Update(plugin_name));
85
86                    let mut update = ViewConfigUpdate::default();
87                    ctx.props().session.set_update_column_defaults(
88                        &mut update,
89                        metadata
90                            .as_ref()
91                            .unwrap_or(&*ctx.props().renderer.metadata()),
92                    );
93
94                    if let Ok(task) = ctx.props().update_and_render(update) {
95                        ApiFuture::spawn(task);
96                    }
97
98                    ctx.props().presentation.set_open_column_settings(None);
99                    self.is_open = false;
100                    false
101                } else {
102                    self.is_open = false;
103                    true
104                }
105            },
106            OpenMenu => {
107                self.is_open = !self.is_open;
108                true
109            },
110        }
111    }
112
113    fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
114        true
115    }
116
117    fn view(&self, ctx: &Context<Self>) -> Html {
118        let callback = ctx.link().callback(|_| OpenMenu);
119
120        let plugin_name = ctx.props().renderer.get_active_plugin().unwrap().name();
121        let plugin_name2 = plugin_name.clone();
122        let class = if self.is_open { "open" } else { "" };
123        let items = self.options.iter().map(|item| match item {
124            SelectItem::OptGroup(_cat, items) => html! {
125                items.iter().filter(|x| *x != &plugin_name2).map(|x| {
126                    let callback = ctx.link().callback(ComponentSelectPlugin);
127                    html! {
128                        <PluginSelect
129                            name={ x.to_owned() }
130                            on_click={ callback } />
131                    }
132                }).collect::<Html>()
133            },
134            SelectItem::Option(_item) => html! {},
135        });
136
137        html! {
138            <>
139                <LocalStyle href={css!("plugin-selector")} />
140                <div id="plugin_selector_container" {class}>
141                    <PluginSelect name={plugin_name} on_click={callback} />
142                    <div id="plugin_selector_border" />
143                    if self.is_open {
144                        <div class="plugin-selector-options">{ items.collect::<Html>() }</div>
145                    }
146                </div>
147            </>
148        }
149    }
150}
151
152/// Generate the opt groups for the plugin selector by collecting by category
153/// then sorting.
154fn generate_plugin_optgroups(renderer: &Renderer) -> Vec<SelectItem<String>> {
155    let mut options = renderer
156        .get_all_plugin_categories()
157        .into_iter()
158        .map(|(category, value)| SelectItem::OptGroup(category.into(), value))
159        .collect::<Vec<_>>();
160
161    options.sort_by_key(|x| x.name());
162    options
163}
164
165#[derive(Properties, PartialEq)]
166struct PluginSelectProps {
167    name: String,
168    on_click: Callback<String>,
169}
170
171#[function_component]
172fn PluginSelect(props: &PluginSelectProps) -> Html {
173    let name = props.name.clone().tee::<2>();
174    let path: String = props
175        .name
176        .chars()
177        .map(|x| {
178            if x.is_alphanumeric() {
179                x.to_ascii_lowercase()
180            } else {
181                '-'
182            }
183        })
184        .collect();
185
186    html! {
187        <div
188            class="plugin-select-item"
189            data-plugin={name.0}
190            style={format!("--default-column-title:var(--plugin-name-{}--content, \"{}\")", path, props.name)}
191            onclick={props.on_click.reform(move |_| name.1.clone())}
192        >
193            <span class="plugin-select-item-name" />
194        </div>
195    }
196}