Skip to main content

perspective_viewer/components/
function_dropdown.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 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}