rat_text/
number_input.rs

1//!
2//! Number input widget
3//!
4
5use crate::_private::NonExhaustive;
6use crate::clipboard::Clipboard;
7use crate::event::{ReadOnly, TextOutcome};
8use crate::text_input_mask::{MaskedInput, MaskedInputState};
9use crate::undo_buffer::{UndoBuffer, UndoEntry};
10use crate::{HasScreenCursor, TextError, TextFocusGained, TextFocusLost, TextStyle, upos_type};
11use format_num_pattern::{NumberFmtError, NumberFormat, NumberSymbols};
12use rat_event::{HandleEvent, MouseOnly, Regular};
13use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
14use rat_reloc::RelocatableState;
15use ratatui::buffer::Buffer;
16use ratatui::layout::Rect;
17use ratatui::style::Style;
18use ratatui::widgets::Block;
19use ratatui::widgets::StatefulWidget;
20use std::fmt::{Debug, Display, LowerExp};
21use std::ops::Range;
22use std::str::FromStr;
23
24/// NumberInput with [format_num_pattern][refFormatNumPattern] backend. A bit
25/// similar to javas DecimalFormat.
26///
27/// # Stateful
28/// This widget implements [`StatefulWidget`], you can use it with
29/// [`NumberInputState`] to handle common actions.
30///
31/// [refFormatNumPattern]: https://docs.rs/format_num_pattern
32#[derive(Debug, Default, Clone)]
33pub struct NumberInput<'a> {
34    widget: MaskedInput<'a>,
35}
36
37/// State & event handling.
38#[derive(Debug, Clone)]
39pub struct NumberInputState {
40    pub widget: MaskedInputState,
41
42    /// NumberFormat pattern.
43    pattern: String,
44    /// Locale
45    locale: format_num_pattern::Locale,
46    // MaskedInput internally always works with the POSIX locale.
47    // So don't be surprised, if you see that one instead of the
48    // paramter locale used here.
49    format: NumberFormat,
50
51    pub non_exhaustive: NonExhaustive,
52}
53
54impl<'a> NumberInput<'a> {
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    /// Show the compact form, if the focus is not with this widget.
60    #[inline]
61    pub fn compact(mut self, compact: bool) -> Self {
62        self.widget = self.widget.compact(compact);
63        self
64    }
65
66    /// Set the combined style.
67    #[inline]
68    pub fn styles(mut self, style: TextStyle) -> Self {
69        self.widget = self.widget.styles(style);
70        self
71    }
72
73    /// Base text style.
74    #[inline]
75    pub fn style(mut self, style: impl Into<Style>) -> Self {
76        self.widget = self.widget.style(style);
77        self
78    }
79
80    /// Style when focused.
81    #[inline]
82    pub fn focus_style(mut self, style: impl Into<Style>) -> Self {
83        self.widget = self.widget.focus_style(style);
84        self
85    }
86
87    /// Style for selection
88    #[inline]
89    pub fn select_style(mut self, style: impl Into<Style>) -> Self {
90        self.widget = self.widget.select_style(style);
91        self
92    }
93
94    /// Style for the invalid indicator.
95    #[inline]
96    pub fn invalid_style(mut self, style: impl Into<Style>) -> Self {
97        self.widget = self.widget.invalid_style(style);
98        self
99    }
100
101    #[inline]
102    pub fn block(mut self, block: Block<'a>) -> Self {
103        self.widget = self.widget.block(block);
104        self
105    }
106
107    /// Focus behaviour
108    #[inline]
109    pub fn on_focus_gained(mut self, of: TextFocusGained) -> Self {
110        self.widget = self.widget.on_focus_gained(of);
111        self
112    }
113
114    /// Focus behaviour
115    #[inline]
116    pub fn on_focus_lost(mut self, of: TextFocusLost) -> Self {
117        self.widget = self.widget.on_focus_lost(of);
118        self
119    }
120}
121
122impl<'a> StatefulWidget for &NumberInput<'a> {
123    type State = NumberInputState;
124
125    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
126        (&self.widget).render(area, buf, &mut state.widget);
127    }
128}
129
130impl StatefulWidget for NumberInput<'_> {
131    type State = NumberInputState;
132
133    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
134        self.widget.render(area, buf, &mut state.widget);
135    }
136}
137
138impl Default for NumberInputState {
139    fn default() -> Self {
140        let mut s = Self {
141            widget: Default::default(),
142            pattern: "".to_string(),
143            locale: Default::default(),
144            format: Default::default(),
145            non_exhaustive: NonExhaustive,
146        };
147        _ = s.set_format("#####");
148        s
149    }
150}
151
152impl HasFocus for NumberInputState {
153    fn build(&self, builder: &mut FocusBuilder) {
154        builder.leaf_widget(self);
155    }
156
157    #[inline]
158    fn focus(&self) -> FocusFlag {
159        self.widget.focus.clone()
160    }
161
162    #[inline]
163    fn area(&self) -> Rect {
164        self.widget.area
165    }
166
167    fn navigable(&self) -> Navigation {
168        self.widget.navigable()
169    }
170}
171
172impl NumberInputState {
173    pub fn new() -> Self {
174        Self::default()
175    }
176
177    pub fn new_pattern<S: AsRef<str>>(pattern: S) -> Result<Self, NumberFmtError> {
178        let mut s = Self::default();
179        s.set_format(pattern)?;
180        Ok(s)
181    }
182
183    pub fn new_loc_pattern<S: AsRef<str>>(
184        pattern: S,
185        locale: format_num_pattern::Locale,
186    ) -> Result<Self, NumberFmtError> {
187        let mut s = Self::default();
188        s.set_format_loc(pattern.as_ref(), locale)?;
189        Ok(s)
190    }
191
192    pub fn named(name: &str) -> Self {
193        Self {
194            widget: MaskedInputState::named(name),
195            ..Default::default()
196        }
197    }
198
199    pub fn with_pattern<S: AsRef<str>>(mut self, pattern: S) -> Result<Self, NumberFmtError> {
200        self.set_format(pattern)?;
201        Ok(self)
202    }
203
204    pub fn with_loc_pattern<S: AsRef<str>>(
205        mut self,
206        pattern: S,
207        locale: format_num_pattern::Locale,
208    ) -> Result<Self, NumberFmtError> {
209        self.set_format_loc(pattern.as_ref(), locale)?;
210        Ok(self)
211    }
212
213    /// [format_num_pattern] format string.
214    #[inline]
215    pub fn format(&self) -> &str {
216        self.pattern.as_str()
217    }
218
219    /// chrono locale.
220    #[inline]
221    pub fn locale(&self) -> chrono::Locale {
222        self.locale
223    }
224
225    /// Set format.
226    pub fn set_format<S: AsRef<str>>(&mut self, pattern: S) -> Result<(), NumberFmtError> {
227        self.set_format_loc(pattern, format_num_pattern::Locale::default())
228    }
229
230    /// Set format and locale.
231    pub fn set_format_loc<S: AsRef<str>>(
232        &mut self,
233        pattern: S,
234        locale: format_num_pattern::Locale,
235    ) -> Result<(), NumberFmtError> {
236        let sym = NumberSymbols::monetary(locale);
237
238        self.format = NumberFormat::new(pattern.as_ref())?;
239        self.widget.set_mask(pattern.as_ref())?;
240        self.widget.set_num_symbols(sym);
241
242        Ok(())
243    }
244
245    /// Renders the widget in invalid style.
246    #[inline]
247    pub fn set_invalid(&mut self, invalid: bool) {
248        self.widget.invalid = invalid;
249    }
250
251    /// Renders the widget in invalid style.
252    #[inline]
253    pub fn get_invalid(&self) -> bool {
254        self.widget.invalid
255    }
256
257    /// The next edit operation will overwrite the current content
258    /// instead of adding text. Any move operations will cancel
259    /// this overwrite.
260    #[inline]
261    pub fn set_overwrite(&mut self, overwrite: bool) {
262        self.widget.set_overwrite(overwrite);
263    }
264
265    /// Will the next edit operation overwrite the content?
266    #[inline]
267    pub fn overwrite(&self) -> bool {
268        self.widget.overwrite()
269    }
270}
271
272impl NumberInputState {
273    /// Clipboard used.
274    /// Default is to use the global_clipboard().
275    #[inline]
276    pub fn set_clipboard(&mut self, clip: Option<impl Clipboard + 'static>) {
277        self.widget.set_clipboard(clip);
278    }
279
280    /// Clipboard used.
281    /// Default is to use the global_clipboard().
282    #[inline]
283    pub fn clipboard(&self) -> Option<&dyn Clipboard> {
284        self.widget.clipboard()
285    }
286
287    /// Copy to internal buffer
288    #[inline]
289    pub fn copy_to_clip(&mut self) -> bool {
290        self.widget.copy_to_clip()
291    }
292
293    /// Cut to internal buffer
294    #[inline]
295    pub fn cut_to_clip(&mut self) -> bool {
296        self.widget.cut_to_clip()
297    }
298
299    /// Paste from internal buffer.
300    #[inline]
301    pub fn paste_from_clip(&mut self) -> bool {
302        self.widget.paste_from_clip()
303    }
304}
305
306impl NumberInputState {
307    /// Set undo buffer.
308    #[inline]
309    pub fn set_undo_buffer(&mut self, undo: Option<impl UndoBuffer + 'static>) {
310        self.widget.set_undo_buffer(undo);
311    }
312
313    /// Undo
314    #[inline]
315    pub fn undo_buffer(&self) -> Option<&dyn UndoBuffer> {
316        self.widget.undo_buffer()
317    }
318
319    /// Undo
320    #[inline]
321    pub fn undo_buffer_mut(&mut self) -> Option<&mut dyn UndoBuffer> {
322        self.widget.undo_buffer_mut()
323    }
324
325    /// Get all recent replay recordings.
326    #[inline]
327    pub fn recent_replay_log(&mut self) -> Vec<UndoEntry> {
328        self.widget.recent_replay_log()
329    }
330
331    /// Apply the replay recording.
332    #[inline]
333    pub fn replay_log(&mut self, replay: &[UndoEntry]) {
334        self.widget.replay_log(replay)
335    }
336
337    /// Undo operation
338    #[inline]
339    pub fn undo(&mut self) -> bool {
340        self.widget.undo()
341    }
342
343    /// Redo operation
344    #[inline]
345    pub fn redo(&mut self) -> bool {
346        self.widget.redo()
347    }
348}
349
350impl NumberInputState {
351    /// Set and replace all styles.
352    #[inline]
353    pub fn set_styles(&mut self, styles: Vec<(Range<usize>, usize)>) {
354        self.widget.set_styles(styles);
355    }
356
357    /// Add a style for a byter-range. The style-nr refers to one
358    /// of the styles set with the widget.
359    #[inline]
360    pub fn add_style(&mut self, range: Range<usize>, style: usize) {
361        self.widget.add_style(range, style);
362    }
363
364    /// Add a style for a `Range<upos_type>` to denote the cells.
365    /// The style-nr refers to one of the styles set with the widget.
366    #[inline]
367    pub fn add_range_style(
368        &mut self,
369        range: Range<upos_type>,
370        style: usize,
371    ) -> Result<(), TextError> {
372        self.widget.add_range_style(range, style)
373    }
374
375    /// Remove the exact TextRange and style.
376    #[inline]
377    pub fn remove_style(&mut self, range: Range<usize>, style: usize) {
378        self.widget.remove_style(range, style);
379    }
380
381    /// Remove the exact `Range<upos_type>` and style.
382    #[inline]
383    pub fn remove_range_style(
384        &mut self,
385        range: Range<upos_type>,
386        style: usize,
387    ) -> Result<(), TextError> {
388        self.widget.remove_range_style(range, style)
389    }
390
391    /// Find all styles that touch the given range.
392    pub fn styles_in(&self, range: Range<usize>, buf: &mut Vec<(Range<usize>, usize)>) {
393        self.widget.styles_in(range, buf)
394    }
395
396    /// All styles active at the given position.
397    #[inline]
398    pub fn styles_at(&self, byte_pos: usize, buf: &mut Vec<(Range<usize>, usize)>) {
399        self.widget.styles_at(byte_pos, buf)
400    }
401
402    /// Check if the given style applies at the position and
403    /// return the complete range for the style.
404    #[inline]
405    pub fn style_match(&self, byte_pos: usize, style: usize) -> Option<Range<usize>> {
406        self.widget.styles_at_match(byte_pos, style)
407    }
408
409    /// List of all styles.
410    #[inline]
411    pub fn styles(&self) -> Option<impl Iterator<Item = (Range<usize>, usize)> + '_> {
412        self.widget.styles()
413    }
414}
415
416impl NumberInputState {
417    /// Offset shown.
418    #[inline]
419    pub fn offset(&self) -> upos_type {
420        self.widget.offset()
421    }
422
423    /// Offset shown. This is corrected if the cursor wouldn't be visible.
424    #[inline]
425    pub fn set_offset(&mut self, offset: upos_type) {
426        self.widget.set_offset(offset)
427    }
428
429    /// Cursor position
430    #[inline]
431    pub fn cursor(&self) -> upos_type {
432        self.widget.cursor()
433    }
434
435    /// Set the cursor position, reset selection.
436    #[inline]
437    pub fn set_cursor(&mut self, cursor: upos_type, extend_selection: bool) -> bool {
438        self.widget.set_cursor(cursor, extend_selection)
439    }
440
441    /// Place cursor at some sensible position according to the mask.
442    #[inline]
443    pub fn set_default_cursor(&mut self) {
444        self.widget.set_default_cursor()
445    }
446
447    /// Selection anchor.
448    #[inline]
449    pub fn anchor(&self) -> upos_type {
450        self.widget.anchor()
451    }
452
453    /// Selection
454    #[inline]
455    pub fn has_selection(&self) -> bool {
456        self.widget.has_selection()
457    }
458
459    /// Selection
460    #[inline]
461    pub fn selection(&self) -> Range<upos_type> {
462        self.widget.selection()
463    }
464
465    /// Selection
466    #[inline]
467    pub fn set_selection(&mut self, anchor: upos_type, cursor: upos_type) -> bool {
468        self.widget.set_selection(anchor, cursor)
469    }
470
471    /// Select all text.
472    #[inline]
473    pub fn select_all(&mut self) {
474        self.widget.select_all();
475    }
476
477    /// Selection
478    #[inline]
479    pub fn selected_text(&self) -> &str {
480        self.widget.selected_text()
481    }
482}
483
484impl NumberInputState {
485    /// Empty
486    #[inline]
487    pub fn is_empty(&self) -> bool {
488        self.widget.is_empty()
489    }
490
491    /// Parses the text as the desired value type.
492    /// If the text content is empty returns None.
493    pub fn value_opt<T: FromStr>(&self) -> Result<Option<T>, NumberFmtError> {
494        let s = self.widget.text();
495        if s.trim().is_empty() {
496            Ok(None)
497        } else {
498            self.format.parse(s).map(|v| Some(v))
499        }
500    }
501
502    /// Parses the text as the desired value type.
503    pub fn value<T: FromStr>(&self) -> Result<T, NumberFmtError> {
504        let s = self.widget.text();
505        self.format.parse(s)
506    }
507
508    /// Length in grapheme count.
509    #[inline]
510    pub fn len(&self) -> upos_type {
511        self.widget.len()
512    }
513
514    /// Length as grapheme count.
515    #[inline]
516    pub fn line_width(&self) -> upos_type {
517        self.widget.line_width()
518    }
519}
520
521impl NumberInputState {
522    /// Reset to empty.
523    #[inline]
524    pub fn clear(&mut self) {
525        self.widget.clear();
526    }
527
528    /// Sets the numeric value.
529    pub fn set_value<T: LowerExp + Display + Debug>(
530        &mut self,
531        number: T,
532    ) -> Result<(), NumberFmtError> {
533        let s = self.format.fmt(number)?;
534        self.widget.set_text(s);
535        Ok(())
536    }
537
538    /// Insert a char at the current position.
539    #[inline]
540    pub fn insert_char(&mut self, c: char) -> bool {
541        self.widget.insert_char(c)
542    }
543
544    /// Remove the selected range. The text will be replaced with the default value
545    /// as defined by the mask.
546    #[inline]
547    pub fn delete_range(&mut self, range: Range<upos_type>) -> bool {
548        self.widget.delete_range(range)
549    }
550
551    /// Remove the selected range. The text will be replaced with the default value
552    /// as defined by the mask.
553    #[inline]
554    pub fn try_delete_range(&mut self, range: Range<upos_type>) -> Result<bool, TextError> {
555        self.widget.try_delete_range(range)
556    }
557}
558
559impl NumberInputState {
560    /// Delete the char after the cursor.
561    #[inline]
562    pub fn delete_next_char(&mut self) -> bool {
563        self.widget.delete_next_char()
564    }
565
566    /// Delete the char before the cursor.
567    #[inline]
568    pub fn delete_prev_char(&mut self) -> bool {
569        self.widget.delete_prev_char()
570    }
571
572    /// Move to the next char.
573    #[inline]
574    pub fn move_right(&mut self, extend_selection: bool) -> bool {
575        self.widget.move_right(extend_selection)
576    }
577
578    /// Move to the previous char.
579    #[inline]
580    pub fn move_left(&mut self, extend_selection: bool) -> bool {
581        self.widget.move_left(extend_selection)
582    }
583
584    /// Start of line
585    #[inline]
586    pub fn move_to_line_start(&mut self, extend_selection: bool) -> bool {
587        self.widget.move_to_line_start(extend_selection)
588    }
589
590    /// End of line
591    #[inline]
592    pub fn move_to_line_end(&mut self, extend_selection: bool) -> bool {
593        self.widget.move_to_line_end(extend_selection)
594    }
595}
596
597impl HasScreenCursor for NumberInputState {
598    /// The current text cursor as an absolute screen position.
599    #[inline]
600    fn screen_cursor(&self) -> Option<(u16, u16)> {
601        self.widget.screen_cursor()
602    }
603}
604
605impl RelocatableState for NumberInputState {
606    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
607        self.widget.relocate(shift, clip);
608    }
609}
610
611impl NumberInputState {
612    /// Converts a grapheme based position to a screen position
613    /// relative to the widget area.
614    #[inline]
615    pub fn col_to_screen(&self, pos: upos_type) -> Option<u16> {
616        self.widget.col_to_screen(pos)
617    }
618
619    /// Converts from a widget relative screen coordinate to a grapheme index.
620    /// x is the relative screen position.
621    #[inline]
622    pub fn screen_to_col(&self, scx: i16) -> upos_type {
623        self.widget.screen_to_col(scx)
624    }
625
626    /// Set the cursor position from a screen position relative to the origin
627    /// of the widget. This value can be negative, which selects a currently
628    /// not visible position and scrolls to it.
629    #[inline]
630    pub fn set_screen_cursor(&mut self, cursor: i16, extend_selection: bool) -> bool {
631        self.widget.set_screen_cursor(cursor, extend_selection)
632    }
633}
634
635impl HandleEvent<crossterm::event::Event, Regular, TextOutcome> for NumberInputState {
636    fn handle(&mut self, event: &crossterm::event::Event, _keymap: Regular) -> TextOutcome {
637        self.widget.handle(event, Regular)
638    }
639}
640
641impl HandleEvent<crossterm::event::Event, ReadOnly, TextOutcome> for NumberInputState {
642    fn handle(&mut self, event: &crossterm::event::Event, _keymap: ReadOnly) -> TextOutcome {
643        self.widget.handle(event, ReadOnly)
644    }
645}
646
647impl HandleEvent<crossterm::event::Event, MouseOnly, TextOutcome> for NumberInputState {
648    fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> TextOutcome {
649        self.widget.handle(event, MouseOnly)
650    }
651}
652
653/// Handle all events.
654/// Text events are only processed if focus is true.
655/// Mouse events are processed if they are in range.
656pub fn handle_events(
657    state: &mut NumberInputState,
658    focus: bool,
659    event: &crossterm::event::Event,
660) -> TextOutcome {
661    state.widget.focus.set(focus);
662    HandleEvent::handle(state, event, Regular)
663}
664
665/// Handle only navigation events.
666/// Text events are only processed if focus is true.
667/// Mouse events are processed if they are in range.
668pub fn handle_readonly_events(
669    state: &mut NumberInputState,
670    focus: bool,
671    event: &crossterm::event::Event,
672) -> TextOutcome {
673    state.widget.focus.set(focus);
674    state.handle(event, ReadOnly)
675}
676
677/// Handle only mouse-events.
678pub fn handle_mouse_events(
679    state: &mut NumberInputState,
680    event: &crossterm::event::Event,
681) -> TextOutcome {
682    HandleEvent::handle(state, event, MouseOnly)
683}