From d848b2a664a7aec65ca01924c54c8b1d00da58a0 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 2 Apr 2021 09:58:55 +0200 Subject: [PATCH] Add TextEdit::password to hide input characters --- CHANGELOG.md | 1 + egui/src/painter.rs | 14 ++++--- egui/src/widgets/text_edit.rs | 55 ++++++++++++++++++-------- egui_demo_lib/src/apps/demo/widgets.rs | 10 ++++- epaint/src/text/font.rs | 1 + epaint/src/text/mod.rs | 3 ++ 6 files changed, 60 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1133c250..1e82ad09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ NOTE: `eframe`, `egui_web` and `egui_glium` has their own changelogs! * Add `DebugOptions::show_widgets` to debug layouting by hovering widgets. * Add `ComboBox` to more easily customize combo boxes. * Add `Slider::new` and `DragValue::new` to replace old type-specific constructors. +* Add `TextEdit::password` to hide input characters. ### Changed 🔧 * `kb_focus` is now just called `focus`. diff --git a/egui/src/painter.rs b/egui/src/painter.rs index e4e9c2e6..6396fde9 100644 --- a/egui/src/painter.rs +++ b/egui/src/painter.rs @@ -313,12 +313,14 @@ impl Painter { color: Color32, fake_italics: bool, ) { - self.add(Shape::Text { - pos, - galley, - color, - fake_italics, - }); + if !galley.is_empty() { + self.add(Shape::Text { + pos, + galley, + color, + fake_italics, + }); + } } } diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 30f15952..85ba62f4 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -130,6 +130,7 @@ pub struct TextEdit<'t> { id_source: Option, text_style: Option, text_color: Option, + password: bool, frame: bool, multiline: bool, enabled: bool, @@ -160,6 +161,7 @@ impl<'t> TextEdit<'t> { id_source: None, text_style: None, text_color: None, + password: false, frame: true, multiline: false, enabled: true, @@ -176,8 +178,9 @@ impl<'t> TextEdit<'t> { id: None, id_source: None, text_style: None, - frame: true, text_color: None, + password: false, + frame: true, multiline: true, enabled: true, desired_width: None, @@ -202,6 +205,12 @@ impl<'t> TextEdit<'t> { self } + /// If true, hide the letters from view and prevent copying from the field. + pub fn password(mut self, password: bool) -> Self { + self.password = password; + self + } + pub fn text_style(mut self, text_style: TextStyle) -> Self { self.text_style = Some(text_style); self @@ -292,6 +301,7 @@ impl<'t> TextEdit<'t> { id_source, text_style, text_color, + password, frame: _, multiline, enabled, @@ -302,13 +312,31 @@ impl<'t> TextEdit<'t> { let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style); let line_spacing = ui.fonts().row_height(text_style); let available_width = ui.available_width(); - let mut galley = if multiline { - ui.fonts() - .layout_multiline(text_style, text.clone(), available_width) - } else { - ui.fonts().layout_single_line(text_style, text.clone()) + + let make_galley = |ui: &Ui, text: &str| { + let text = if password { + std::iter::repeat(epaint::text::PASSWORD_REPLACEMENT_CHAR) + .take(text.chars().count()) + .collect::() + } else { + text.to_owned() + }; + if multiline { + ui.fonts() + .layout_multiline(text_style, text, available_width) + } else { + ui.fonts().layout_single_line(text_style, text) + } }; + let copy_if_not_password = |ui: &Ui, text: String| { + if !password { + ui.ctx().output().copied_text = text; + } + }; + + let mut galley = make_galley(ui, text); + let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width); let desired_height = (desired_height_rows.at_least(1) as f32) * line_spacing; let desired_size = vec2( @@ -411,18 +439,18 @@ impl<'t> TextEdit<'t> { let did_mutate_text = match event { Event::Copy => { if cursorp.is_empty() { - ui.ctx().output().copied_text = text.clone(); + copy_if_not_password(ui, text.clone()); } else { - ui.ctx().output().copied_text = selected_str(text, &cursorp).to_owned(); + copy_if_not_password(ui, selected_str(text, &cursorp).to_owned()); } None } Event::Cut => { if cursorp.is_empty() { - ui.ctx().output().copied_text = std::mem::take(text); + copy_if_not_password(ui, std::mem::take(text)); Some(CCursorPair::default()) } else { - ui.ctx().output().copied_text = selected_str(text, &cursorp).to_owned(); + copy_if_not_password(ui, selected_str(text, &cursorp).to_owned()); Some(CCursorPair::one(delete_selected(text, &cursorp))) } } @@ -482,12 +510,7 @@ impl<'t> TextEdit<'t> { response.mark_changed(); // Layout again to avoid frame delay, and to keep `text` and `galley` in sync. - galley = if multiline { - ui.fonts() - .layout_multiline(text_style, text.clone(), available_width) - } else { - ui.fonts().layout_single_line(text_style, text.clone()) - }; + galley = make_galley(ui, text); // Set cursorp using new galley: cursorp = CursorPair { diff --git a/egui_demo_lib/src/apps/demo/widgets.rs b/egui_demo_lib/src/apps/demo/widgets.rs index 0fbf5da6..7170dc8f 100644 --- a/egui_demo_lib/src/apps/demo/widgets.rs +++ b/egui_demo_lib/src/apps/demo/widgets.rs @@ -24,6 +24,7 @@ pub struct Widgets { color: Color32, single_line_text_input: String, multiline_text_input: String, + show_password: bool, } impl Default for Widgets { @@ -36,6 +37,7 @@ impl Default for Widgets { color: (Rgba::from_rgb(0.0, 1.0, 0.5) * 0.75).into(), single_line_text_input: "Hello World!".to_owned(), multiline_text_input: "Text can both be so wide that it needs a line break, but you can also add manual line break by pressing enter, creating new paragraphs.\nThis is the start of the next paragraph.\n\nClick me to edit me!".to_owned(), + show_password: false, } } } @@ -128,11 +130,15 @@ impl Widgets { ui.separator(); ui.horizontal(|ui| { - ui.label("Single line text input:"); - let response = ui.text_edit_singleline(&mut self.single_line_text_input); + ui.label("Password:"); + let response = ui.add( + egui::TextEdit::singleline(&mut self.single_line_text_input) + .password(!self.show_password), + ); if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) { // … } + ui.checkbox(&mut self.show_password, "Show password"); }); ui.label("Multiline text input:"); diff --git a/epaint/src/text/font.rs b/epaint/src/text/font.rs index 7137fd36..f053f9d2 100644 --- a/epaint/src/text/font.rs +++ b/epaint/src/text/font.rs @@ -211,6 +211,7 @@ impl Font { slf.glyph_info(c); } slf.glyph_info('°'); + slf.glyph_info(crate::text::PASSWORD_REPLACEMENT_CHAR); // password replacement character slf } diff --git a/epaint/src/text/mod.rs b/epaint/src/text/mod.rs index 301320cc..9706e473 100644 --- a/epaint/src/text/mod.rs +++ b/epaint/src/text/mod.rs @@ -9,3 +9,6 @@ pub use { fonts::{FontDefinitions, FontFamily, Fonts, TextStyle}, galley::{Galley, Row}, }; + +/// Suggested character to use to replace those in password text fields. +pub const PASSWORD_REPLACEMENT_CHAR: char = '•';