PanAndZoom for Avalonia

repository·master·Indexed 19 days ago

https://github.com/wieslawsoltes/panandzoom

A control for Avalonia that provides panning and zooming capabilities for UI elements, such as large canvases or images. It features the ZoomBorder container with support for zoom constraints, smooth animations, keyboard navigation, view history (undo/redo), MVVM commands, and coordinate conversion. Advanced functionality includes discrete zoom levels, viewport culling, rotation support, multi-touch gestures, and state serialization via ZoomBorderState.

Tokens
51.9K
Snippets
116
Records
194
Agent score
65%

What's inside PanAndZoom

  1. Overview of PanAndZoom libraries

    master

    The PanAndZoom repository provides two distinct libraries for building and testing interactive Avalonia UI experiences:

    1. PanAndZoom: A library for creating zoomable and pannable interfaces. It includes the ZoomBorder control, matrix helpers, commands, bounds logic, and APIs for managing view state. Use this to build canvases, diagram surfaces, or image viewers.
    2. HeadlessTestingFramework: A testing utility for validating Avalonia controls. It provides input simulation (touch, wheel, keyboard, gestures), tree inspection, recording, and Appium-style helpers. This allows you to test interactive scenarios without a full desktop session and capture frames for diagnostics.
  2. Overview of PanAndZoom for Avalonia

    master

    PanAndZoom is an Avalonia Control and Testing Toolkit consisting of two primary libraries designed to enhance UI interaction and testing capabilities:

    1. PanAndZoom: Provides the ZoomBorder control, which handles panning, zooming, bounds management, and view-state workflows. It includes matrix helpers, commands, history, state persistence, and advanced viewport utilities.
    2. HeadlessTestingFramework: A framework for simulating gestures, inspecting the UI tree, using Appium-style APIs, and performing visual recording/video conversion within headless Avalonia tests.

    Use the PanAndZoom package for production UI features and the HeadlessTestingFramework to drive controls with touch, keyboard, and automated interaction layers.

  3. Understand the Lunet Docs Pipeline site structure

    master

    The documentation site is built using the Lunet framework. The project structure is organized as follows:

    • site/config.scriban: Contains Lunet configuration, project metadata, and settings for .NET API generation.
    • site/menu.yml: Defines the top-level navigation.
    • site/readme.md: The documentation home page.
    • site/articles/**: Contains narrative documentation files.
    • site/articles/**/menu.yml: Defines sidebars for specific documentation sections.
    • site/images/**: Shared assets and images.
    • site/.lunet/css/template-main.css: Precompiled template stylesheet.
    • site/.lunet/css/site-overrides.css: Project-specific visual customizations.
  4. Choose between PanAndZoom and HeadlessTestingFramework

    master

    The repository provides two distinct packages depending on whether you are building interactive UI or automated tests:

    • PanAndZoom: Use this for interactive viewport control. It provides zoom, pan, history, bounds management, resizing, rotation, and state persistence.
    • HeadlessTestingFramework: Use this for automated Avalonia UI tests. It provides touch, wheel, keyboard, and tree inspection, as well as screenshots, recordings, and Appium-like interaction APIs.

    You can use both together to implement zoom-heavy interactions and verify them with headless gesture tests in your CI pipeline.

  5. Use Avalonia.HeadlessTestingFramework for core testing utilities

    master

    The Avalonia.HeadlessTestingFramework namespace provides core testing utilities for Avalonia applications that are independent of Appium-style automation or output recording. It is divided into two main functional areas: simulating user input/gestures and querying/asserting against the UI tree.

    Input and Gesture Simulation

    Use these types to simulate user interactions in a headless environment:

    • TouchInputSimulator
    • KeyboardInputSimulator
    • MouseInputSimulator
    • GestureSimulator
    • MultiTouchTestHelperFactory
    • GestureRecognizerTestHelper
    • SwipeDirection (Enum)

    UI Tree Querying and Assertion

    Use these types to find controls and validate the state of the UI tree:

    • Finding Controls: ControlFinder and ControlFinderExtensions are recommended for readable tests. Use TreeXPath and TreeXPathExtensions for selector-style queries.
    • Tree Traversal: VisualTreeTestHelper and LogicalTreeTestHelper.
    • Assertions and Validation:
      • Use TreeValidator for reusable regression assertions.
      • Use TreeComparer for snapshot-based comparisons and detecting differences.
      • Use TemplateComparer for comparing control templates.
  6. Use the Appium-like API for Avalonia testing

    master

    The HeadlessTestingFramework provides an AvaloniaDriver that implements an Appium/WebDriver-compatible API. This allows developers familiar with mobile automation to write tests for Avalonia applications using By locators, AvaloniaElement actions, and WebDriverWait-style explicit waits.

    To use this API, include the following namespace:

    using Avalonia.HeadlessTestingFramework.Appium;
    using Avalonia.HeadlessTestingFramework.Appium;
    
    // Create driver from a window
    using var driver = new AvaloniaDriver(window);
    
    // Or from a control
    using var driver = new AvaloniaDriver(rootControl);
  7. Simulate high-level gestures with GestureSimulator

    master

    The GestureSimulator class is used to directly raise high-level gesture events (like PinchEvent or TappedEvent) rather than simulating raw touch points. This is ideal for testing processed gesture logic.

    Choosing a Simulator

    SimulatorUse Case
    TouchInputSimulatorLow-level touch input testing, gesture recognizer testing
    GestureSimulatorDirect gesture event testing, simulating processed gestures
    GestureRecognizerTestHelperTesting gesture recognizer behavior with reflection

    Common Gesture Types

    • Tap: Tap(control, point), DoubleTap(control, point), or RightTap(control, point).
    • Hold: Hold(control, point, holdDuration) simulates a press-and-hold sequence.
    • Pinch (Zoom/Rotate): PinchZoom(control, origin, startScale, endScale, steps) or PinchRotate(control, origin, startAngle, endAngle, steps).
    • Scroll: ScrollSequence(control, totalDelta, steps, withInertia, inertiaVelocity).
    • Pull-to-Refresh: PullToRefresh(control, direction, distance, steps) using PullDirection (e.g., TopToBottom).
    • Touchpad (macOS): TouchpadMagnifySequence, TouchpadRotateSequence, and TouchpadSwipeSequence.
    • Compound Gestures: TapAndHold, DoubleTapZoom, Flick, and ThreeFingerSwipe.
    var simulator = new GestureSimulator();
    
    // Simulate pinch zoom from 1x to 2x
    simulator.PinchZoom(zoomBorder, new Point(200, 150), 1.0, 2.0, steps: 10);
    
    // Simulate a flick
    simulator.Flick(control, direction: SwipeDirection.Up, distance: 50, velocity: 500);
  8. Restrict panning with Content Bounds Mode

    master

    Prevent panning beyond content boundaries using BoundsMode. You can also set BoundsPadding and MinimumVisibleContentPercentage to ensure a certain amount of content remains visible.

    Bounds Modes:

    • Unrestricted: No bounds checking (default)
    • KeepContentVisible: Ensures minimum percentage of content stays visible
    • FillViewport: Centers small content and prevents empty space for large content
    • KeepCentered: Always keeps content centered
    • Custom: Requires overriding GetContentBounds() and ValidateTransform()
    zoomBorder.BoundsMode = ContentBoundsMode.KeepContentVisible;
    zoomBorder.BoundsPadding = new Thickness(10);
    zoomBorder.MinimumVisibleContentPercentage = 0.1; // 10% of content must remain visible
  9. Configure BoundsMode for viewport constraints

    master

    The BoundsMode property on ZoomBorder determines how the viewport constrains the content.

    Available modes:

    • Unrestricted: No constraints applied.
    • KeepContentVisible: Ensures the content stays within the viewport boundaries.
    • FillViewport: Forces the content to fill the available viewport.
    • KeepCentered: Keeps the content centered within the viewport.
    • Custom: Allows for manual constraint logic. Use this only if you intend to override GetContentBounds() or ValidateTransform(...).

    Supporting properties to fine-tune constraints:

    • BoundsPadding: Adds padding around the content boundaries.
    • MinimumVisibleContentPercentage: Sets the minimum amount of content that must remain visible.
    • EnableConstrains: A boolean to enable or disable all active constraints.
  10. Understand the three coordinate spaces in PanAndZoom

    master

    PanAndZoom operates using three distinct views of space to manage transformations between the child content and the visible area:

    1. Content space: The original, unscaled coordinate system of the child control.
    2. Viewport space: The visible region defined by the boundaries of the ZoomBorder.
    3. Screen-like vectors and sizes: Transformed measurements (like width, height, or positions) derived from the active transformation matrix.

    Understanding these spaces is essential for tasks like translating pointer positions into domain coordinates or showing overlays that align with content.

  11. Important: Register Gesture Handlers before window.Show()

    master

    When testing gesture events such as Pinch or Scroll, you must register the gesture handlers BEFORE calling window.Show(). If you call window.Show() first, the handlers will not be attached in time to catch the events.

    // ✅ CORRECT
    var control = new MyControl();
    Gestures.AddPinchHandler(control, handler);  // Register FIRST
    var window = new Window { Content = control };
    window.Show();  // Show AFTER
    
    // ❌ WRONG - events won't fire
    var window = new Window { Content = control };
    window.Show();  // Show FIRST
    Gestures.AddPinchHandler(control, handler);  // Too late!