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 super::style::StyleSurface;
24use crate::utils::*;
25
26#[derive(Default)]
27struct FunctionDropDownState {
28 values: Vec<CompletionItemSuggestion>,
29 selected: usize,
30 on_select: Option<Callback<CompletionItemSuggestion>>,
31 target: Option<HtmlElement>,
32}
33
34#[derive(Clone, Default)]
35pub struct FunctionDropDownElement {
36 state: Rc<RefCell<FunctionDropDownState>>,
37 notify: Rc<PubSub<()>>,
38}
39
40impl PartialEq for FunctionDropDownElement {
41 fn eq(&self, other: &Self) -> bool {
42 Rc::ptr_eq(&self.state, &other.state)
43 }
44}
45
46impl ImplicitClone for FunctionDropDownElement {}
47
48impl FunctionDropDownElement {
49 pub fn reautocomplete(&self) {
50 self.notify.emit(());
51 }
52
53 pub fn autocomplete(
54 &self,
55 input: String,
56 target: HtmlElement,
57 callback: Callback<CompletionItemSuggestion>,
58 ) -> ApiResult<()> {
59 let values = filter_values(&input);
60 if values.is_empty() {
61 self.hide()?;
62 } else {
63 let mut s = self.state.borrow_mut();
64 s.values = values;
65 s.selected = 0;
66 s.on_select = Some(callback);
67 s.target = Some(target);
68 drop(s);
69 self.notify.emit(());
70 }
71
72 Ok(())
73 }
74
75 pub fn item_select(&self) {
76 let state = self.state.borrow();
77 if let Some(value) = state.values.get(state.selected)
78 && let Some(ref cb) = state.on_select
79 {
80 cb.emit(*value);
81 }
82 }
83
84 pub fn item_down(&self) {
85 let mut state = self.state.borrow_mut();
86 state.selected += 1;
87 if state.selected >= state.values.len() {
88 state.selected = 0;
89 }
90
91 drop(state);
92 self.notify.emit(());
93 }
94
95 pub fn item_up(&self) {
96 let mut state = self.state.borrow_mut();
97 if state.selected < 1 {
98 state.selected = state.values.len();
99 }
100
101 state.selected -= 1;
102 drop(state);
103 self.notify.emit(());
104 }
105
106 pub fn hide(&self) -> ApiResult<()> {
107 self.state.borrow_mut().target = None;
108 self.notify.emit(());
109 Ok(())
110 }
111}
112
113#[derive(Properties, PartialEq)]
114pub struct FunctionDropDownPortalProps {
115 pub element: FunctionDropDownElement,
116 pub theme: String,
117}
118
119pub struct FunctionDropDownPortal {
120 _sub: Subscription,
121}
122
123impl Component for FunctionDropDownPortal {
124 type Message = ();
125 type Properties = FunctionDropDownPortalProps;
126
127 fn create(ctx: &Context<Self>) -> Self {
128 let link = ctx.link().clone();
129 let sub = ctx
130 .props()
131 .element
132 .notify
133 .add_listener(move |()| link.send_message(()));
134 Self { _sub: sub }
135 }
136
137 fn update(&mut self, _ctx: &Context<Self>, _msg: ()) -> bool {
138 true
139 }
140
141 fn view(&self, ctx: &Context<Self>) -> Html {
142 let state = ctx.props().element.state.borrow();
143 let target = state.target.clone();
144 let on_close = {
145 let element = ctx.props().element.clone();
146 Callback::from(move |()| {
147 let _ = element.hide();
148 })
149 };
150
151 if target.is_some() {
152 let values = state.values.clone();
153 let selected = state.selected;
154 let on_select = state.on_select.clone();
155 drop(state);
156
157 html! {
158 <PortalModal
159 tag_name="perspective-dropdown"
160 surface={StyleSurface::FunctionDropdown}
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! { <>{ 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}