Skip to content

Commit d848b2a

Browse files
committed
Add TextEdit::password to hide input characters
1 parent 33a4058 commit d848b2a

File tree

6 files changed

+60
-24
lines changed

6 files changed

+60
-24
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ NOTE: `eframe`, `egui_web` and `egui_glium` has their own changelogs!
1919
* Add `DebugOptions::show_widgets` to debug layouting by hovering widgets.
2020
* Add `ComboBox` to more easily customize combo boxes.
2121
* Add `Slider::new` and `DragValue::new` to replace old type-specific constructors.
22+
* Add `TextEdit::password` to hide input characters.
2223

2324
### Changed 🔧
2425
* `kb_focus` is now just called `focus`.

egui/src/painter.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -313,12 +313,14 @@ impl Painter {
313313
color: Color32,
314314
fake_italics: bool,
315315
) {
316-
self.add(Shape::Text {
317-
pos,
318-
galley,
319-
color,
320-
fake_italics,
321-
});
316+
if !galley.is_empty() {
317+
self.add(Shape::Text {
318+
pos,
319+
galley,
320+
color,
321+
fake_italics,
322+
});
323+
}
322324
}
323325
}
324326

egui/src/widgets/text_edit.rs

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ pub struct TextEdit<'t> {
130130
id_source: Option<Id>,
131131
text_style: Option<TextStyle>,
132132
text_color: Option<Color32>,
133+
password: bool,
133134
frame: bool,
134135
multiline: bool,
135136
enabled: bool,
@@ -160,6 +161,7 @@ impl<'t> TextEdit<'t> {
160161
id_source: None,
161162
text_style: None,
162163
text_color: None,
164+
password: false,
163165
frame: true,
164166
multiline: false,
165167
enabled: true,
@@ -176,8 +178,9 @@ impl<'t> TextEdit<'t> {
176178
id: None,
177179
id_source: None,
178180
text_style: None,
179-
frame: true,
180181
text_color: None,
182+
password: false,
183+
frame: true,
181184
multiline: true,
182185
enabled: true,
183186
desired_width: None,
@@ -202,6 +205,12 @@ impl<'t> TextEdit<'t> {
202205
self
203206
}
204207

208+
/// If true, hide the letters from view and prevent copying from the field.
209+
pub fn password(mut self, password: bool) -> Self {
210+
self.password = password;
211+
self
212+
}
213+
205214
pub fn text_style(mut self, text_style: TextStyle) -> Self {
206215
self.text_style = Some(text_style);
207216
self
@@ -292,6 +301,7 @@ impl<'t> TextEdit<'t> {
292301
id_source,
293302
text_style,
294303
text_color,
304+
password,
295305
frame: _,
296306
multiline,
297307
enabled,
@@ -302,13 +312,31 @@ impl<'t> TextEdit<'t> {
302312
let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style);
303313
let line_spacing = ui.fonts().row_height(text_style);
304314
let available_width = ui.available_width();
305-
let mut galley = if multiline {
306-
ui.fonts()
307-
.layout_multiline(text_style, text.clone(), available_width)
308-
} else {
309-
ui.fonts().layout_single_line(text_style, text.clone())
315+
316+
let make_galley = |ui: &Ui, text: &str| {
317+
let text = if password {
318+
std::iter::repeat(epaint::text::PASSWORD_REPLACEMENT_CHAR)
319+
.take(text.chars().count())
320+
.collect::<String>()
321+
} else {
322+
text.to_owned()
323+
};
324+
if multiline {
325+
ui.fonts()
326+
.layout_multiline(text_style, text, available_width)
327+
} else {
328+
ui.fonts().layout_single_line(text_style, text)
329+
}
310330
};
311331

332+
let copy_if_not_password = |ui: &Ui, text: String| {
333+
if !password {
334+
ui.ctx().output().copied_text = text;
335+
}
336+
};
337+
338+
let mut galley = make_galley(ui, text);
339+
312340
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
313341
let desired_height = (desired_height_rows.at_least(1) as f32) * line_spacing;
314342
let desired_size = vec2(
@@ -411,18 +439,18 @@ impl<'t> TextEdit<'t> {
411439
let did_mutate_text = match event {
412440
Event::Copy => {
413441
if cursorp.is_empty() {
414-
ui.ctx().output().copied_text = text.clone();
442+
copy_if_not_password(ui, text.clone());
415443
} else {
416-
ui.ctx().output().copied_text = selected_str(text, &cursorp).to_owned();
444+
copy_if_not_password(ui, selected_str(text, &cursorp).to_owned());
417445
}
418446
None
419447
}
420448
Event::Cut => {
421449
if cursorp.is_empty() {
422-
ui.ctx().output().copied_text = std::mem::take(text);
450+
copy_if_not_password(ui, std::mem::take(text));
423451
Some(CCursorPair::default())
424452
} else {
425-
ui.ctx().output().copied_text = selected_str(text, &cursorp).to_owned();
453+
copy_if_not_password(ui, selected_str(text, &cursorp).to_owned());
426454
Some(CCursorPair::one(delete_selected(text, &cursorp)))
427455
}
428456
}
@@ -482,12 +510,7 @@ impl<'t> TextEdit<'t> {
482510
response.mark_changed();
483511

484512
// Layout again to avoid frame delay, and to keep `text` and `galley` in sync.
485-
galley = if multiline {
486-
ui.fonts()
487-
.layout_multiline(text_style, text.clone(), available_width)
488-
} else {
489-
ui.fonts().layout_single_line(text_style, text.clone())
490-
};
513+
galley = make_galley(ui, text);
491514

492515
// Set cursorp using new galley:
493516
cursorp = CursorPair {

egui_demo_lib/src/apps/demo/widgets.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub struct Widgets {
2424
color: Color32,
2525
single_line_text_input: String,
2626
multiline_text_input: String,
27+
show_password: bool,
2728
}
2829

2930
impl Default for Widgets {
@@ -36,6 +37,7 @@ impl Default for Widgets {
3637
color: (Rgba::from_rgb(0.0, 1.0, 0.5) * 0.75).into(),
3738
single_line_text_input: "Hello World!".to_owned(),
3839
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(),
40+
show_password: false,
3941
}
4042
}
4143
}
@@ -128,11 +130,15 @@ impl Widgets {
128130
ui.separator();
129131

130132
ui.horizontal(|ui| {
131-
ui.label("Single line text input:");
132-
let response = ui.text_edit_singleline(&mut self.single_line_text_input);
133+
ui.label("Password:");
134+
let response = ui.add(
135+
egui::TextEdit::singleline(&mut self.single_line_text_input)
136+
.password(!self.show_password),
137+
);
133138
if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) {
134139
// …
135140
}
141+
ui.checkbox(&mut self.show_password, "Show password");
136142
});
137143

138144
ui.label("Multiline text input:");

epaint/src/text/font.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ impl Font {
211211
slf.glyph_info(c);
212212
}
213213
slf.glyph_info('°');
214+
slf.glyph_info(crate::text::PASSWORD_REPLACEMENT_CHAR); // password replacement character
214215

215216
slf
216217
}

epaint/src/text/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ pub use {
99
fonts::{FontDefinitions, FontFamily, Fonts, TextStyle},
1010
galley::{Galley, Row},
1111
};
12+
13+
/// Suggested character to use to replace those in password text fields.
14+
pub const PASSWORD_REPLACEMENT_CHAR: char = '•';

0 commit comments

Comments
 (0)