Interaction Services & Spatial Math
When building seamless UI interactions (like spatial cursor navigation between distinct text controls), it is critical to keep the UI math decoupled from the event triggers. Dumping complex coordinate translation directly into a View's code-behind or an Attached Behavior creates brittle, unmaintainable code.
To ensure complex UI logic is reproducible and factorized, implement a strict 3-Layer Architecture.
The 3-Layer Architecture
1. The Trigger (Attached Behaviors)
Purpose: Determines when an interaction occurs.
- Lives in the
Behaviors/directory (e.g.,EditorNavigation.cs). - Subscribes to input events (like
KeyDownviaRoutingStrategies.Tunnelto bypass control-internal event swallowing). - Contains no layout math. It simply detects the trigger (e.g.,
ArrowDownat caret line 1) and delegates to a service.
2. The Translator (UI Services)
Purpose: Calculates where the interaction resolves physically.
- Lives in a static service class (e.g.,
Services/UI/SpatialNavigationService.cs). - Handles aligning the universes. If two controls have different margins or font sizes, coordinate
X: 50in one is notX: 50in the other. - Uses Avalonia's
Visual.TranslatePoint(Point, Visual)to perfectly map physical pixels from the source control's coordinate space into the target control's coordinate space.
3. The Control Adapters (The Glue)
Purpose: Abstracts away the unique hit-testing quirks of specific controls.
- Lives in extension methods (e.g.,
Utils/TextBoxExtensions.cs). - Standard Avalonia
TextBoxandAvaloniaEdit:TextEditorcalculate text physics completely differently. - Services should not know how to extract the caret
Rectfrom aTextPresenter, nor should they know how to calleditor.TextArea.TextView.GetPosition(). - Adapters expose simple methods like
GetCaretVisualX()andSetCaretFromVisualX(double x), allowing the Service to orchestrate the translation abstractly without tightly coupling to framework-specific rendering internals.