tui_components/components/
num_input.rs

1use std::fmt::Display;
2use std::marker::PhantomData;
3
4use crossterm::event::KeyCode;
5use num::traits::{FromPrimitive, SaturatingAdd, SaturatingMul, SaturatingSub};
6use num::{Bounded, Float, Integer, Signed, Unsigned};
7use tui::style::{Color, Style};
8use tui::text::{Span, Spans};
9use tui::widgets::{Paragraph, Widget};
10use tui::{buffer::Buffer, layout::Rect};
11
12use crate::{Component, Event, Spannable};
13
14#[derive(Debug, Clone, Copy)]
15pub enum NumInputResponse {
16    None,
17    Submit,
18    Cancel,
19}
20
21#[derive(Debug)]
22pub struct SignedIntInput<T: InputSignedInt> {
23    current: T,
24    negative: bool,
25}
26
27impl<T: InputSignedInt> SignedIntInput<T> {
28    pub fn new(initial_value: T) -> Self {
29        Self {
30            current: initial_value,
31            negative: initial_value.is_negative(),
32        }
33    }
34
35    pub fn set(&mut self, value: T) {
36        self.current = value.clamp(T::min_value(), T::max_value());
37        // If the user removes all digits, keep the sign the same
38        if value != T::zero() {
39            self.negative = value.is_negative();
40        }
41    }
42
43    pub fn add(&mut self, value: T) -> &mut Self {
44        self.set(self.current.saturating_add(&value));
45        self
46    }
47
48    pub fn sub(&mut self, value: T) -> &mut Self {
49        self.set(self.current.saturating_sub(&value));
50        self
51    }
52
53    pub fn multiply(&mut self, value: T) -> &mut Self {
54        self.set(self.current.saturating_mul(&value));
55        self
56    }
57
58    pub fn invert(&mut self) {
59        if self.current == T::zero() {
60            self.negative = !self.negative;
61        } else {
62            self.set(T::zero().saturating_sub(&self.current))
63        }
64    }
65
66    pub fn remove_digit(&mut self) {
67        // integer division with 10
68        self.set(self.current / T::from_u32(10).unwrap())
69    }
70
71    pub fn value(&self) -> T {
72        self.current
73    }
74
75    pub fn append_digit(&mut self, digit: char) -> bool {
76        if let Some(dig) = digit.to_digit(10) {
77            // instead of converting to string, just multiply by 10 and add/sub
78            // the digit. This way, we also cap out at the min/max of the number
79            if self.negative {
80                self.multiply(T::from_u32(10).unwrap())
81                    .sub(T::from_u32(dig).unwrap());
82            } else {
83                self.multiply(T::from_u32(10).unwrap())
84                    .add(T::from_u32(dig).unwrap());
85            }
86            true
87        } else {
88            false
89        }
90    }
91}
92
93impl<T: InputSignedInt> Component for SignedIntInput<T> {
94    type Response = NumInputResponse;
95    type DrawResponse = ();
96
97    fn handle_event(&mut self, event: crate::Event) -> Self::Response {
98        if let Event::Key(key_event) = event {
99            match key_event.code {
100                KeyCode::Char(c) => {
101                    if !self.append_digit(c) && c == '-' {
102                        self.invert();
103                    }
104                }
105                KeyCode::Backspace => {
106                    self.remove_digit();
107                }
108                KeyCode::Up => {
109                    self.add(T::one());
110                }
111                KeyCode::Down => {
112                    self.sub(T::one());
113                }
114                KeyCode::Enter => return NumInputResponse::Submit,
115                KeyCode::Esc => return NumInputResponse::Cancel,
116                _ => {}
117            }
118        }
119        NumInputResponse::None
120    }
121
122    fn draw(&mut self, rect: Rect, buffer: &mut Buffer) -> Self::DrawResponse {
123        let text = Paragraph::new(self.get_spans());
124        Widget::render(text, rect, buffer);
125    }
126}
127
128impl<T: InputSignedInt> Spannable for SignedIntInput<T> {
129    fn get_spans<'a, 'b>(&'a self) -> tui::text::Spans<'b> {
130        let mut spans = Spans::default();
131        spans.0.push(Span::styled(
132            String::from(if self.negative { "- " } else { "+ " }),
133            Style::default().fg(Color::Green),
134        ));
135        let number_no_sign = if self.current.is_negative() {
136            let base = format!("{}", self.current);
137            if !base.is_empty() {
138                String::from(&format!("{}", self.current)[1..])
139            } else {
140                base
141            }
142        } else {
143            format!("{}", self.current)
144        };
145        spans.0.push(Span::raw(number_no_sign));
146        if self.current == T::max_value() {
147            spans.0.push(Span::styled(
148                String::from(" (max value)"),
149                Style::default().fg(Color::Gray),
150            ))
151        } else if self.current == T::min_value() {
152            spans.0.push(Span::styled(
153                String::from(" (min value)"),
154                Style::default().fg(Color::Gray),
155            ))
156        }
157        spans
158    }
159}
160
161#[derive(Debug)]
162pub struct UnsignedIntInput<T: InputUnsignedInt> {
163    current: T,
164}
165
166impl<T: InputUnsignedInt> UnsignedIntInput<T> {
167    pub fn new(initial_value: T) -> Self {
168        Self {
169            current: initial_value,
170        }
171    }
172
173    pub fn set(&mut self, value: T) {
174        self.current = value.clamp(T::min_value(), T::max_value());
175    }
176
177    pub fn add(&mut self, value: T) -> &mut Self {
178        self.set(self.current.saturating_add(&value));
179        self
180    }
181
182    pub fn sub(&mut self, value: T) -> &mut Self {
183        self.set(self.current.saturating_sub(&value));
184        self
185    }
186
187    pub fn multiply(&mut self, value: T) -> &mut Self {
188        self.set(self.current.saturating_mul(&value));
189        self
190    }
191
192    pub fn remove_digit(&mut self) {
193        // integer division with 10
194        self.set(self.current / T::from_u32(10).unwrap())
195    }
196
197    pub fn value(&self) -> T {
198        self.current
199    }
200
201    pub fn append_digit(&mut self, digit: char) -> bool {
202        if let Some(dig) = digit.to_digit(10) {
203            // instead of converting to string, just multiply by 10 and add/sub
204            // the digit. This way, we also cap out at the min/max of the number
205            self.multiply(T::from_u32(10).unwrap())
206                .add(T::from_u32(dig).unwrap());
207            true
208        } else {
209            false
210        }
211    }
212}
213
214impl<T: InputUnsignedInt> Component for UnsignedIntInput<T> {
215    type Response = NumInputResponse;
216    type DrawResponse = ();
217
218    fn handle_event(&mut self, event: crate::Event) -> Self::Response {
219        if let Event::Key(key_event) = event {
220            match key_event.code {
221                KeyCode::Char(c) => {
222                    self.append_digit(c);
223                }
224                KeyCode::Backspace => {
225                    self.remove_digit();
226                }
227                KeyCode::Up => {
228                    self.add(T::one());
229                }
230                KeyCode::Down => {
231                    self.sub(T::one());
232                }
233                KeyCode::Enter => return NumInputResponse::Submit,
234                KeyCode::Esc => return NumInputResponse::Cancel,
235                _ => {}
236            }
237        }
238        NumInputResponse::None
239    }
240
241    fn draw(&mut self, rect: Rect, buffer: &mut Buffer) -> Self::DrawResponse {
242        let text = Paragraph::new(self.get_spans());
243        Widget::render(text, rect, buffer);
244    }
245}
246
247impl<T: InputUnsignedInt> Spannable for UnsignedIntInput<T> {
248    fn get_spans<'a, 'b>(&'a self) -> Spans<'b> {
249        let mut spans = Spans::default();
250        spans.0.push(Span::styled(
251            String::from("> "),
252            Style::default().fg(Color::Green),
253        ));
254        spans.0.push(Span::raw(format!("{}", self.current)));
255        if self.current == T::max_value() {
256            spans.0.push(Span::styled(
257                String::from(" (max value)"),
258                Style::default().fg(Color::Gray),
259            ))
260        } else if self.current == T::min_value() {
261            spans.0.push(Span::styled(
262                String::from(" (min value)"),
263                Style::default().fg(Color::Gray),
264            ))
265        }
266        spans
267    }
268}
269
270#[derive(Debug)]
271pub struct FloatInput<T: InputFloat> {
272    value: FloatValue,
273    _phantom: PhantomData<T>,
274}
275
276#[derive(Debug)]
277pub enum FloatValue {
278    Infinity { negative: bool },
279    Nan,
280    Number(FloatNum),
281}
282
283#[derive(Debug)]
284pub struct FloatNum {
285    whole: String,
286    integral: Option<String>,
287    negative: bool,
288}
289
290#[derive(Debug)]
291pub enum NewFloatError {
292    /// The float was neither infinity, Nan, or finite
293    InvalidState,
294    /// The float's string representation isn't a valid decimal
295    ParseError,
296}
297
298fn parse_digit_string(string: &str) -> Result<&str, NewFloatError> {
299    if string.chars().all(|c| char::is_ascii_digit(&c)) {
300        Ok(string)
301    } else {
302        Err(NewFloatError::ParseError)
303    }
304}
305
306impl<T: InputFloat> FloatInput<T> {
307    pub fn new(initial_value: T) -> Result<Self, NewFloatError> {
308        let value = if initial_value.is_infinite() {
309            FloatValue::Infinity {
310                negative: initial_value.is_sign_negative(),
311            }
312        } else if initial_value.is_nan() {
313            FloatValue::Nan
314        } else if initial_value.is_finite() {
315            let repr = initial_value.to_string();
316            // TODO: make number parsing cleaner
317            let has_decimal = repr.contains('.');
318            let is_negative = repr.chars().next().map_or(false, |c| c == '-');
319            let repr_no_sign = if is_negative { &repr[1..] } else { &repr[..] };
320            if has_decimal {
321                let (first_maybe, second_maybe) = repr_no_sign.split_once('.').unwrap();
322                if first_maybe.is_empty() && second_maybe.is_empty() {
323                    return Err(NewFloatError::ParseError);
324                }
325                FloatValue::Number(FloatNum {
326                    whole: parse_digit_string(first_maybe)?.into(),
327                    integral: Some(parse_digit_string(second_maybe)?.into()),
328                    negative: is_negative,
329                })
330            } else {
331                if repr_no_sign.is_empty() {
332                    return Err(NewFloatError::ParseError);
333                }
334                FloatValue::Number(FloatNum {
335                    whole: parse_digit_string(repr_no_sign)?.into(),
336                    integral: None,
337                    negative: is_negative,
338                })
339            }
340        } else {
341            return Err(NewFloatError::ParseError);
342        };
343        Ok(FloatInput {
344            value,
345            _phantom: PhantomData::default(),
346        })
347    }
348
349    pub fn push_digit(&mut self, digit: char) {
350        if let FloatValue::Number(value) = &mut self.value {
351            if digit.is_ascii_digit() {
352                value
353                    .integral
354                    .as_mut()
355                    .unwrap_or(&mut value.whole)
356                    .push(digit);
357                if value.integral.is_none() {
358                    value.whole = value.whole.trim_start_matches('0').into();
359                }
360            } else if digit == '.' && value.integral.is_none() {
361                value.integral = Some("".into());
362            }
363        }
364    }
365
366    pub fn remove_digit(&mut self) {
367        if let FloatValue::Number(value) = &mut self.value {
368            if let Some(integral) = &mut value.integral {
369                if integral.is_empty() {
370                    value.integral = None;
371                } else {
372                    integral.pop();
373                }
374            } else {
375                value.whole.pop();
376            }
377        }
378    }
379
380    pub fn value(&self) -> T {
381        match &self.value {
382            FloatValue::Infinity {
383                negative: is_negative,
384            } => {
385                if *is_negative {
386                    T::neg_infinity()
387                } else {
388                    T::infinity()
389                }
390            }
391            FloatValue::Nan => T::nan(),
392            FloatValue::Number(number) => {
393                let whole_part = if number.whole.is_empty() {
394                    "0"
395                } else {
396                    &number.whole
397                };
398                let entire = if let Some(integral) = &number.integral {
399                    format!("{}.{}", whole_part, integral)
400                } else {
401                    whole_part.to_string()
402                };
403
404                let raw = T::from_str_radix(&entire, 10)
405                    .map_err(|_| NewFloatError::ParseError)
406                    .unwrap();
407                if number.negative {
408                    -raw
409                } else {
410                    raw
411                }
412            }
413        }
414    }
415}
416
417impl<T: InputFloat> Component for FloatInput<T> {
418    type Response = NumInputResponse;
419    type DrawResponse = ();
420
421    fn handle_event(&mut self, event: crate::Event) -> Self::Response {
422        if let Event::Key(key_event) = event {
423            match key_event.code {
424                KeyCode::Char(c) => {
425                    if c.is_ascii_digit() || c == '.' {
426                        self.push_digit(c)
427                    } else if c == '-' {
428                        match &mut self.value {
429                            FloatValue::Number(num) => num.negative = !num.negative,
430                            FloatValue::Infinity {
431                                negative: is_negative,
432                            } => *is_negative = !*is_negative,
433                            _ => {}
434                        }
435                    }
436                }
437                KeyCode::Backspace => {
438                    self.remove_digit();
439                }
440                KeyCode::Tab => match &self.value {
441                    FloatValue::Number(..) => self.value = FloatValue::Infinity { negative: false },
442                    FloatValue::Infinity { .. } => {
443                        self.value = FloatValue::Nan;
444                    }
445                    FloatValue::Nan => {
446                        self.value = FloatValue::Number(FloatNum {
447                            whole: String::new(),
448                            integral: None,
449                            negative: false,
450                        })
451                    }
452                },
453                KeyCode::Enter => return NumInputResponse::Submit,
454                KeyCode::Esc => return NumInputResponse::Cancel,
455                _ => {}
456            }
457        }
458        NumInputResponse::None
459    }
460
461    fn draw(&mut self, rect: Rect, buffer: &mut Buffer) -> Self::DrawResponse {
462        let text = Paragraph::new(self.get_spans());
463        Widget::render(text, rect, buffer);
464    }
465}
466
467impl<T: InputFloat> Spannable for FloatInput<T> {
468    fn get_spans<'a, 'b>(&'a self) -> Spans<'b> {
469        let mut spans = Spans::default();
470        match &self.value {
471            FloatValue::Infinity { negative } => {
472                spans.0.push(Span::styled(
473                    String::from(if *negative { "- " } else { "+ " }),
474                    Style::default().fg(Color::Green),
475                ));
476                spans.0.push(Span::raw(T::infinity().to_string()));
477            }
478            FloatValue::Nan => {
479                spans.0.push(Span::styled(
480                    String::from("> "),
481                    Style::default().fg(Color::Green),
482                ));
483                spans.0.push(Span::raw(T::nan().to_string()));
484            }
485            FloatValue::Number(number) => {
486                let whole_part = if number.whole.is_empty() {
487                    "0"
488                } else {
489                    &number.whole
490                };
491                let entire = if let Some(integral) = &number.integral {
492                    format!("{}.{}", whole_part, integral)
493                } else {
494                    whole_part.to_string()
495                };
496                spans.0.push(Span::styled(
497                    String::from(if number.negative { "- " } else { "+ " }),
498                    Style::default().fg(Color::Green),
499                ));
500                spans.0.push(Span::raw(entire));
501            }
502        }
503        spans
504    }
505}
506
507pub trait InputSignedInt:
508    Integer
509    + Signed
510    + Bounded
511    + SaturatingAdd
512    + SaturatingMul
513    + SaturatingSub
514    + FromPrimitive
515    + Copy
516    + Display
517{
518}
519
520impl<T> InputSignedInt for T where
521    T: Integer
522        + Signed
523        + Bounded
524        + SaturatingAdd
525        + SaturatingMul
526        + SaturatingSub
527        + FromPrimitive
528        + Copy
529        + Display
530{
531}
532
533pub trait InputUnsignedInt:
534    Integer
535    + Unsigned
536    + Bounded
537    + SaturatingAdd
538    + SaturatingMul
539    + SaturatingSub
540    + FromPrimitive
541    + Copy
542    + Display
543{
544}
545
546impl<T> InputUnsignedInt for T where
547    T: Integer
548        + Unsigned
549        + Bounded
550        + SaturatingAdd
551        + SaturatingMul
552        + SaturatingSub
553        + FromPrimitive
554        + Copy
555        + Display
556{
557}
558
559pub trait InputFloat: Float + Signed + FromPrimitive + Copy + Display {}
560
561impl<T> InputFloat for T where T: Float + Signed + FromPrimitive + Copy + Display {}