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