#1299: the server key field shows dots, as a password field does

This commit is contained in:
bflatastic
2026-10-26 09:15:55 +07:00
parent a288fac725
commit 994ddfc47e
3 changed files with 34 additions and 11 deletions
+1 -1
View File
@@ -772,7 +772,7 @@ fn start_ui(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
// The server key (a server that needs one; the address in Preferences is without it).
ui.horizontal(|ui| {
widgets::label(ui, tr("Server key"), t.text);
if let Some(key) = widgets::text_field_named(ui, egui::Id::new("live_share_key"), &app.ui.live.share_key, 200.0, tr("Server key")) {
if let Some(key) = widgets::secret_field_named(ui, egui::Id::new("live_share_key"), &app.ui.live.share_key, 200.0, tr("Server key")) {
app.ui.live.share_key = key.trim().chars().filter(|c| !c.is_whitespace() && !c.is_control()).take(128).collect();
}
});
+14
View File
@@ -232,6 +232,20 @@ fn not_sharing_the_popover_offers_one_way_to_start_and_one_to_join() {
assert_eq!(h.app.session.live().status, Status::Off);
}
#[test]
fn the_server_key_shows_as_dots_while_shown_and_typed() {
let (mut h, _) = harness();
let shapes = open(&mut h);
click(&mut h, &shapes, "Advanced");
h.app.ui.live.share_key = "s3cret-key".into();
let shapes = h.frames(2);
let dots = "•".repeat(10);
assert!(has_text(&shapes, &dots) && !has_text(&shapes, "s3cret-key"));
// Editing it keeps it hidden.
let shapes = click(&mut h, &shapes, &dots);
assert!(!has_text(&shapes, "s3cret-key"));
}
#[test]
fn sharing_online_shows_the_code_copies_it_and_stops() {
let (mut h, script) = harness();
+19 -10
View File
@@ -486,13 +486,20 @@ pub fn bound_field(ui: &mut Ui, id: Id, lead: Lead, rect: Rect, name: &str) -> O
/// A text field that commits on Enter / focus loss. Returns the new text when committed.
pub fn text_field(ui: &mut Ui, id: Id, value: &str, width: f32, placeholder: &str) -> Option<String> {
let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover());
text_field_in(ui, id, value, rect, placeholder, placeholder)
text_field_in(ui, id, value, rect, placeholder, placeholder, false)
}
/// [`text_field`] without a placeholder, named `name` for screen readers (its label is beside it).
pub fn text_field_named(ui: &mut Ui, id: Id, value: &str, width: f32, name: &str) -> Option<String> {
let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover());
text_field_in(ui, id, value, rect, "", name)
text_field_in(ui, id, value, rect, "", name, false)
}
/// [`text_field_named`] for a secret (a server key): it shows dots, typing included, and screen
/// readers hear dots too.
pub fn secret_field_named(ui: &mut Ui, id: Id, value: &str, width: f32, name: &str) -> Option<String> {
let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover());
text_field_in(ui, id, value, rect, "", name, true)
}
/// A multi-line text field across the panel (descriptions, notes) that commits when it loses
@@ -536,7 +543,7 @@ pub fn multiline_field(ui: &mut Ui, id: Id, value: &str, hint: &str, rows: usize
pub fn combo_field(ui: &mut Ui, id: Id, value: &str, width: f32, options: &[String]) -> Option<String> {
let t = Tokens::get(ui.ctx());
let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover());
let mut out = text_field_in(ui, id, value, rect, "", "");
let mut out = text_field_in(ui, id, value, rect, "", "", false);
// The chevron sits on top of the field's right end.
let chev = Rect::from_min_max(pos2(rect.max.x - 18.0, rect.min.y), rect.max);
let resp = ui.interact(chev, id.with("menu"), Sense::click());
@@ -557,13 +564,14 @@ pub fn combo_field(ui: &mut Ui, id: Id, value: &str, width: f32, options: &[Stri
}
/// [`text_field`] drawn into `rect`; `name` names it for screen readers.
fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str, name: &str) -> Option<String> {
fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str, name: &str, secret: bool) -> Option<String> {
cancel_retargeted_field_save(ui, id);
let t = Tokens::get(ui.ctx());
let editing_id = id.with("editing");
let mut editing: Option<String> = ui.data(|d| d.get_temp(editing_id));
let resp = ui.interact(rect, id.with("click"), Sense::click());
a11y::text(&resp, name, value);
let shown = if secret { std::borrow::Cow::Owned("•".repeat(value.chars().count())) } else { std::borrow::Cow::Borrowed(value) };
a11y::text(&resp, name, &shown);
ui.painter().rect_filled(rect, CornerRadius::same(5), t.field);
if editing.is_some() {
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, crate::theme::ACCENT), StrokeKind::Inside);
@@ -579,7 +587,7 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str
let mut out = None;
match editing {
Some(mut text) => {
let r = field_editor(ui, rect, id.with("te"), &mut text, "");
let r = field_editor(ui, rect, id.with("te"), &mut text, "", secret);
a11y::own_focus(&r);
keep_focus(&r, started);
crate::shortcuts::release_field_for_save(&r);
@@ -595,7 +603,7 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str
}
}
None => {
let (s, col) = if value.is_empty() { (placeholder, t.text_tertiary) } else { (value, t.text) };
let (s, col) = if value.is_empty() { (placeholder, t.text_tertiary) } else { (&*shown, t.text) };
// Long text stays inside the field.
let painter = ui.painter().with_clip_rect(rect.shrink2(vec2(8.0, 0.0)));
painter.text(pos2(rect.min.x + 8.0, rect.center().y), Align2::LEFT_CENTER, s, regular(11.0), col);
@@ -605,14 +613,15 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str
}
/// The editor inside a field's `rect`: one line of text in the field's font, without a frame of
/// its own (the field draws it), showing `hint` while empty.
fn field_editor(ui: &mut Ui, rect: Rect, id: Id, text: &mut String, hint: &str) -> Response {
/// its own (the field draws it), showing `hint` while empty; `password` shows dots.
fn field_editor(ui: &mut Ui, rect: Rect, id: Id, text: &mut String, hint: &str, password: bool) -> Response {
let t = Tokens::get(ui.ctx());
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect.shrink2(vec2(8.0, 4.0))));
let r = child.add(
egui::TextEdit::singleline(text)
.id(id)
.frame(egui::Frame::NONE)
.password(password)
.font(regular(11.0))
.hint_text(egui::RichText::new(hint).font(regular(11.0)).color(t.text_tertiary))
.desired_width(rect.width() - 16.0),
@@ -628,7 +637,7 @@ pub fn text_input(ui: &mut Ui, id: Id, text: &mut String, width: f32, hint: &str
let t = Tokens::get(ui.ctx());
let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover());
ui.painter().rect_filled(rect, CornerRadius::same(5), t.field);
let r = field_editor(ui, rect, id, text, hint);
let r = field_editor(ui, rect, id, text, hint, false);
if r.has_focus() {
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, crate::theme::ACCENT), StrokeKind::Inside);
} else if ui.rect_contains_pointer(rect) {