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 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}