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