1use web_sys::HtmlInputElement;
2use yew::{
3 events::KeyboardEvent, html, Callback, Component, Context, Html, InputEvent, Properties,
4 TargetCast,
5};
6
7use std::cell::{Cell, RefCell};
8use std::rc::Rc;
9use std::str::FromStr;
10
11#[derive(Debug)]
15pub struct TypedInputParseError {}
16
17impl std::error::Error for TypedInputParseError {}
18
19impl std::fmt::Display for TypedInputParseError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
21 write!(f, "parse error for typed input")
22 }
23}
24
25#[derive(Clone)]
26pub struct RawAndParsed<T>
27where
28 T: FromStr + Clone,
29 <T as FromStr>::Err: Clone,
30{
31 raw_value: String,
32 parsed: Result<T, <T as FromStr>::Err>,
33}
34
35impl<T> RawAndParsed<T>
36where
37 T: FromStr + Clone,
38 <T as FromStr>::Err: Clone,
39{
40 fn empty() -> Self
42 where
43 T: std::fmt::Display,
44 {
45 let raw_value = "".to_string();
46 let parsed = raw_value.parse();
47 Self { raw_value, parsed }
48 }
49
50 fn from_initial(value: T) -> Self
52 where
53 T: std::fmt::Display,
54 {
55 let raw_value = format!("{}", value);
56 let parsed = Ok(value);
57 Self { raw_value, parsed }
58 }
59}
60
61fn new_focus_state() -> Rc<Cell<FocusState>> {
62 Rc::new(Cell::new(FocusState::IsBlurred))
63}
64
65#[derive(PartialEq, Clone)]
66pub struct TypedInputStorage<T>
67where
68 T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
69 <T as FromStr>::Err: Clone,
70{
71 rc: Rc<RefCell<TypedInputStorageInner<T>>>,
72}
73
74impl<T> Default for TypedInputStorage<T>
75where
76 T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
77 <T as FromStr>::Err: Clone,
78{
79 fn default() -> Self {
80 TypedInputStorage::empty()
81 }
82}
83
84impl<T> TypedInputStorage<T>
85where
86 T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
87 <T as FromStr>::Err: Clone,
88 Result<T, <T as FromStr>::Err>: Clone,
89{
90 pub fn empty() -> Self
92 where
93 T: std::fmt::Display,
94 {
95 Self::create(RawAndParsed::empty())
96 }
97
98 pub fn from_initial(value: T) -> Self
100 where
101 T: std::fmt::Display,
102 {
103 Self::create(RawAndParsed::from_initial(value))
104 }
105
106 fn create(raw_and_parsed: RawAndParsed<T>) -> Self
108 where
109 T: std::fmt::Display,
110 {
111 let inner = TypedInputStorageInner {
112 raw_and_parsed,
113 focus_state: new_focus_state(),
114 link: None,
115 };
116 let me = Rc::new(RefCell::new(inner));
117 Self { rc: me }
118 }
119
120 pub fn parsed(&self) -> Result<T, <T as FromStr>::Err> {
121 self.rc.borrow().raw_and_parsed.parsed.clone()
122 }
123
124 fn set_link(&mut self, link: yew::html::Scope<TypedInput<T>>) {
125 let mut inner = self.rc.borrow_mut();
126 inner.link = Some(link);
127 }
128
129 pub fn modify<F>(&mut self, f: F) -> Result<(), TypedInputParseError>
133 where
134 F: Fn(&mut T),
135 T: std::fmt::Display,
136 {
137 let mut inner = self.rc.borrow_mut();
138 let raw_value = match &mut inner.raw_and_parsed.parsed {
139 Ok(value) => {
140 f(value);
141 format!("{}", value)
142 }
143 Err(_e) => {
144 return Err(TypedInputParseError {});
145 }
146 };
147 if let Some(link) = &inner.link {
148 link.send_message(Msg::NewValue(raw_value));
149 }
150 Ok(())
151 }
152
153 pub fn get(&self) -> Result<T, <T as FromStr>::Err> {
155 self.rc.borrow_mut().raw_and_parsed.parsed.clone()
156 }
157
158 pub fn set_if_not_focused(&mut self, value: T)
162 where
163 T: std::fmt::Display,
164 {
165 use std::ops::Deref;
166 {
167 let mut inner = self.rc.borrow_mut();
168
169 match (*(inner.focus_state).deref()).get() {
170 FocusState::IsFocused => {}
171 FocusState::IsBlurred => {
172 inner.raw_and_parsed.raw_value = format!("{}", value);
173 inner.raw_and_parsed.parsed = Ok(value);
174 }
175 }
176 }
177 }
178}
179
180struct TypedInputStorageInner<T>
181where
182 T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
183 <T as FromStr>::Err: Clone,
184{
185 raw_and_parsed: RawAndParsed<T>,
186 focus_state: Rc<Cell<FocusState>>,
188 link: Option<yew::html::Scope<TypedInput<T>>>,
189}
190
191impl<T> PartialEq for TypedInputStorageInner<T>
192where
193 T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
194 <T as FromStr>::Err: Clone,
195{
196 fn eq(&self, rhs: &Self) -> bool {
197 Rc::ptr_eq(&self.focus_state, &rhs.focus_state)
200 && self.raw_and_parsed.raw_value == rhs.raw_and_parsed.raw_value
201 }
203}
204
205#[derive(PartialEq, Clone, Copy, Debug, Default)]
206enum FocusState {
207 IsFocused,
208 #[default]
209 IsBlurred,
210}
211
212pub struct TypedInput<T>
213where
214 T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
215 <T as FromStr>::Err: Clone,
216{
217 storage: TypedInputStorage<T>,
218}
219
220pub enum Msg {
221 NewValue(String),
222 OnFocus,
223 OnBlur,
224 SendValueIfValid,
225 Ignore,
226}
227
228#[derive(PartialEq, Properties)]
229pub struct Props<T>
230where
231 T: FromStr + Clone + PartialEq + std::fmt::Display + 'static,
232 <T as FromStr>::Err: Clone,
233{
234 pub storage: TypedInputStorage<T>,
236 #[prop_or_default]
238 pub placeholder: String,
239 #[prop_or_default]
241 pub on_send_valid: Option<Callback<T>>,
242 #[prop_or_default]
244 pub on_input: Option<Callback<RawAndParsed<T>>>,
245}
246
247impl<T> TypedInput<T>
248where
249 T: std::fmt::Display + FromStr + PartialEq + Clone,
250 <T as FromStr>::Err: Clone,
251{
252 fn send_value_if_valid(&mut self, on_send_valid: Option<&Callback<T>>) {
253 if let Some(callback) = on_send_valid {
254 if let Ok(value) = &self.storage.rc.borrow().raw_and_parsed.parsed {
255 callback.emit(value.clone());
256 }
257 }
258 }
259}
260
261impl<T> Component for TypedInput<T>
262where
263 T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
264 <T as FromStr>::Err: Clone,
265{
266 type Message = Msg;
267 type Properties = Props<T>;
268
269 fn create(ctx: &Context<Self>) -> Self {
270 let mut storage = ctx.props().storage.clone();
271 let link: yew::html::Scope<TypedInput<T>> = ctx.link().clone();
272 storage.set_link(link);
273 Self { storage }
274 }
275
276 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
277 match msg {
278 Msg::NewValue(raw_value) => {
279 let parsed = raw_value.parse();
280 let stor2 = {
281 let mut stor = self.storage.rc.borrow_mut();
282 stor.raw_and_parsed.raw_value = raw_value;
283 stor.raw_and_parsed.parsed = parsed;
284 stor.raw_and_parsed.clone()
285 };
286
287 if let Some(ref callback) = &ctx.props().on_input {
288 callback.emit(stor2);
289 }
290 }
291 Msg::OnFocus => {
292 let stor = self.storage.rc.borrow_mut();
293 stor.focus_state.replace(FocusState::IsFocused);
294 }
295 Msg::OnBlur => {
296 {
297 let stor = self.storage.rc.borrow_mut();
298 stor.focus_state.replace(FocusState::IsBlurred);
299 }
300 self.send_value_if_valid(ctx.props().on_send_valid.as_ref());
301 }
302 Msg::SendValueIfValid => {
303 self.send_value_if_valid(ctx.props().on_send_valid.as_ref());
304 return false;
305 }
306 Msg::Ignore => {
307 return false; }
309 }
310 true
311 }
312
313 fn view(&self, ctx: &Context<Self>) -> Html {
314 let value = self.storage.rc.borrow().raw_and_parsed.raw_value.clone();
315 let input_class = match &self.storage.rc.borrow().raw_and_parsed.parsed {
316 Ok(_) => "ranged-value-input",
317 Err(_) => "ranged-value-input-error",
318 };
319
320 html! {
321 <input type="text"
322 class={input_class}
323 placeholder={ctx.props().placeholder.clone()}
324 value={value}
325 oninput={ctx.link().callback(|e: InputEvent| {
326 let input: HtmlInputElement = e.target_unchecked_into();
327 Msg::NewValue(input.value())
328 })}
329 onfocus={ctx.link().callback(|_| Msg::OnFocus)}
330 onblur={ctx.link().callback(|_| Msg::OnBlur)}
331 onkeypress={ctx.link().callback(|e: KeyboardEvent| {
332 if e.key() == "Enter" { Msg::SendValueIfValid } else { Msg::Ignore }
333 })}
334 />
335 }
336 }
337}