rat_text/
number_input.rs

1//!
2//! Number input widget
3//!
4use crate::_private::NonExhaustive;
5use crate::event::{ReadOnly, TextOutcome};
6use crate::text_input_mask::{MaskedInput, MaskedInputState};
7use crate::{
8    TextFocusGained, TextFocusLost, TextStyle, TextTab, derive_text_widget,
9    derive_text_widget_state,
10};
11use format_num_pattern::{NumberFmtError, NumberFormat, NumberSymbols};
12use rat_event::{HandleEvent, MouseOnly, Regular};
13use ratatui::buffer::Buffer;
14use ratatui::layout::Rect;
15use ratatui::style::Style;
16use ratatui::widgets::Block;
17use ratatui::widgets::StatefulWidget;
18use std::fmt::{Debug, Display, LowerExp};
19use std::str::FromStr;
20
21/// NumberInput with [format_num_pattern][refFormatNumPattern] backend. A bit
22/// similar to javas DecimalFormat.
23///
24/// # Stateful
25/// This widget implements [`StatefulWidget`], you can use it with
26/// [`NumberInputState`] to handle common actions.
27///
28/// [refFormatNumPattern]: https://docs.rs/format_num_pattern
29#[derive(Debug, Default, Clone)]
30pub struct NumberInput<'a> {
31    widget: MaskedInput<'a>,
32}
33
34/// State & event handling.
35#[derive(Debug, Clone)]
36pub struct NumberInputState {
37    /// Area of the widget.
38    /// __read only__ renewed with each render.
39    pub area: Rect,
40    /// Area inside the block.
41    /// __read only__ renewed with each render.
42    pub inner: Rect,
43
44    pub widget: MaskedInputState,
45
46    /// NumberFormat pattern.
47    pattern: String,
48    /// Locale
49    locale: format_num_pattern::Locale,
50    // MaskedInput internally always works with the POSIX locale.
51    // So don't be surprised, if you see that one instead of the
52    // paramter locale used here.
53    format: NumberFormat,
54
55    pub non_exhaustive: NonExhaustive,
56}
57
58derive_text_widget!(NumberInput<'a>);
59
60impl<'a> NumberInput<'a> {
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Show the compact form, if the focus is not with this widget.
66    #[inline]
67    pub fn compact(mut self, compact: bool) -> Self {
68        self.widget = self.widget.compact(compact);
69        self
70    }
71
72    /// Preferred width.
73    pub fn width(&self, state: &NumberInputState) -> u16 {
74        state.widget.mask.len() as u16 + 1
75    }
76
77    /// Preferred width.
78    pub fn height(&self) -> u16 {
79        1
80    }
81}
82
83impl<'a> StatefulWidget for &NumberInput<'a> {
84    type State = NumberInputState;
85
86    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
87        (&self.widget).render(area, buf, &mut state.widget);
88
89        state.area = state.widget.area;
90        state.inner = state.widget.inner;
91    }
92}
93
94impl StatefulWidget for NumberInput<'_> {
95    type State = NumberInputState;
96
97    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
98        self.widget.render(area, buf, &mut state.widget);
99
100        state.area = state.widget.area;
101        state.inner = state.widget.inner;
102    }
103}
104
105derive_text_widget_state!(NumberInputState);
106
107impl Default for NumberInputState {
108    fn default() -> Self {
109        let mut s = Self {
110            area: Default::default(),
111            inner: Default::default(),
112            widget: Default::default(),
113            pattern: "".to_string(),
114            locale: Default::default(),
115            format: Default::default(),
116            non_exhaustive: NonExhaustive,
117        };
118        _ = s.set_format("#####");
119        s
120    }
121}
122
123impl NumberInputState {
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    pub fn new_pattern<S: AsRef<str>>(pattern: S) -> Result<Self, NumberFmtError> {
129        let mut s = Self::default();
130        s.set_format(pattern)?;
131        Ok(s)
132    }
133
134    pub fn new_loc_pattern<S: AsRef<str>>(
135        pattern: S,
136        locale: format_num_pattern::Locale,
137    ) -> Result<Self, NumberFmtError> {
138        let mut s = Self::default();
139        s.set_format_loc(pattern.as_ref(), locale)?;
140        Ok(s)
141    }
142
143    pub fn named(name: &str) -> Self {
144        let mut z = Self::default();
145        z.widget.focus = z.widget.focus.with_name(name);
146        z
147    }
148
149    pub fn with_pattern<S: AsRef<str>>(mut self, pattern: S) -> Result<Self, NumberFmtError> {
150        self.set_format(pattern)?;
151        Ok(self)
152    }
153
154    pub fn with_loc_pattern<S: AsRef<str>>(
155        mut self,
156        pattern: S,
157        locale: format_num_pattern::Locale,
158    ) -> Result<Self, NumberFmtError> {
159        self.set_format_loc(pattern.as_ref(), locale)?;
160        Ok(self)
161    }
162
163    /// [format_num_pattern] format string.
164    #[inline]
165    pub fn format(&self) -> &str {
166        self.pattern.as_str()
167    }
168
169    /// chrono locale.
170    #[inline]
171    pub fn locale(&self) -> chrono::Locale {
172        self.locale
173    }
174
175    /// Set format.
176    pub fn set_format<S: AsRef<str>>(&mut self, pattern: S) -> Result<(), NumberFmtError> {
177        self.set_format_loc(pattern, format_num_pattern::Locale::default())
178    }
179
180    /// Set format and locale.
181    pub fn set_format_loc<S: AsRef<str>>(
182        &mut self,
183        pattern: S,
184        locale: format_num_pattern::Locale,
185    ) -> Result<(), NumberFmtError> {
186        let sym = NumberSymbols::monetary(locale);
187
188        self.format = NumberFormat::new(pattern.as_ref())?;
189        self.widget.set_mask(pattern.as_ref())?;
190        self.widget.set_num_symbols(sym);
191
192        Ok(())
193    }
194}
195
196impl NumberInputState {
197    /// Parses the text as the desired value type.
198    /// If the text content is empty returns None.
199    pub fn value_opt<T: FromStr>(&self) -> Result<Option<T>, NumberFmtError> {
200        let s = self.widget.text();
201        if s.trim().is_empty() {
202            Ok(None)
203        } else {
204            self.format.parse(s).map(|v| Some(v))
205        }
206    }
207
208    /// Parses the text as the desired value type.
209    pub fn value<T: FromStr>(&self) -> Result<T, NumberFmtError> {
210        let s = self.widget.text();
211        self.format.parse(s)
212    }
213}
214
215impl NumberInputState {
216    /// Reset to empty.
217    #[inline]
218    pub fn clear(&mut self) {
219        self.widget.clear();
220    }
221
222    /// Sets the numeric value.
223    pub fn set_value<T: LowerExp + Display + Debug>(
224        &mut self,
225        number: T,
226    ) -> Result<(), NumberFmtError> {
227        let s = self.format.fmt(number)?;
228        self.widget.set_text(s);
229        Ok(())
230    }
231}
232
233impl HandleEvent<crossterm::event::Event, Regular, TextOutcome> for NumberInputState {
234    fn handle(&mut self, event: &crossterm::event::Event, _keymap: Regular) -> TextOutcome {
235        self.widget.handle(event, Regular)
236    }
237}
238
239impl HandleEvent<crossterm::event::Event, ReadOnly, TextOutcome> for NumberInputState {
240    fn handle(&mut self, event: &crossterm::event::Event, _keymap: ReadOnly) -> TextOutcome {
241        self.widget.handle(event, ReadOnly)
242    }
243}
244
245impl HandleEvent<crossterm::event::Event, MouseOnly, TextOutcome> for NumberInputState {
246    fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> TextOutcome {
247        self.widget.handle(event, MouseOnly)
248    }
249}
250
251/// Handle all events.
252/// Text events are only processed if focus is true.
253/// Mouse events are processed if they are in range.
254pub fn handle_events(
255    state: &mut NumberInputState,
256    focus: bool,
257    event: &crossterm::event::Event,
258) -> TextOutcome {
259    state.widget.focus.set(focus);
260    HandleEvent::handle(state, event, Regular)
261}
262
263/// Handle only navigation events.
264/// Text events are only processed if focus is true.
265/// Mouse events are processed if they are in range.
266pub fn handle_readonly_events(
267    state: &mut NumberInputState,
268    focus: bool,
269    event: &crossterm::event::Event,
270) -> TextOutcome {
271    state.widget.focus.set(focus);
272    state.handle(event, ReadOnly)
273}
274
275/// Handle only mouse-events.
276pub fn handle_mouse_events(
277    state: &mut NumberInputState,
278    event: &crossterm::event::Event,
279) -> TextOutcome {
280    HandleEvent::handle(state, event, MouseOnly)
281}