tui_components/components/
num_input.rs1use 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 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 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 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 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 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 InvalidState,
294 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 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 {}