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