diff --git a/Cargo.lock b/Cargo.lock index 90690bf9efe..723914e6ae4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4107,6 +4107,14 @@ dependencies = [ "env_logger", ] +[[package]] +name = "text_validation" +version = "0.1.0" +dependencies = [ + "eframe", + "env_logger", +] + [[package]] name = "thiserror" version = "1.0.66" diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 357e1353077..2b392ae8122 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2038,7 +2038,7 @@ impl Ui { /// No newlines (`\n`) allowed. Pressing enter key will result in the [`TextEdit`] losing focus (`response.lost_focus`). /// /// See also [`TextEdit`]. - pub fn text_edit_singleline( + pub fn text_edit_singleline( &mut self, text: &mut S, ) -> Response { @@ -2048,7 +2048,7 @@ impl Ui { /// A [`TextEdit`] for multiple lines. Pressing enter key will create a new line. /// /// See also [`TextEdit`]. - pub fn text_edit_multiline( + pub fn text_edit_multiline( &mut self, text: &mut S, ) -> Response { @@ -2060,7 +2060,7 @@ impl Ui { /// This will be multiline, monospace, and will insert tabs instead of moving focus. /// /// See also [`TextEdit::code_editor`]. - pub fn code_editor(&mut self, text: &mut S) -> Response { + pub fn code_editor(&mut self, text: &mut S) -> Response { self.add(TextEdit::multiline(text).code_editor()) } diff --git a/crates/egui/src/widgets/mod.rs b/crates/egui/src/widgets/mod.rs index 9cdefb699d0..ec8adc3a30e 100644 --- a/crates/egui/src/widgets/mod.rs +++ b/crates/egui/src/widgets/mod.rs @@ -41,7 +41,7 @@ pub use self::{ separator::Separator, slider::{Slider, SliderClamping, SliderOrientation}, spinner::Spinner, - text_edit::{TextBuffer, TextEdit}, + text_edit::{TextBuffer, TextEdit, TextType}, }; // ---------------------------------------------------------------------------- diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 1b585dab4f3..66513587faa 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use emath::{Rect, TSTransform}; use epaint::{ StrokeKind, + mutex::Mutex, text::{Galley, LayoutJob, cursor::CCursor}, }; @@ -12,12 +13,15 @@ use crate::{ TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, epaint, os::OperatingSystem, output::OutputEvent, - response, text_selection, - text_selection::{CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection}, + response, + text_edit::{TextCursorState, state::TextEditUndoer}, + text_selection::{ + self, CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection, + }, vec2, }; -use super::{TextEditOutput, TextEditState}; +use super::{TextEditOutput, TextEditState, text_type::TextType}; type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc; @@ -65,8 +69,8 @@ type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc { - text: &'t mut dyn TextBuffer, +pub struct TextEdit<'t, Value: TextType = String> { + represents: &'t mut Value, hint_text: WidgetText, hint_text_font: Option, id: Option, @@ -91,11 +95,13 @@ pub struct TextEdit<'t> { background_color: Option, } -impl WidgetWithState for TextEdit<'_> { +impl WidgetWithState for TextEdit<'_, Value> { type State = TextEditState; } -impl TextEdit<'_> { +// This doesn't have to be a string. +// It's just to prevent having specify the generic type, as it's not used by these functions +impl TextEdit<'_, String> { pub fn load_state(ctx: &Context, id: Id) -> Option { TextEditState::load(ctx, id) } @@ -105,21 +111,21 @@ impl TextEdit<'_> { } } -impl<'t> TextEdit<'t> { +impl<'t, Value: TextType> TextEdit<'t, Value> { /// No newlines (`\n`) allowed. Pressing enter key will result in the [`TextEdit`] losing focus (`response.lost_focus`). - pub fn singleline(text: &'t mut dyn TextBuffer) -> Self { + pub fn singleline(value: &'t mut Value) -> Self { Self { desired_height_rows: 1, multiline: false, clip_text: true, - ..Self::multiline(text) + ..Self::multiline(value) } } /// A [`TextEdit`] for multiple lines. Pressing enter key will create a new line by default (can be changed with [`return_key`](TextEdit::return_key)). - pub fn multiline(text: &'t mut dyn TextBuffer) -> Self { + pub fn multiline(value: &'t mut Value) -> Self { Self { - text, + represents: value, hint_text: Default::default(), hint_text_font: None, id: None, @@ -277,7 +283,6 @@ impl<'t> TextEdit<'t> { layouter: &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc, ) -> Self { self.layouter = Some(layouter); - self } @@ -400,13 +405,13 @@ impl<'t> TextEdit<'t> { // ---------------------------------------------------------------------------- -impl Widget for TextEdit<'_> { +impl Widget for TextEdit<'_, Value> { fn ui(self, ui: &mut Ui) -> Response { self.show(ui).response } } -impl TextEdit<'_> { +impl TextEdit<'_, Value> { /// Show the [`TextEdit`], returning a rich [`TextEditOutput`]. /// /// ``` @@ -423,7 +428,7 @@ impl TextEdit<'_> { /// # }); /// ``` pub fn show(self, ui: &mut Ui) -> TextEditOutput { - let is_mutable = self.text.is_mutable(); + let is_mutable = Value::is_mutable(); let frame = self.frame; let where_to_put_background = ui.painter().add(Shape::Noop); let background_color = self @@ -470,7 +475,7 @@ impl TextEdit<'_> { fn show_content(self, ui: &mut Ui) -> TextEditOutput { let TextEdit { - text, + represents, hint_text, hint_text_font, id, @@ -495,6 +500,20 @@ impl TextEdit<'_> { background_color: _, } = self; + // An Id is required, as the displayed string is owned by the state. + let id = id.unwrap_or_else(|| { + if let Some(id_salt) = id_salt { + ui.make_persistent_id(id_salt) + } else { + ui.next_auto_id() + } + }); + + let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default(); + let mut text = state + .text + .get_or_insert_with(|| represents.string_representation()); + let text_color = text_color .or(ui.visuals().override_text_color) // .unwrap_or_else(|| ui.style().interact(&response).text_color()); // too bright @@ -537,18 +556,9 @@ impl TextEdit<'_> { let desired_height = (desired_height_rows.at_least(1) as f32) * row_height; let desired_inner_size = vec2(desired_inner_width, galley.size().y.max(desired_height)); let desired_outer_size = (desired_inner_size + margin.sum()).at_least(min_size); - let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size); + let outer_rect = ui.allocate_space(desired_outer_size).1; let rect = outer_rect - margin; // inner rect (excluding frame/margin). - let id = id.unwrap_or_else(|| { - if let Some(id_salt) = id_salt { - ui.make_persistent_id(id_salt) - } else { - auto_id // Since we are only storing the cursor a persistent Id is not super important - } - }); - let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default(); - // On touch screens (e.g. mobile in `eframe` web), should // dragging select text, or scroll the enclosing [`ScrollArea`] (if any)? // Since currently copying selected text in not supported on `eframe` web, @@ -575,7 +585,7 @@ impl TextEdit<'_> { if interactive { if let Some(pointer_pos) = response.interact_pointer_pos() { - if response.hovered() && text.is_mutable() { + if response.hovered() && Value::is_mutable() { ui.output_mut(|o| o.mutable_text_under_cursor = true); } @@ -628,8 +638,11 @@ impl TextEdit<'_> { let (changed, new_cursor_range) = events( ui, - &mut state, - text, + &mut state.cursor, + &mut state.undoer, + &mut state.ime_enabled, + &mut state.ime_cursor_range, + &mut text, &mut galley, layouter, id, @@ -773,7 +786,7 @@ impl TextEdit<'_> { ui.scroll_to_rect(primary_cursor_rect + margin, None); } - if text.is_mutable() && interactive { + if Value::is_mutable() && interactive { let now = ui.ctx().input(|i| i.time); if response.changed() || selection_changed { state.last_interaction_time = now; @@ -820,9 +833,36 @@ impl TextEdit<'_> { ui.input_mut(|i| i.events.retain(|e| !matches!(e, Event::Ime(_)))); } - state.clone().store(ui.ctx(), id); + // TODO(tye-exe): Simplify once https://github.com/emilk/egui/issues/2142 is fixed + if response.lost_focus() || response.clicked_elsewhere() { + // TODO(tye-exe): Parsing can be skipped if the text has not changed + // since the value is updated in real time. + // However, the reason for parsing failure would not get logged. + match Value::read_from_string(&represents, &text) { + Some(Ok(var)) => *represents = var, + Some(Err(err)) => { + #[cfg(feature = "log")] + log::info!("Failed to parse value for text edit: {err}"); + } + None => + { + #[cfg(feature = "log")] + if Value::is_mutable() { + log::warn!("Incorrectly marked unparsable TextType as mutable.",) + } + } + } + + // The user might have changed the text + *text = represents.string_representation() + } if response.changed() { + // Update represented value if the current state is valid + if let Some(Ok(var)) = Value::read_from_string(&represents, &text) { + *represents = var + }; + response.widget_info(|| { WidgetInfo::text_edit( ui.is_enabled(), @@ -871,6 +911,8 @@ impl TextEdit<'_> { ); } + state.clone().store(ui.ctx(), id); + TextEditOutput { response, galley, @@ -904,8 +946,11 @@ fn mask_if_password(is_password: bool, text: &str) -> String { #[expect(clippy::too_many_arguments)] fn events( ui: &crate::Ui, - state: &mut TextEditState, - text: &mut dyn TextBuffer, + cursor: &mut TextCursorState, + undoer: &mut Arc>, + ime_enabled: &mut bool, + ime_cursor_range: &mut CCursorRange, + text: &mut String, galley: &mut Arc, layouter: &mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc, id: Id, @@ -919,11 +964,11 @@ fn events( ) -> (bool, CCursorRange) { let os = ui.ctx().os(); - let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range); + let mut cursor_range = cursor.range(galley).unwrap_or(default_cursor_range); // We feed state to the undoer both before and after handling input // so that the undoer creates automatic saves even when there are no events for a while. - state.undoer.lock().feed_state( + undoer.lock().feed_state( ui.input(|i| i.time), &(cursor_range, text.as_str().to_owned()), ); @@ -938,7 +983,7 @@ fn events( let mut events = ui.input(|i| i.filtered_events(&event_filter)); - if state.ime_enabled { + if *ime_enabled { remove_ime_incompatible_events(&mut events); // Process IME events first: events.sort_by_key(|e| !matches!(e, Event::Ime(_))); @@ -1034,8 +1079,7 @@ fn events( || (modifiers.matches_logically(Modifiers::SHIFT | Modifiers::COMMAND) && *key == Key::Z) => { - if let Some((redo_ccursor_range, redo_txt)) = state - .undoer + if let Some((redo_ccursor_range, redo_txt)) = undoer .lock() .redo(&(cursor_range, text.as_str().to_owned())) { @@ -1052,8 +1096,7 @@ fn events( modifiers, .. } if modifiers.matches_logically(Modifiers::COMMAND) => { - if let Some((undo_ccursor_range, undo_txt)) = state - .undoer + if let Some((undo_ccursor_range, undo_txt)) = undoer .lock() .undo(&(cursor_range, text.as_str().to_owned())) { @@ -1073,8 +1116,8 @@ fn events( Event::Ime(ime_event) => match ime_event { ImeEvent::Enabled => { - state.ime_enabled = true; - state.ime_cursor_range = cursor_range; + *ime_enabled = true; + *ime_cursor_range = cursor_range; None } ImeEvent::Preedit(text_mark) => { @@ -1088,7 +1131,7 @@ fn events( if !text_mark.is_empty() { text.insert_text_at(&mut ccursor, text_mark, char_limit); } - state.ime_cursor_range = cursor_range; + *ime_cursor_range = cursor_range; Some(CCursorRange::two(start_cursor, ccursor)) } } @@ -1096,11 +1139,10 @@ fn events( if prediction == "\n" || prediction == "\r" { None } else { - state.ime_enabled = false; + *ime_enabled = false; if !prediction.is_empty() - && cursor_range.secondary.index - == state.ime_cursor_range.secondary.index + && cursor_range.secondary.index == ime_cursor_range.secondary.index { let mut ccursor = text.delete_selected(&cursor_range); text.insert_text_at(&mut ccursor, prediction, char_limit); @@ -1112,7 +1154,7 @@ fn events( } } ImeEvent::Disabled => { - state.ime_enabled = false; + *ime_enabled = false; None } }, @@ -1131,9 +1173,9 @@ fn events( } } - state.cursor.set_char_range(Some(cursor_range)); + cursor.set_char_range(Some(cursor_range)); - state.undoer.lock().feed_state( + undoer.lock().feed_state( ui.input(|i| i.time), &(cursor_range, text.as_str().to_owned()), ); diff --git a/crates/egui/src/widgets/text_edit/mod.rs b/crates/egui/src/widgets/text_edit/mod.rs index 698a551acf3..17d4b2c7481 100644 --- a/crates/egui/src/widgets/text_edit/mod.rs +++ b/crates/egui/src/widgets/text_edit/mod.rs @@ -2,8 +2,9 @@ mod builder; mod output; mod state; mod text_buffer; +mod text_type; pub use { crate::text_selection::TextCursorState, builder::TextEdit, output::TextEditOutput, - state::TextEditState, text_buffer::TextBuffer, + state::TextEditState, text_buffer::TextBuffer, text_type::TextType, }; diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index 5827aac4bb9..8eb3415c96e 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -37,6 +37,11 @@ pub struct TextEditState { /// Controls the text selection. pub cursor: TextCursorState, + // TODO(tye-exe): Should this be public? + /// Displayed string. + /// This may differ from the value represented if the user is actively editing the string. + pub text: Option, + /// Wrapped in Arc for cheaper clones. #[cfg_attr(feature = "serde", serde(skip))] pub(crate) undoer: Arc>, diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index a67dc1b3851..78b2796444f 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -18,9 +18,6 @@ use crate::{ /// /// Most likely you will use a [`String`] which implements [`TextBuffer`]. pub trait TextBuffer { - /// Can this text be edited? - fn is_mutable(&self) -> bool; - /// Returns this buffer as a `str`. fn as_str(&self) -> &str; @@ -212,10 +209,6 @@ pub trait TextBuffer { } impl TextBuffer for String { - fn is_mutable(&self) -> bool { - true - } - fn as_str(&self) -> &str { self.as_ref() } @@ -260,58 +253,3 @@ impl TextBuffer for String { std::any::TypeId::of::() } } - -impl TextBuffer for Cow<'_, str> { - fn is_mutable(&self) -> bool { - true - } - - fn as_str(&self) -> &str { - self.as_ref() - } - - fn insert_text(&mut self, text: &str, char_index: usize) -> usize { - ::insert_text(self.to_mut(), text, char_index) - } - - fn delete_char_range(&mut self, char_range: Range) { - ::delete_char_range(self.to_mut(), char_range); - } - - fn clear(&mut self) { - ::clear(self.to_mut()); - } - - fn replace_with(&mut self, text: &str) { - *self = Cow::Owned(text.to_owned()); - } - - fn take(&mut self) -> String { - std::mem::take(self).into_owned() - } - - fn type_id(&self) -> std::any::TypeId { - std::any::TypeId::of::>() - } -} - -/// Immutable view of a `&str`! -impl TextBuffer for &str { - fn is_mutable(&self) -> bool { - false - } - - fn as_str(&self) -> &str { - self - } - - fn insert_text(&mut self, _text: &str, _ch_idx: usize) -> usize { - 0 - } - - fn delete_char_range(&mut self, _ch_range: Range) {} - - fn type_id(&self) -> std::any::TypeId { - std::any::TypeId::of::<&str>() - } -} diff --git a/crates/egui/src/widgets/text_edit/text_type.rs b/crates/egui/src/widgets/text_edit/text_type.rs new file mode 100644 index 00000000000..2a3fc150557 --- /dev/null +++ b/crates/egui/src/widgets/text_edit/text_type.rs @@ -0,0 +1,303 @@ +use std::{borrow::Cow, convert::Infallible, error::Error, fmt::Display}; + +/// Any type can be displayed and validated by a [`TextEdit`]. +/// +/// [`TextType`] converts data to its string representation and then attempts to parse any changes the +/// user made has made. If the modified text is can be parsed, then the [`TextType`] will be updated. +/// Otherwise the value will be reset to its last valid state. +/// +/// [`TextType`] is implemented for many of the numeric and string types (including references) within the +/// standard library. But if custom parsing behavior is needed, or implementation does not exist the +/// [`New Type`] pattern can be used. +/// +/// ## Example Implementation +#[doc = "``` +# use egui::TextType; +struct NoCaps(String); + +impl TextType for NoCaps { + type Err = std::convert::Infallible; + + fn read_from_string(_previous: &Self, modified: &str) -> Option> { + Some(Ok(NoCaps(modified.to_lowercase()))) + } + + fn string_representation(&self) -> String { + self.0.clone() + } + + fn is_mutable() -> bool { + true + } +} +```"] +/// This example converts any text the user enters to lowercase. +/// +/// An alternate implementation may choose to reject user input if it contains any capital letters. +#[doc = "``` +# use egui::TextType; +struct NoCaps(String); + +impl TextType for NoCaps { + // Type implementation hidden for brevity + type Err = IncorrectCaseError; + + fn read_from_string(_previous: &Self, modified: &str) -> Option> { + if modified.to_lowercase() == modified { + Some(Ok(NoCaps(modified.to_owned()))) + } else { + Some(Err(IncorrectCaseError( + \"Contained uppercase letters\".to_owned(), + ))) + } + } + + fn string_representation(&self) -> String { + self.0.clone() + } + + fn is_mutable() -> bool { + true + } +} +# #[derive(Debug)] +# pub struct IncorrectCaseError(String); +# impl std::fmt::Display for IncorrectCaseError { +# fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +# f.write_str(&self.0) +# } +# } +# impl std::error::Error for IncorrectCaseError {} +```"] +/// +/// [`TextEdit`]: super::TextEdit +/// [`New Type`]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html +pub trait TextType: Sized { + /// Error returned when [`read_from_string`] parsing fails. + /// If this parsing cannot fail, then [`Infallible`] can be used. + /// + /// [`read_from_string`]: TextType::read_from_string() + /// [`Infallible`]: std::convert::Infallible + type Err: Error; + + /// The value of represented data type depending on the previous valid value and the string modified by the user. + /// + /// `None` is output if this type is immutable (such as a reference). + /// `Some(result)` is the result of parsing. + /// + /// This **must** be able to parse output from [`TextType::string_representation`]. + fn read_from_string(previous: &Self, modified: &str) -> Option>; + /// Generate the string representation of this type. + /// + /// This **must** be parseable by [`TextType::read_from_strings`]. + fn string_representation(&self) -> String; + + /// Whether this data type can be modified. + /// + /// If true for a data type cannot be modified (such as a referenced type), it will appear editable, but no modifications will persist. + /// This will not cause unexpected behavior, but will be confusing for users. + fn is_mutable() -> bool; +} + +/// A generic error that can occur when parsing a type as [`TextType`]. +#[derive(Debug)] +pub struct ConversionError(String); + +impl Display for ConversionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl Error for ConversionError {} + +impl TextType for &str { + type Err = Infallible; + + fn read_from_string(_previous: &Self, _modified: &str) -> Option> { + None + } + + fn string_representation(&self) -> String { + self.to_string() + } + + fn is_mutable() -> bool { + false + } +} + +impl TextType for String { + type Err = Infallible; + + fn read_from_string(_previous: &Self, modified: &str) -> Option> { + Some(Ok(modified.to_string())) + } + + fn string_representation(&self) -> String { + self.to_string() + } + + fn is_mutable() -> bool { + true + } +} + +impl TextType for char { + type Err = ConversionError; + + fn read_from_string(previous: &Self, modified: &str) -> Option> { + let modified: Vec = modified.chars().collect(); + + Some(match (modified.get(0), modified.get(1), modified.get(2)) { + (Some(_), Some(_), Some(_)) => Err(ConversionError( + "Three or more characters present".to_string(), + )), + (Some(first), Some(second), None) if first == previous => Ok(*second), + (Some(first), Some(second), None) if first == second => Ok(*first), + (Some(_), Some(_), None) => Err(ConversionError( + "Two different characters present".to_string(), + )), + (None, _, _) => Err(ConversionError("Zero characters present".to_string())), + (Some(only), _, _) => Ok(*only), + }) + } + + fn string_representation(&self) -> String { + self.to_string() + } + + fn is_mutable() -> bool { + true + } +} + +impl TextType for &char { + type Err = Infallible; + + fn read_from_string(_previous: &Self, _modified: &str) -> Option> { + None + } + + fn string_representation(&self) -> String { + self.to_string() + } + + fn is_mutable() -> bool { + false + } +} + +impl TextType for Cow<'_, str> { + type Err = Infallible; + + fn read_from_string(_previous: &Self, modified: &str) -> Option> { + Some(Ok(Cow::from(modified.to_string()))) + } + + fn string_representation(&self) -> String { + self.to_string() + } + + fn is_mutable() -> bool { + true + } +} + +/// Implementation for number types. +mod num_impls { + /// Reduces repetition in implementation and tests for implementing on numeric types. + macro_rules! num_impl { + ($num:path, $err:path; $test_name:ident, $($init:expr),*) => { + impl super::TextType for $num { + type Err = $err; + + fn read_from_string( + _previous: &Self, + modified: &str, + ) -> Option> { + Some(modified.parse()) + } + + fn string_representation(&self) -> String { + self.to_string() + } + + fn is_mutable() -> bool { + true + } + } + impl super::TextType for &$num { + type Err = std::convert::Infallible; + + fn read_from_string( + _previous: &Self, + _modified: &str, + ) -> Option> { + None + } + + fn string_representation(&self) -> String { + self.to_string() + } + + fn is_mutable() -> bool { + false + } + } + // Requires separate parameter as an "identity" cannot be constructed in a declarative macro + #[test] + fn $test_name() { + use super::TextType; + // Test if values can be parsed from it's string representation + $( + let string = $init.string_representation(); + let parsed_string = TextType::read_from_string(&$init, &string).expect("Can Parse"); + assert_eq!(Ok($init), parsed_string, stringify!(Failed parsing $num with value of $init)); + assert!(TextType::read_from_string(&(&$init), &string).is_none(), stringify!(Parsing a reference (&$init) must return None)); + )* + // Test mutability + assert!(<$num as TextType>::is_mutable(), stringify!($num must be mutable)); + assert!(!<&$num as TextType>::is_mutable(), stringify!(&$num must not be mutable)); + } + }; + ($num:path; $($tail:tt)*) => { + num_impl!($num, std::num::ParseIntError; $($tail)*); + }; + } + + num_impl!(u8; u8_test, 0, 1); + num_impl!(u16; u16_test, 0, 1); + num_impl!(u32; u32_test, 0, 1); + num_impl!(u64; u64_test, 0, 1); + num_impl!(u128; u128_test, 0, 1); + num_impl!(usize; usize_test, 0, 1); + num_impl!(i8; i8_test, -1, 0, 1); + num_impl!(i16; i16_test, -1, 0, 1); + num_impl!(i32; i32_test, -1, 0, 1); + num_impl!(i64; i64_test, -1, 0, 1); + num_impl!(i128; i128_test, -1, 0, 1); + num_impl!(isize; isize_test, -1, 0, 1); + + // These imports also affect the macro. + use std::num::{ + NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize, NonZeroU8, + NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, NonZeroUsize, + }; + num_impl!(NonZeroU8; non0u8_test, NonZeroU8::MIN, NonZeroU8::MAX); + num_impl!(NonZeroU16; non0u16_test, NonZeroU16::MIN, NonZeroU16::MAX); + num_impl!(NonZeroU32; non0u32_test, NonZeroU32::MIN, NonZeroU32::MAX); + num_impl!(NonZeroU64; non0u64_test, NonZeroU64::MIN, NonZeroU64::MAX); + num_impl!(NonZeroU128; non0u128_test, NonZeroU128::MIN, NonZeroU128::MAX); + num_impl!(NonZeroUsize; non0usize_test, NonZeroUsize::MIN, NonZeroUsize::MAX); + num_impl!(NonZeroI8; non0i8_test, NonZeroI8::MIN, NonZeroI8::MAX); + num_impl!(NonZeroI16; non0i16_test, NonZeroI16::MIN, NonZeroI16::MAX); + num_impl!(NonZeroI32; non0i32_test, NonZeroI32::MIN, NonZeroI32::MAX); + num_impl!(NonZeroI64; non0i64_test, NonZeroI64::MIN, NonZeroI64::MAX); + num_impl!(NonZeroI128; non0i128_test, NonZeroI128::MIN, NonZeroI128::MAX); + num_impl!(NonZeroIsize; non0isize_test, NonZeroIsize::MIN, NonZeroIsize::MAX); + + // NAN can be parsed, it just errors since NAN != NAN + num_impl!(f32, std::num::ParseFloatError; f32_test, -1.0, 0.0, 1.0, f32::INFINITY, f32::NEG_INFINITY); + num_impl!(f64, std::num::ParseFloatError; f64_test, -1.0, 0.0, 1.0, f64::INFINITY, f64::NEG_INFINITY); +} diff --git a/examples/text_validation/Cargo.toml b/examples/text_validation/Cargo.toml new file mode 100644 index 00000000000..9f8b112b51f --- /dev/null +++ b/examples/text_validation/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "text_validation" +version = "0.1.0" +authors = ["Emil Ernerfeldt "] +license = "MIT OR Apache-2.0" +edition = "2024" +rust-version = "1.86" +publish = false + +[lints] +workspace = true + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } + +# For image support: +# egui_extras = { workspace = true, features = ["default", "image"] } + +env_logger = { version = "0.10", default-features = false, features = [ + "auto-color", + "humantime", +] } diff --git a/examples/text_validation/src/main.rs b/examples/text_validation/src/main.rs new file mode 100644 index 00000000000..8ec4a69ea37 --- /dev/null +++ b/examples/text_validation/src/main.rs @@ -0,0 +1,103 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + +use eframe::egui::{self, text_edit::TextType}; + +fn main() -> eframe::Result { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]), + ..Default::default() + }; + eframe::run_native( + "My egui App", + options, + Box::new(|cc| Ok(Box::::default())), + ) +} + +struct MyApp { + name: String, + age: u8, + favorite_letter: char, + ice_cream: String, + lowercase: NoCaps, +} + +impl Default for MyApp { + fn default() -> Self { + Self { + name: "James".to_owned(), + age: 42, + favorite_letter: 'H', + ice_cream: "Raspberry".to_owned(), + lowercase: NoCaps("no caps here!".to_owned()), + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.label(&format!( + "I am {}. I am {} years old. My favorite letter is {}.", + self.name, self.age, self.favorite_letter + )); + ui.label(&format!( + "I know for sure that the best ice cream flaviour is {}!", + self.ice_cream + )); + + ui.horizontal(|ui| { + let name_label = ui.label("Your name: "); + ui.text_edit_singleline(&mut self.name) + .labelled_by(name_label.id); + }); + + ui.horizontal(|ui| { + let name_label = ui.label("Your Age: "); + ui.text_edit_singleline(&mut self.age) + .labelled_by(name_label.id); + }); + + ui.horizontal(|ui| { + let name_label = ui.label("Favorite character: "); + ui.text_edit_singleline(&mut self.favorite_letter) + .labelled_by(name_label.id); + }); + + ui.horizontal(|ui| { + let name_label = ui.label("Ice cream: "); + ui.text_edit_singleline(&mut self.ice_cream.as_str()) + .labelled_by(name_label.id); + }); + + ui.separator(); + ui.heading("welcome to the no caps zone, where only lowercase is allowed."); + + ui.horizontal(|ui| { + let name_label = ui.label("no caps allowed: "); + ui.text_edit_singleline(&mut self.lowercase) + .labelled_by(name_label.id); + }); + }); + } +} + +struct NoCaps(String); + +impl TextType for NoCaps { + type Err = std::convert::Infallible; + + fn read_from_string(_previous: &Self, modified: &str) -> Option> { + Some(Ok(NoCaps(modified.to_lowercase()))) + } + + fn string_representation(&self) -> String { + self.0.clone() + } + + fn is_mutable() -> bool { + true + } +}