Password-style character masking for editable text inputs - #25117
Password-style character masking for editable text inputs#25117Cyannide wants to merge 2 commits into
Conversation
|
Data point: this has been in production for about a month now (applied to 0.19, wasm + native desktop) in an app with daily testers. No issues beyond what's already noted in the thread -- placeholder overlay tracks the field correctly across resizes and focus changes / masking hasn't produced any cursor or IME surprises. |
|
merged main and resolved conflicts |
|
The value -> Cow migration seems useful in its own right; it should probably be a separate PR. I know I have had a lot of frustration parsing and comparing edit buffers (particularly in FeathersNumberInput). |
Yeah, it kinda came about as a workaround to simplify the edit buffer interactions. I'd be happy to split the |
Previously value() returned a parley SplitString, which required every consumer to reassemble the segments by hand for just about any use case. With Cow<str> (usually borrowed) most existing code continues working unchanged using deref, IME being an owned exception because of the preedit split during composition. FeathersNumberInput and the multiline/multiple-input examples converted to the new API with manual reassembly now one liners.
19072e5 to
1ca4e7e
Compare
Rebased onto the value() -> Cow split (bevyengine#25532) The display buffer holds mask glyphs while the entered text lives in EditableText::shadow_value, and value() reads through the shadow into the entered text so the mask only affects the display. Paste routes through masked_insert so the clipboard string never exists in the visible buffer. value() migration guide now part of the Cow return PR, the apply_pending_edits() guide still here.
1ca4e7e to
a5c3730
Compare
Objective
Password fields.
EditableText's own docs list "Password-style character masking" as planned-but-unimplemented and invite contribution...sooo this implements it.Solution
Where the value lives: the mask is presentation;
EditableText::value()is the contract. The entered text sits in a shadow slot onEditableTextwhile a mask is present, sovalue()always returns the entered text and reading it requires no knowledge of the mask. RemovingEditableTextremoves the value with it. The shadow slot is a pub field of an opaqueShadowValuetype, so struct-literal construction (..default()) keeps working while the contents stay managed by the mask. The example's submit handler shows the consumer shape; one query, one call, no mask in sight:Alternative considered: keeping the value on
CharacterMaskand exposing an honest read through aQueryDatawrapper (&EditableText+Option<&CharacterMask>). Rejected because the plainQuery<&EditableText>+.value()path would keep returning mask glyphs, leaving the honest API as opt-in knowledge.Parley quirks:
SplitStringhas no public constructor and can't carry the shadow value, which is why this PR now sits on #25532.Why a twin representation: caret position, selection geometry, and click-to-position all come from the editor's own parley layout, IOW what is drawn must be what is laid out, or mask glyphs and real characters (different advances) desync every caret coordinate. One glyph per character preserves index parity in both char and byte
space, so cursor/selection operations pass through untouched and content operations mirror by char range.
apply_pending_edits(the documented entry point);TextEdit::applyis unchanged and documented as bypassing the mask. Paste routes throughpoll_and_apply_paste, the one place clipboard text is visible, and the reason this lives inbevy_text.clear(), externalset_text) is adopted and re-concealed. This makes the component-hook lifecycle safe in any insertion order, and the hooks give show/hide password for free: add conceals, remove reveals.EditableTextFilter/max_charactersapply to the real characters.UAX #29, so driver word segmentation is unreliable over them, and word ops shouldn't reflect the real text's word structure anyway).*, Bevy's embedded default font is an ASCII subset and•renders as tofu out of the box; one line opts into•with a real font.Composes with #25110's
Placeholderwith no integration code:value()is the entered text, so a hint keyed tovalue()emptiness just works, a hinted password field is two components on one entity. (Both PRs touch the text_input example...so there will be conflict resolution required by one or the other)Migration
EditableText::valuereturnsCow<'_, str>instead ofSplitString(see #25532).EditableText::apply_pending_editstakes a new final parametermask: Option<&CharacterMask>(passNonefor existing behavior).apply_text_edits' query changed accordingly.Follow-ups (declared, not solved)
char, not per grapheme cluster.•as the default glyph (asset regeneration).ImePurposeis not exposed bybevy_window; until it is, mobile masked fields receive the standard soft keyboard rather than the platform's secure/password keyboard (predictions active). Exposing it and settingImePurpose::Passwordwhile a masked field has focus is the completing piece.Testing
ambiguity_detectionis unaffected.cargo test -p bevy_text, 17 new headless tests against real parley layout: conceal-on-add, per-op mirroring, type-over-selection, selection-gated Cut, clipboard untouched under Copy/Cut, filter-on-real-chars, explicit word-op semantics, reveal-on-remove, reconcile after externalset_text, the IME-commit (mobile soft keyboard) path, and thevalue()contract set: honest-while-masked, shadow-presence-iff-masked, multibyte conceal/reveal round-trip, andclear().cargo run --example text_input, right input masked; the submit handler readsEditableText::value()with no mask in the query.LLM Usage Disclosure:
Showcase