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