This document captures the design and implementation details of MaskedInput.
For end-user usage and examples, see MaskedInput.
TextEditorCore infrastructure (caret, selection, clipboard) while enforcing per-slot constraints.Value representation suitable for binding to models.MaskedInput derives from TextEditorBase and exposes:
Template : string? (bindable) - the template mask.Value : string? (bindable) - the slot string (separators are not included).CompactValue : string - Value with empty slots removed.IsValid : bool - true when all required slots are filled and all filled slots match constraints.The template is a string composed of:
Escaping and directives:
\\ escapes the next character so it is treated as a literal.> enables uppercasing for subsequent alphabetic tokens.< enables lowercasing for subsequent alphabetic tokens.! disables automatic case conversion.;c where c is the placeholder glyph for all empty slots.Template notes:
; and c).Token characters map to internal token kinds:
A / a: alphabetic (required / optional)N / n: alphanumeric (required / optional)X / x: non-space (required / optional)9 / 0: digit (required / optional)D / d: digit 1-9 (required / optional)#: digit or sign (+/-) (optional)H / h: hexadecimal digit (required / optional)B / b: binary digit (required / optional)ValueValue stores only the slot characters, not the separators. It is positional:
Example:
99-99;_12-341234CompactValueCompactValue is derived from Value by removing all space characters. This is a convenience API for masks where empty slots
should be ignored by consumers.
IsValidIsValid checks the current masked document:
Internally, MaskedInput uses a private ITextDocument implementation (MaskedInputDocument) that:
Synchronization:
Template is parsed into token metadata (cached by value equality on the template string).PrepareChildren synchronizes the internal document from Template and Value.MaskedInput recomputes Value and updates the bindable property.Implementation detail:
RenderOverride reads Template and Value to participate in dependency tracking even if the current render path does not
otherwise need the values. This is intentional for the binding dirty model.MeasureCore requests a fixed size derived from the template length and MaskedInputStyle.Padding:
tokens.Length + padding.Horizontal, clamped to the available width.1 + padding.Vertical, clamped to the available height (minimum 1).ArrangeCore computes a content rectangle from padding and calls UpdateEditorLayout(...) from the text editor base class.
Rendering is delegated to the text editor base class (RenderEditor), but MaskedInput overrides WriteTextSegment to render:
MaskedInputStyle.SeparatorForeground (or theme muted foreground).;c) or style defaults:
MaskedInputStyle.DigitPlaceholderCharMaskedInputStyle.AlphaPlaceholderCharMaskedInputStyle.DefaultPlaceholderCharTextStyle.Dim.The control fills its padded background using MaskedInputStyle.BackgroundStyle(...).
Because MaskedInput derives from TextEditorBase, it supports:
MaskedInput-specific behavior:
MaskedInput uses MaskedInputStyle (src/XenoAtom.Terminal.UI/Styling/MaskedInputStyle.cs):
TextBoxStyle to reuse padding, focused background fill, and selection visuals.DefaultPlaceholderChar, DigitPlaceholderChar, AlphaPlaceholderChar).SeparatorForeground and a specialized PlaceholderCellStyle.Tests:
src/XenoAtom.Terminal.UI.Tests/MaskedInputTests.cs (placeholders, token filtering, case directives, selection/copy)Demo:
samples/ControlsDemo/Demos/MaskedInputDemo.csUser documentation:
site/docs/controls/maskedinput.mdchar. Multi-char grapheme clusters and surrogate pairs are currently ignored during insertion.;c or style defaults.MaskedInput for discoverability (command palette / command bar integration).Rune per slot instead of char).