Skip to main content

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 yew::prelude::*;
14
15use super::style::LocalStyle;
16use crate::css;
17use crate::utils::PtrEqRc;
18
19/// Pure value props — no engine handles, no PubSub subscriptions.
20/// The parent passes updated values whenever the renderer state changes.
21#[derive(Properties, PartialEq)]
22pub struct PluginSelectorProps {
23    /// Name of the currently active plugin.
24    pub plugin_name: Option<String>,
25
26    /// Flat list of all registered plugin names (all categories merged).
27    pub available_plugins: PtrEqRc<Vec<String>>,
28
29    /// Called when the user selects a different plugin.
30    pub on_select_plugin: Callback<String>,
31}
32
33#[derive(Debug)]
34pub enum PluginSelectorMsg {
35    ComponentSelectPlugin(String),
36    OpenMenu,
37}
38
39use PluginSelectorMsg::*;
40
41pub struct PluginSelector {
42    is_open: bool,
43}
44
45impl Component for PluginSelector {
46    type Message = PluginSelectorMsg;
47    type Properties = PluginSelectorProps;
48
49    fn create(_ctx: &Context<Self>) -> Self {
50        Self { is_open: false }
51    }
52
53    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
54        match msg {
55            ComponentSelectPlugin(plugin_name) => {
56                ctx.props().on_select_plugin.emit(plugin_name);
57                self.is_open = false;
58                false
59            },
60            OpenMenu => {
61                self.is_open = !self.is_open;
62                true
63            },
64        }
65    }
66
67    fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
68        true
69    }
70
71    fn view(&self, ctx: &Context<Self>) -> Html {
72        let callback = ctx.link().callback(|_| OpenMenu);
73        let plugin_name = ctx.props().plugin_name.clone().unwrap_or_default();
74        let plugin_name2 = plugin_name.clone();
75        let class = if self.is_open { "open" } else { "" };
76        let items = ctx
77            .props()
78            .available_plugins
79            .iter()
80            .filter(|x| x.as_str() != plugin_name2.as_str())
81            .map(|x| {
82                let callback = ctx.link().callback(ComponentSelectPlugin);
83                html! { <PluginSelect name={x.to_owned()} on_click={callback} /> }
84            });
85
86        html! {
87            <>
88                <LocalStyle href={css!("plugin-selector")} />
89                <div id="plugin_selector_container" {class}>
90                    <PluginSelect name={plugin_name} on_click={callback} />
91                    <div id="plugin_selector_border" />
92                    if self.is_open {
93                        <div class="plugin-selector-options scrollable">
94                            { items.collect::<Html>() }
95                        </div>
96                    }
97                </div>
98            </>
99        }
100    }
101}
102
103#[derive(Properties, PartialEq)]
104struct PluginSelectProps {
105    name: String,
106    on_click: Callback<String>,
107}
108
109#[function_component]
110fn PluginSelect(props: &PluginSelectProps) -> Html {
111    let name = props.name.clone();
112    let path: String = props
113        .name
114        .chars()
115        .map(|x| {
116            if x.is_alphanumeric() {
117                x.to_ascii_lowercase()
118            } else {
119                '-'
120            }
121        })
122        .collect();
123
124    html! {
125        <div
126            class="plugin-select-item"
127            data-plugin={name.clone()}
128            style={format!("--default-column-title:var(--psp-plugin-name--{}--content, \"{}\")", path, props.name)}
129            onclick={props.on_click.reform(move |_| name.clone())}
130        >
131            <span class="icon" />
132            <span class="plugin-select-item-name" />
133        </div>
134    }
135}