perspective_viewer/ui/containers/
select.rs1use std::borrow::{Borrow, Cow};
14use std::fmt::Display;
15use std::rc::Rc;
16
17use wasm_bindgen::JsCast;
18use yew::prelude::*;
19
20#[derive(Properties)]
21pub struct SelectProps<T>
22where
23 T: Clone + Display + PartialEq + 'static,
24{
25 pub values: Rc<Vec<SelectItem<T>>>,
26 pub selected: T,
27 pub on_select: Callback<T>,
28
29 #[prop_or_default]
30 pub is_autosize: bool,
31
32 #[prop_or_default]
33 pub label: Option<Cow<'static, str>>,
34
35 #[prop_or_default]
36 pub id: Option<&'static str>,
37
38 #[prop_or_default]
39 pub class: Option<String>,
40
41 #[prop_or_default]
42 pub wrapper_class: Option<String>,
43}
44
45impl<T> PartialEq for SelectProps<T>
46where
47 T: Clone + Display + PartialEq + 'static,
48{
49 fn eq(&self, rhs: &Self) -> bool {
50 self.selected == rhs.selected && self.values == rhs.values
51 }
52}
53
54pub enum SelectMsg {
55 SelectedChanged(i32),
56 KeyboardInput(bool, i32, String),
57}
58
59pub struct Select<T>
62where
63 T: Clone + Display + PartialEq + 'static,
64{
65 select_ref: NodeRef,
66 selected: T,
67}
68
69impl<T> Component for Select<T>
70where
71 T: Clone + Display + PartialEq + 'static,
72{
73 type Message = SelectMsg;
74 type Properties = SelectProps<T>;
75
76 fn create(_ctx: &Context<Self>) -> Self {
77 Self {
78 select_ref: NodeRef::default(),
79 selected: _ctx.props().selected.clone(),
80 }
81 }
82
83 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
84 match msg {
85 SelectMsg::SelectedChanged(x) => {
86 self.selected = find_nth(x, &ctx.props().values).unwrap().clone();
87 ctx.props().on_select.emit(self.selected.clone());
88 true
89 },
90
91 SelectMsg::KeyboardInput(is_shift, idx, code) => {
92 if is_shift {
93 if code.as_str() == "ArrowUp" {
94 if let Some(x) = find_nth(idx - 1, &ctx.props().values) {
95 self.selected = x.clone();
96 ctx.props().on_select.emit(self.selected.clone());
97 return true;
98 }
99 } else if code.as_str() == "ArrowDown"
100 && let Some(x) = find_nth(idx + 1, &ctx.props().values)
101 {
102 self.selected = x.clone();
103 ctx.props().on_select.emit(self.selected.clone());
104 return true;
105 }
106 }
107
108 false
109 },
110 }
111 }
112
113 fn changed(&mut self, ctx: &Context<Self>, _old: &Self::Properties) -> bool {
114 self.selected = ctx.props().selected.clone();
115 true
116 }
117
118 fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
119 if let Some(elem) = self.select_ref.cast::<web_sys::HtmlSelectElement>() {
120 elem.set_value(&format!("{}", self.selected))
121 }
122 }
123
124 fn view(&self, ctx: &Context<Self>) -> Html {
125 let callback = ctx.link().callback(|event: InputEvent| {
126 let value = event
127 .target()
128 .unwrap()
129 .unchecked_into::<web_sys::HtmlSelectElement>()
130 .selected_index();
131 SelectMsg::SelectedChanged(value)
132 });
133
134 let key_callback = ctx.link().callback(|event: KeyboardEvent| {
135 event.prevent_default();
136 let value = event
137 .target()
138 .unwrap()
139 .unchecked_into::<web_sys::HtmlSelectElement>()
140 .selected_index();
141
142 let is_shift = event.shift_key();
143 SelectMsg::KeyboardInput(is_shift, value, event.code())
144 });
145
146 let class = if let Some(class) = &ctx.props().class {
147 format!("noselect {class}")
148 } else {
149 "noselect".to_owned()
150 };
151
152 let is_group_selected = !ctx
153 .props()
154 .values
155 .iter()
156 .any(|x| matches!(x, SelectItem::Option(y) if *y == ctx.props().selected));
157
158 let select = html! {
159 <select
160 id={ctx.props().id}
161 {class}
162 ref={&self.select_ref}
163 oninput={callback}
164 onkeydown={key_callback}
165 >
166 { for ctx.props().values.iter().map(|value| match value {
167 SelectItem::Option(value) => {
168 let selected = *value == ctx.props().selected;
169 html! {
170 <option
171 key={ format!("{}", value) }
172 selected={ selected }
173 value={ format!("{value}") }>
174 { format!("{}", value) }
175 </option>
176 }
177 },
178 SelectItem::OptGroup(name, group) => html! {
179 <optgroup
180 key={ name.to_string() }
181 label={ name.to_string() }>
182 {
183 for group.iter().map(|value| {
184 let selected =
185 *value == ctx.props().selected;
186
187 let label = format!("{value}");
188 let category: &str = name.borrow();
189 let label = label
190 .strip_prefix(category)
191 .unwrap_or(&label)
192 .trim()
193 .to_owned();
194
195 html! {
196 <option
197 key={ format!("{}", value) }
198 selected={ selected }
199 value={ format!("{value}") }>
200 { label }
201 </option>
202 }
203 })
204 }
205 </optgroup>
206 }
207 }) }
208 </select>
209 };
210
211 let wrapper_class = match &ctx.props().wrapper_class {
212 Some(x) => classes!("dropdown-width-container", x),
213 None => classes!("dropdown-width-container"),
214 };
215
216 let value = if ctx.props().is_autosize {
217 Some(self.selected.to_string())
218 } else {
219 None
220 };
221
222 html! {
223 if is_group_selected && ctx.props().label.is_some() {
224 <label>
225 { ctx.props().label.as_ref().map(|x| x.to_string()).unwrap_or_default() }
226 </label>
227 <div class={wrapper_class} data-value={value}>
228 { select }
229 <span class="select-arrow" />
230 </div>
231 } else {
232 <div class={wrapper_class} data-value={value}>
233 { select }
234 <span class="select-arrow" />
235 </div>
236 }
237 }
238 }
239}
240
241#[derive(Clone, Eq, PartialEq)]
242pub enum SelectItem<T> {
243 Option(T),
244 OptGroup(Cow<'static, str>, Vec<T>),
245}
246
247impl<T: Display> SelectItem<T> {
248 pub fn name<'a>(&self) -> Cow<'a, str> {
249 match self {
250 Self::Option(x) => format!("{x}").into(),
251 Self::OptGroup(x, _) => x.clone(),
252 }
253 }
254}
255
256fn find_nth<T>(mut count: i32, items: &[SelectItem<T>]) -> Option<&T> {
257 for ref item in items.iter() {
258 match item {
259 SelectItem::Option(_) if count > 0 => {
260 count -= 1;
261 },
262 SelectItem::OptGroup(_, items) if count >= items.len() as i32 => {
263 count -= items.len() as i32;
264 },
265 SelectItem::OptGroup(_, items) => return items.get(count as usize),
266 SelectItem::Option(x) => return Some(x),
267 }
268 }
269
270 None
271}