perspective_viewer/components/
function_dropdown.rs1use std::cell::RefCell;
14use std::rc::Rc;
15
16use perspective_client::config::{COMPLETIONS, CompletionItemSuggestion};
17use perspective_js::utils::ApiResult;
18use web_sys::*;
19use yew::html::ImplicitClone;
20use yew::prelude::*;
21
22use super::portal::PortalModal;
23use crate::utils::*;
24
25static CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/css/function-dropdown.css"));
26
27#[derive(Default)]
28struct FunctionDropDownState {
29 values: Vec<CompletionItemSuggestion>,
30 selected: usize,
31 on_select: Option<Callback<CompletionItemSuggestion>>,
32 target: Option<HtmlElement>,
33}
34
35#[derive(Clone, Default)]
36pub struct FunctionDropDownElement {
37 state: Rc<RefCell<FunctionDropDownState>>,
38 notify: Rc<PubSub<()>>,
39}
40
41impl PartialEq for FunctionDropDownElement {
42 fn eq(&self, other: &Self) -> bool {
43 Rc::ptr_eq(&self.state, &other.state)
44 }
45}
46
47impl ImplicitClone for FunctionDropDownElement {}
48
49impl FunctionDropDownElement {
50 pub fn reautocomplete(&self) {
51 self.notify.emit(());
52 }
53
54 pub fn autocomplete(
55 &self,
56 input: String,
57 target: HtmlElement,
58 callback: Callback<CompletionItemSuggestion>,
59 ) -> ApiResult<()> {
60 let values = filter_values(&input);
61 if values.is_empty() {
62 self.hide()?;
63 } else {
64 let mut s = self.state.borrow_mut();
65 s.values = values;
66 s.selected = 0;
67 s.on_select = Some(callback);
68 s.target = Some(target);
69 drop(s);
70 self.notify.emit(());
71 }
72
73 Ok(())
74 }
75
76 pub fn item_select(&self) {
77 let state = self.state.borrow();
78 if let Some(value) = state.values.get(state.selected)
79 && let Some(ref cb) = state.on_select
80 {
81 cb.emit(*value);
82 }
83 }
84
85 pub fn item_down(&self) {
86 let mut state = self.state.borrow_mut();
87 state.selected += 1;
88 if state.selected >= state.values.len() {
89 state.selected = 0;
90 }
91
92 drop(state);
93 self.notify.emit(());
94 }
95
96 pub fn item_up(&self) {
97 let mut state = self.state.borrow_mut();
98 if state.selected < 1 {
99 state.selected = state.values.len();
100 }
101
102 state.selected -= 1;
103 drop(state);
104 self.notify.emit(());
105 }
106
107 pub fn hide(&self) -> ApiResult<()> {
108 self.state.borrow_mut().target = None;
109 self.notify.emit(());
110 Ok(())
111 }
112}
113
114#[derive(Properties, PartialEq)]
115pub struct FunctionDropDownPortalProps {
116 pub element: FunctionDropDownElement,
117 pub theme: String,
118}
119
120pub struct FunctionDropDownPortal {
121 _sub: Subscription,
122}
123
124impl Component for FunctionDropDownPortal {
125 type Message = ();
126 type Properties = FunctionDropDownPortalProps;
127
128 fn create(ctx: &Context<Self>) -> Self {
129 let link = ctx.link().clone();
130 let sub = ctx
131 .props()
132 .element
133 .notify
134 .add_listener(move |()| link.send_message(()));
135 Self { _sub: sub }
136 }
137
138 fn update(&mut self, _ctx: &Context<Self>, _msg: ()) -> bool {
139 true
140 }
141
142 fn view(&self, ctx: &Context<Self>) -> Html {
143 let state = ctx.props().element.state.borrow();
144 let target = state.target.clone();
145 let on_close = {
146 let element = ctx.props().element.clone();
147 Callback::from(move |()| {
148 let _ = element.hide();
149 })
150 };
151
152 if target.is_some() {
153 let values = state.values.clone();
154 let selected = state.selected;
155 let on_select = state.on_select.clone();
156 drop(state);
157
158 html! {
159 <PortalModal
160 tag_name="perspective-dropdown"
161 {target}
162 own_focus=false
163 {on_close}
164 theme={ctx.props().theme.clone()}
165 >
166 <FunctionDropDownView {values} {selected} {on_select} />
167 </PortalModal>
168 }
169 } else {
170 html! {}
171 }
172 }
173}
174
175#[derive(Properties, PartialEq)]
176struct FunctionDropDownViewProps {
177 values: Vec<CompletionItemSuggestion>,
178 selected: usize,
179 on_select: Option<Callback<CompletionItemSuggestion>>,
180}
181
182#[function_component]
183fn FunctionDropDownView(props: &FunctionDropDownViewProps) -> Html {
184 let body = html! {
185 if !props.values.is_empty() {
186 { for props.values
187 .iter()
188 .enumerate()
189 .map(|(idx, value)| {
190 let click = props.on_select.as_ref().unwrap().reform({
191 let value = *value;
192 move |_: MouseEvent| value
193 });
194
195 html! {
196 if idx == props.selected {
197 <div onmousedown={click} class="selected">
198 <span style="font-weight:500">{ value.label }</span>
199 <br/>
200 <span style="padding-left:12px">{ value.documentation }</span>
201 </div>
202 } else {
203 <div onmousedown={click}>
204 <span style="font-weight:500">{ value.label }</span>
205 <br/>
206 <span style="padding-left:12px">{ value.documentation }</span>
207 </div>
208 }
209 }
210 }) }
211 }
212 };
213
214 html! { <><style>{ CSS }</style>{ body }</> }
215}
216
217fn filter_values(input: &str) -> Vec<CompletionItemSuggestion> {
218 let input = input.to_lowercase();
219 COMPLETIONS
220 .iter()
221 .filter(|x| x.label.to_lowercase().starts_with(&input))
222 .cloned()
223 .collect::<Vec<_>>()
224}