Skip to main content

egui_typed_input/
lib.rs

1#![warn(clippy::all)]
2#![allow(clippy::type_complexity)]
3
4use core::{fmt::Display, str::FromStr};
5use egui::TextBuffer;
6
7mod impls;
8
9/// A mutable `TextBuffer` that will validate it's contents when changed.\
10/// And check an input before adding it to the text.
11///
12/// The default validator will simply attempt to parse the text as `T`,
13/// but a custom validator function can be provided.
14///
15/// ## Usage
16/// ```
17/// use egui_typed_input::ValText;
18///
19/// # fn main() {
20/// let mut alphabetical_order: ValText<Vec<char>, ()> = ValText::new(
21///     // parser
22///     (|str| Ok(str.chars().collect::<Vec<_>>())),
23///     // input validator
24///     (|current_text, input, index| {
25///         if input.chars().all(|c| c.is_ascii_alphabetic()) {
26///             input.chars().all(|c| {
27///                 c.to_ascii_lowercase() >= current_text.chars().skip(index.saturating_sub(1)).take(1).last().unwrap_or('a')
28///             })
29///         } else { false }
30///     }),
31/// );
32///
33/// # eframe::run_simple_native(
34/// #    "alphabetical order input",
35/// #    eframe::NativeOptions::default(),
36/// #    move |ctx, _frame| {
37/// #        egui::CentralPanel::default().show(ctx, |ui| {
38/// ui.text_edit_singleline(&mut alphabetical_order);
39/// println!("alphabetical_order: {:?}", alphabetical_order.get_val());
40/// #        });
41/// #    }
42/// # ).unwrap();
43/// # }
44/// ```
45/// See hex color example (color_hex.rs) and number examples (number.rs) for more
46#[must_use = "The input parsing buffer must be used in a ui input"]
47pub struct ValText<T, E> {
48    /// The current text buffer
49    text: String,
50    /// The value parsed from `text` if not empty (Option) and valid (Result)
51    parsed_val: Option<Result<T, E>>,
52    /// A function run each time text changes parsing it
53    value_parser: Box<dyn Fn(&str) -> Result<T, E>>,
54    /// Whether a user input should be added to the string at index
55    ///
56    /// The signature is `(current_text, input, insertion_index) -> should_add_to_text`
57    ///
58    /// Note: `insertion_index` is a character index, not a byte index.
59    input_validator: Box<dyn Fn(&str, &str, usize) -> bool>,
60}
61
62impl<T, E> ValText<T, E> {
63    pub fn new(
64        value_parser: impl Fn(&str) -> Result<T, E> + 'static,
65        input_validator: impl Fn(&str, &str, usize) -> bool + 'static,
66    ) -> Self {
67        ValText {
68            text: String::new(),
69            parsed_val: None,
70            value_parser: Box::new(value_parser),
71            input_validator: Box::new(input_validator),
72        }
73    }
74
75    pub fn new_box(
76        value_parser: Box<dyn Fn(&str) -> Result<T, E>>,
77        input_validator: Box<dyn Fn(&str, &str, usize) -> bool>,
78    ) -> Self {
79        ValText {
80            text: String::new(),
81            parsed_val: None,
82            value_parser,
83            input_validator,
84        }
85    }
86
87    pub fn with_parser(validator: impl Fn(&str) -> Result<T, E> + 'static) -> Self {
88        Self {
89            text: String::new(),
90            parsed_val: None,
91            value_parser: Box::new(validator),
92            input_validator: Box::new(|_, _, _| true),
93        }
94    }
95
96    /// Only chars in `charset` can be input
97    /// ## Usage
98    /// ```
99    /// # use egui_typed_input::ValText;
100    /// # let _: ValText<_, ()> =
101    /// ValText::with_parser_fixed_charset(|str| Ok(str.to_owned()), &['a', 'c']);
102    /// ```
103    /// Would allow 'a' and 'c' but no others.
104    pub fn with_parser_fixed_charset(
105        parser: impl Fn(&str) -> Result<T, E> + 'static,
106        charset: &'static [char],
107    ) -> Self {
108        Self {
109            text: String::new(),
110            parsed_val: None,
111            value_parser: Box::new(parser),
112            input_validator: Box::new(|_, s, _| s.chars().all(|c| charset.contains(&c))),
113        }
114    }
115
116    /// `ValText` must be used before getting value
117    pub const fn get_val(&self) -> Option<Result<&T, &E>> {
118        match self.parsed_val.as_ref() {
119            Some(res) => Some(res.as_ref()),
120            None => None,
121        }
122    }
123
124    pub fn is_valid(&self) -> bool {
125        self.parsed_val.as_ref().is_some_and(Result::is_ok)
126    }
127}
128
129impl<T: FromStr> ValText<Option<T>, T::Err> {
130    pub fn option_parse() -> Self {
131        Self {
132            text: String::new(),
133            parsed_val: None,
134            value_parser: Box::new(|str| {
135                if str.is_empty() {
136                    Ok(None)
137                } else {
138                    str.parse::<T>().map(|t| Some(t))
139                }
140            }),
141            input_validator: Box::new(|_, _, _| true),
142        }
143    }
144}
145
146impl<T: Display, E> ValText<T, E> {
147    pub fn set_val(&mut self, val: T) {
148        self.text = val.to_string();
149        self.parsed_val = Some(Ok(val));
150    }
151}
152
153impl<T: FromStr> Default for ValText<T, T::Err> {
154    /// Parse the text using `FromStr`
155    fn default() -> Self {
156        Self {
157            text: String::new(),
158            parsed_val: None,
159            value_parser: Box::new(|text| text.parse()),
160            input_validator: Box::new(|_, _, _| true),
161        }
162    }
163}
164
165impl<T: 'static, E> TextBuffer for ValText<T, E> {
166    fn is_mutable(&self) -> bool {
167        true
168    }
169
170    fn as_str(&self) -> &str {
171        self.text.as_str()
172    }
173
174    fn insert_text(&mut self, text: &str, char_index: usize) -> usize {
175        if (self.input_validator)(&self.text, text, char_index) {
176            let n = self.text.insert_text(text, char_index);
177            self.parsed_val = Some((self.value_parser)(&self.text));
178            n
179        } else {
180            0
181        }
182    }
183
184    fn delete_char_range(&mut self, char_range: std::ops::Range<usize>) {
185        self.text.delete_char_range(char_range);
186        self.parsed_val = Some((self.value_parser)(&self.text));
187    }
188
189    fn clear(&mut self) {
190        self.parsed_val = None;
191        self.text.clear();
192    }
193
194    fn take(&mut self) -> String {
195        self.parsed_val = None;
196        self.text.take()
197    }
198
199    fn type_id(&self) -> std::any::TypeId {
200        std::any::TypeId::of::<T>()
201    }
202}