├── README.md ├── Directory.Build.props ├── CompositionScroll.DesktopExample ├── Assets │ └── delicate-arch-896885_640.jpg ├── MainView.axaml.cs ├── MainWindow.axaml.cs ├── Pages │ ├── TextBoxPage.axaml.cs │ ├── SnappingPage.axaml.cs │ ├── ImageViewerPage.axaml.cs │ ├── StackPanelPage.axaml.cs │ ├── VirtualizingPage.axaml.cs │ ├── ItemsRepeaterPage.axaml.cs │ ├── TextBoxPage.axaml │ ├── ImageViewerPage.axaml │ ├── ItemsRepeaterPage.axaml │ ├── StackPanelPage.axaml │ ├── VirtualizingPage.axaml │ ├── ParallaxPage.axaml │ ├── ParallaxPage.axaml.cs │ └── SnappingPage.axaml ├── MainWindow.axaml ├── TemplateSelector.cs ├── Program.cs ├── app.manifest ├── VM │ ├── SimpleDataListViewModel.cs │ ├── SnappingViewModel.cs │ └── TextViewModel.cs ├── App.axaml.cs ├── CompositionScroll.DesktopExample.csproj ├── App.axaml └── MainView.axaml ├── CompositionScroll ├── CompositionScroll.csproj ├── Interactions │ ├── Factory.cs │ ├── InteractionTrackerInertiaModifier.cs │ ├── ScrollPropertiesSource.cs │ ├── Server │ │ ├── InteractionTrackerMessage.cs │ │ ├── ServerScrollPropertiesSource.cs │ │ ├── Scroller.cs │ │ └── ServerInteractionTracker.cs │ ├── IInteractionTrackerOwner.cs │ ├── InteractionSource.cs │ └── InteractionTracker.cs └── CompositionScrollContentPresenter.cs ├── CompositionScroll.sln └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | # CompositionScroll (AvaloniaUI) 2 | 3 | Test implementation of the scroll on the composition layer. 4 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | disable 4 | 11.3.3 5 | 6 | 7 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Assets/delicate-arch-896885_640.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meloman19/CompositionScroll/HEAD/CompositionScroll.DesktopExample/Assets/delicate-arch-896885_640.jpg -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/MainView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace CompositionScroll.DesktopExample 4 | { 5 | public partial class MainView : UserControl 6 | { 7 | public MainView() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace CompositionScroll.DesktopExample 4 | { 5 | public partial class MainWindow : Window 6 | { 7 | public MainWindow() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/TextBoxPage.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace CompositionScroll.DesktopExample.Pages 4 | { 5 | public partial class TextBoxPage : UserControl 6 | { 7 | public TextBoxPage() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/SnappingPage.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace CompositionScroll.DesktopExample.Pages 4 | { 5 | public partial class SnappingPage : UserControl 6 | { 7 | public SnappingPage() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/ImageViewerPage.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace CompositionScroll.DesktopExample.Pages 4 | { 5 | public partial class ImageViewerPage : UserControl 6 | { 7 | public ImageViewerPage() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/StackPanelPage.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace CompositionScroll.DesktopExample.Pages 4 | { 5 | public partial class StackPanelPage : UserControl 6 | { 7 | public StackPanelPage() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/VirtualizingPage.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace CompositionScroll.DesktopExample.Pages 4 | { 5 | public partial class VirtualizingPage : UserControl 6 | { 7 | public VirtualizingPage() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/ItemsRepeaterPage.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace CompositionScroll.DesktopExample.Pages 4 | { 5 | public partial class ItemsRepeaterPage : UserControl 6 | { 7 | public ItemsRepeaterPage() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CompositionScroll/CompositionScroll.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/TextBoxPage.axaml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/TemplateSelector.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Controls.Templates; 3 | using Avalonia.Metadata; 4 | using System.Collections.Generic; 5 | 6 | namespace CompositionScroll.DesktopExample 7 | { 8 | public sealed class TemplateSelector : IDataTemplate 9 | { 10 | [Content] 11 | public Dictionary Templates { get; } = new Dictionary(); 12 | 13 | public Control Build(object param) 14 | { 15 | return Templates[param as string].Build(param); 16 | } 17 | 18 | public bool Match(object param) 19 | { 20 | if (param is not string key) 21 | return false; 22 | 23 | return Templates.ContainsKey(key); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/ImageViewerPage.axaml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /CompositionScroll/Interactions/Factory.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Rendering.Composition; 2 | 3 | namespace CompositionScroll.Interactions 4 | { 5 | public static class Factory 6 | { 7 | public static InteractionTracker CreateInteractionTracker(this Compositor compositor) 8 | { 9 | return compositor.CreateInteractionTracker(null); 10 | } 11 | 12 | public static InteractionTracker CreateInteractionTracker(this Compositor compositor, IInteractionTrackerOwner owner) 13 | { 14 | var tracker = new InteractionTracker(compositor, owner); 15 | tracker.Init(); 16 | return tracker; 17 | } 18 | 19 | public static InteractionTrackerInertiaRestingValue CreateInteractionTrackerInertiaRestingValue(this Compositor compositor) 20 | => InteractionTrackerInertiaRestingValue.Create(compositor); 21 | } 22 | } -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia; 3 | 4 | namespace CompositionScroll.DesktopExample 5 | { 6 | class Program 7 | { 8 | // Initialization code. Don't use any Avalonia, third-party APIs or any 9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 10 | // yet and stuff might break. 11 | [STAThread] 12 | public static void Main(string[] args) 13 | { 14 | BuildAvaloniaApp() 15 | .StartWithClassicDesktopLifetime(args); 16 | } 17 | 18 | // Avalonia configuration, don't remove; also used by visual designer. 19 | public static AppBuilder BuildAvaloniaApp() 20 | => AppBuilder.Configure() 21 | .UsePlatformDetect() 22 | .WithInterFont() 23 | .LogToTrace(); 24 | 25 | } 26 | } -------------------------------------------------------------------------------- /CompositionScroll/Interactions/InteractionTrackerInertiaModifier.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Rendering.Composition; 2 | using Avalonia.Rendering.Composition.Animations; 3 | 4 | namespace CompositionScroll.Interactions 5 | { 6 | public abstract class InteractionTrackerInertiaModifier : CompositionObject 7 | { 8 | internal InteractionTrackerInertiaModifier(Compositor compositor) : base(compositor, null) 9 | { 10 | } 11 | } 12 | 13 | public sealed class InteractionTrackerInertiaRestingValue : InteractionTrackerInertiaModifier 14 | { 15 | internal InteractionTrackerInertiaRestingValue(Compositor compositor) : base(compositor) 16 | { 17 | } 18 | 19 | public ExpressionAnimation Condition { get; set; } 20 | 21 | public ExpressionAnimation Value { get; set; } 22 | 23 | public static InteractionTrackerInertiaRestingValue Create(Compositor compositor) 24 | => new(compositor); 25 | } 26 | } -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/VM/SimpleDataListViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using System.Collections.ObjectModel; 3 | 4 | namespace CompositionScroll.DesktopExample.VM 5 | { 6 | public class SimpleDataListViewModel : ObservableObject 7 | { 8 | public SimpleDataListViewModel() 9 | { 10 | for (int i = 0; i < 1000; i++) 11 | { 12 | var text = i.ToString(); 13 | if (i % 2 == 0) 14 | { 15 | text += "\n" + text; 16 | } 17 | 18 | var data = new SimpleDataViewModel() 19 | { 20 | Text = text, 21 | }; 22 | Items.Add(data); 23 | } 24 | } 25 | 26 | public ObservableCollection Items { get; } = new ObservableCollection(); 27 | } 28 | 29 | public class SimpleDataViewModel : ObservableObject 30 | { 31 | public string Text { get; set; } 32 | } 33 | } -------------------------------------------------------------------------------- /CompositionScroll/Interactions/ScrollPropertiesSource.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Rendering.Composition; 2 | using CompositionScroll.Interactions.Server; 3 | 4 | namespace CompositionScroll.Interactions 5 | { 6 | public sealed class ScrollPropertiesSource : CompositionObject 7 | { 8 | private readonly CompositionScrollContentPresenter _presenter; 9 | private readonly InteractionTracker _tracker; 10 | 11 | internal ScrollPropertiesSource(Compositor compositor, CompositionScrollContentPresenter presenter, InteractionTracker tracker) 12 | : base(compositor, new ServerScrollPropertiesSource(compositor.Server, tracker.Server as ServerInteractionTracker)) 13 | { 14 | _presenter = presenter; 15 | _tracker = tracker; 16 | RegisterForSerialization(); 17 | } 18 | 19 | internal static ScrollPropertiesSource Create(CompositionScrollContentPresenter presenter, InteractionTracker tracker) 20 | { 21 | var compositor = ElementComposition.GetElementVisual(presenter).Compositor; 22 | return new ScrollPropertiesSource(compositor, presenter, tracker); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Data.Core.Plugins; 4 | using Avalonia.Markup.Xaml; 5 | 6 | namespace CompositionScroll.DesktopExample 7 | { 8 | public partial class App : Application 9 | { 10 | public override void Initialize() 11 | { 12 | AvaloniaXamlLoader.Load(this); 13 | } 14 | 15 | public override void OnFrameworkInitializationCompleted() 16 | { 17 | // Line below is needed to remove Avalonia data validation. 18 | // Without this line you will get duplicate validations from both Avalonia and CT 19 | BindingPlugins.DataValidators.RemoveAt(0); 20 | 21 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 22 | { 23 | desktop.MainWindow = new MainWindow(); 24 | } 25 | else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) 26 | { 27 | singleViewPlatform.MainView = new MainView(); 28 | } 29 | 30 | base.OnFrameworkInitializationCompleted(); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/ItemsRepeaterPage.axaml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/CompositionScroll.DesktopExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WinExe 4 | 6 | net6.0 7 | true 8 | app.manifest 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/StackPanelPage.axaml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/VirtualizingPage.axaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /CompositionScroll/Interactions/Server/InteractionTrackerMessage.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Rendering.Composition.Transport; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace CompositionScroll.Interactions.Server 7 | { 8 | internal enum RequestType 9 | { 10 | AnimatePositionBy, 11 | AnimatePositionTo, 12 | ShiftPositionBy, 13 | UpdatePositionTo, 14 | 15 | InteractionStart, 16 | InteractionMove, 17 | InteractionEnd, 18 | InteractionEndWithInertia, 19 | 20 | SetMinPosition, 21 | SetMaxPosition, 22 | } 23 | 24 | internal readonly struct InteractionTrackerRequest 25 | { 26 | public InteractionTrackerRequest(RequestType requestType, Vector3D value, long requestId) 27 | { 28 | Type = requestType; 29 | VectorValue = value; 30 | RequestId = requestId; 31 | } 32 | 33 | public RequestType Type { get; init; } 34 | 35 | public Vector3D? VectorValue { get; init; } 36 | 37 | public TimeSpan? TimeSpanValue { get; init; } 38 | 39 | public int? IntValue { get; init; } 40 | 41 | public long RequestId { get; init; } 42 | } 43 | 44 | internal sealed class InteractionTrackerMessage 45 | { 46 | public List Requests { get; } = new(); 47 | 48 | public void Serialize(BatchStreamWriter writer) 49 | { 50 | writer.Write(Requests.Count); 51 | 52 | foreach (var request in Requests) 53 | writer.Write(request); 54 | } 55 | 56 | public void Clear() 57 | { 58 | Requests.Clear(); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /CompositionScroll.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.7.34031.279 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompositionScroll.DesktopExample", "CompositionScroll.DesktopExample\CompositionScroll.DesktopExample.csproj", "{3D4F8616-E937-4C9C-A867-8FB710DB53D8}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompositionScroll", "CompositionScroll\CompositionScroll.csproj", "{2D545229-1EF1-4767-A6C8-419BAD709BEF}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {3D4F8616-E937-4C9C-A867-8FB710DB53D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {3D4F8616-E937-4C9C-A867-8FB710DB53D8}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {3D4F8616-E937-4C9C-A867-8FB710DB53D8}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {3D4F8616-E937-4C9C-A867-8FB710DB53D8}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {2D545229-1EF1-4767-A6C8-419BAD709BEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2D545229-1EF1-4767-A6C8-419BAD709BEF}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2D545229-1EF1-4767-A6C8-419BAD709BEF}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {2D545229-1EF1-4767-A6C8-419BAD709BEF}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {DD7D19C8-A16D-4441-8374-4FEE223066AD} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/ParallaxPage.axaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 14 | 15 | 16 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/ParallaxPage.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Interactivity; 3 | using Avalonia.Rendering.Composition; 4 | using CompositionScroll.Interactions; 5 | 6 | namespace CompositionScroll.DesktopExample.Pages 7 | { 8 | public partial class ParallaxPage : UserControl 9 | { 10 | private ScrollPropertiesSource _scrollSource; 11 | 12 | public ParallaxPage() 13 | { 14 | InitializeComponent(); 15 | } 16 | 17 | protected override void OnLoaded(RoutedEventArgs e) 18 | { 19 | base.OnLoaded(e); 20 | 21 | ScrollViewer.PropertyChanged += ScrollViewer_PropertyChanged; 22 | _scrollSource = (ScrollViewer.Presenter as CompositionScrollContentPresenter).GetScrollPropertiesSource(); 23 | UpdateParallax(); 24 | } 25 | 26 | private void ScrollViewer_PropertyChanged(object sender, Avalonia.AvaloniaPropertyChangedEventArgs e) 27 | { 28 | if (e.Property == ScrollViewer.ExtentProperty || 29 | e.Property == ScrollViewer.ViewportProperty) 30 | { 31 | UpdateParallax(); 32 | } 33 | } 34 | 35 | protected override void OnSizeChanged(SizeChangedEventArgs e) 36 | { 37 | base.OnSizeChanged(e); 38 | 39 | UpdateParallax(); 40 | } 41 | 42 | private void UpdateParallax() 43 | { 44 | if (_scrollSource == null) 45 | return; 46 | 47 | var scrollableHeight = ScrollViewer.Extent.Height - ScrollViewer.Viewport.Height; 48 | if (scrollableHeight <= 0) 49 | { 50 | Image.Height = double.NaN; 51 | return; 52 | } 53 | 54 | Image.Height = this.Bounds.Height + (scrollableHeight / 2); 55 | var imageVisual = ElementComposition.GetElementVisual(Image); 56 | 57 | var animation = imageVisual.Compositor.CreateExpressionAnimation(); 58 | animation.Expression = "-Source.Position / 2"; 59 | animation.Target = "Offset"; 60 | animation.SetReferenceParameter("Source", _scrollSource); 61 | 62 | var impl = imageVisual.Compositor.CreateImplicitAnimationCollection(); 63 | impl["Offset"] = animation; 64 | 65 | imageVisual.ImplicitAnimations = impl; 66 | imageVisual.Offset = new Avalonia.Vector3D(1, 0, 0); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /CompositionScroll/Interactions/IInteractionTrackerOwner.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | 3 | namespace CompositionScroll.Interactions 4 | { 5 | public interface IInteractionTrackerOwner 6 | { 7 | void ValuesChanged(InteractionTracker sender, InteractionTrackerValuesChangedArgs args); 8 | 9 | void IdleStateEntered(InteractionTracker sender, InteractionTrackerIdleStateEnteredArgs args); 10 | 11 | void InertiaStateEntered(InteractionTracker sender, InteractionTrackerInertiaStateEnteredArgs args); 12 | 13 | void InteractingStateEntered(InteractionTracker sender, InteractionTrackerInteractingStateEnteredArgs args); 14 | } 15 | 16 | public sealed class InteractionTrackerValuesChangedArgs 17 | { 18 | internal InteractionTrackerValuesChangedArgs(Vector3D position, long requestId) 19 | { 20 | Position = position; 21 | RequestId = requestId; 22 | } 23 | 24 | public Vector3D Position { get; } 25 | 26 | public long RequestId { get; } 27 | } 28 | 29 | public sealed class InteractionTrackerIdleStateEnteredArgs 30 | { 31 | internal InteractionTrackerIdleStateEnteredArgs(long requestId) 32 | { 33 | RequestId = requestId; 34 | } 35 | 36 | public long RequestId { get; } 37 | } 38 | 39 | public sealed class InteractionTrackerInertiaStateEnteredArgs 40 | { 41 | internal InteractionTrackerInertiaStateEnteredArgs(bool fromImpulse, Vector3D? modifiedRestingPosition, Vector3D naturalRestingPosition, Vector3D positionVelocity, long requestId) 42 | { 43 | IsInertiaFromImpulse = fromImpulse; 44 | ModifiedRestingPosition = modifiedRestingPosition; 45 | NaturalRestingPosition = naturalRestingPosition; 46 | PositionVelocityInPixelsPerSecond = positionVelocity; 47 | RequestId = requestId; 48 | } 49 | 50 | public bool IsInertiaFromImpulse { get; } 51 | 52 | public Vector3D? ModifiedRestingPosition { get; } 53 | 54 | public Vector3D NaturalRestingPosition { get; } 55 | 56 | public Vector3D PositionVelocityInPixelsPerSecond { get; } 57 | 58 | public long RequestId { get; } 59 | } 60 | 61 | public class InteractionTrackerInteractingStateEnteredArgs 62 | { 63 | internal InteractionTrackerInteractingStateEnteredArgs(long requestId) 64 | { 65 | RequestId = requestId; 66 | } 67 | 68 | public long RequestId { get; } 69 | } 70 | } -------------------------------------------------------------------------------- /CompositionScroll/Interactions/Server/ServerScrollPropertiesSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia; 3 | using Avalonia.Rendering.Composition.Animations; 4 | using Avalonia.Rendering.Composition.Expressions; 5 | using Avalonia.Rendering.Composition.Server; 6 | using Avalonia.Rendering.Composition.Transport; 7 | 8 | namespace CompositionScroll.Interactions.Server 9 | { 10 | internal class ServerScrollPropertiesSource : ServerObject, IDisposable 11 | { 12 | internal static CompositionProperty IdOfPositionProperty = 13 | CompositionProperty.Register( 14 | "Position", 15 | obj => ((ServerScrollPropertiesSource)obj)._position, 16 | (obj, v) => ((ServerScrollPropertiesSource)obj)._position = v, 17 | obj => ((ServerScrollPropertiesSource)obj)._position); 18 | 19 | private readonly ServerInteractionTracker _tracker; 20 | 21 | private Vector3D _position; 22 | 23 | private ExpressionAnimationInstance _positionAnimation; 24 | 25 | public ServerScrollPropertiesSource(ServerCompositor compositor, ServerInteractionTracker tracker) 26 | : base(compositor) 27 | { 28 | _tracker = tracker; 29 | } 30 | 31 | public Vector3D Position 32 | { 33 | get => _position; 34 | set => SetAnimatedValue(IdOfPositionProperty, out _position, value); 35 | } 36 | 37 | protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) 38 | { 39 | base.DeserializeChangesCore(reader, committedAt); 40 | 41 | if (IsActive) 42 | return; 43 | 44 | var expressionString = "Tracker.Position"; 45 | var expression = ExpressionParser.Parse(expressionString.AsSpan()); 46 | var set = new PropertySetSnapshot(new System.Collections.Generic.Dictionary 47 | { 48 | { "Tracker", new PropertySetSnapshot.Value(_tracker) } 49 | }); 50 | _positionAnimation = new ExpressionAnimationInstance(expression, this, null, set); 51 | 52 | SetAnimatedValue(IdOfPositionProperty, ref _position, committedAt, _positionAnimation); 53 | Activate(); 54 | } 55 | 56 | public override CompositionProperty GetCompositionProperty(string fieldName) 57 | { 58 | switch (fieldName) 59 | { 60 | case nameof(Position): 61 | return IdOfPositionProperty; 62 | } 63 | 64 | return base.GetCompositionProperty(fieldName); 65 | } 66 | 67 | public void Dispose() 68 | { 69 | Deactivate(); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/App.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 31 | 34 | 37 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 59 | 61 | 62 | 63 | 66 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/VM/SnappingViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls.Primitives; 2 | using CommunityToolkit.Mvvm.ComponentModel; 3 | using System.Collections.Generic; 4 | 5 | namespace CompositionScroll.DesktopExample.VM 6 | { 7 | public class SnappingViewModel : ObservableObject 8 | { 9 | private bool _allowAutoHide; 10 | private bool _enableInertia; 11 | private ScrollBarVisibility _horizontalScrollVisibility; 12 | private ScrollBarVisibility _verticalScrollVisibility; 13 | private SnapPointsType _snapPointsType; 14 | private SnapPointsAlignment _snapPointsAlignment; 15 | private bool _areSnapPointsRegular; 16 | 17 | public SnappingViewModel() 18 | { 19 | AvailableVisibility = new List 20 | { 21 | ScrollBarVisibility.Auto, 22 | ScrollBarVisibility.Visible, 23 | ScrollBarVisibility.Hidden, 24 | ScrollBarVisibility.Disabled, 25 | }; 26 | 27 | AvailableSnapPointsType = new List() 28 | { 29 | SnapPointsType.None, 30 | SnapPointsType.Mandatory, 31 | SnapPointsType.MandatorySingle 32 | }; 33 | 34 | AvailableSnapPointsAlignment = new List() 35 | { 36 | SnapPointsAlignment.Near, 37 | SnapPointsAlignment.Center, 38 | SnapPointsAlignment.Far, 39 | }; 40 | 41 | HorizontalScrollVisibility = ScrollBarVisibility.Auto; 42 | VerticalScrollVisibility = ScrollBarVisibility.Auto; 43 | AllowAutoHide = true; 44 | EnableInertia = true; 45 | } 46 | 47 | public bool AllowAutoHide 48 | { 49 | get => _allowAutoHide; 50 | set => SetProperty(ref _allowAutoHide, value); 51 | } 52 | 53 | public bool EnableInertia 54 | { 55 | get => _enableInertia; 56 | set => SetProperty(ref _enableInertia, value); 57 | } 58 | 59 | public ScrollBarVisibility HorizontalScrollVisibility 60 | { 61 | get => _horizontalScrollVisibility; 62 | set => SetProperty(ref _horizontalScrollVisibility, value); 63 | } 64 | 65 | public ScrollBarVisibility VerticalScrollVisibility 66 | { 67 | get => _verticalScrollVisibility; 68 | set => SetProperty(ref _verticalScrollVisibility, value); 69 | } 70 | 71 | public List AvailableVisibility { get; } 72 | 73 | public bool AreSnapPointsRegular 74 | { 75 | get => _areSnapPointsRegular; 76 | set => SetProperty(ref _areSnapPointsRegular, value); 77 | } 78 | 79 | public SnapPointsType SnapPointsType 80 | { 81 | get => _snapPointsType; 82 | set => SetProperty(ref _snapPointsType, value); 83 | } 84 | 85 | public SnapPointsAlignment SnapPointsAlignment 86 | { 87 | get => _snapPointsAlignment; 88 | set => SetProperty(ref _snapPointsAlignment, value); 89 | } 90 | public List AvailableSnapPointsType { get; } 91 | public List AvailableSnapPointsAlignment { get; } 92 | } 93 | } -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/MainView.axaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 64 | 65 | 66 | 67 | 68 | 69 | 72 | 74 | 76 | 78 | 80 | 82 | 84 | 86 | 87 | 88 | 89 | 92 | 93 | 94 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /CompositionScroll/Interactions/Server/Scroller.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using System; 3 | 4 | namespace CompositionScroll.Interactions.Server 5 | { 6 | internal sealed class InteractionScroller 7 | { 8 | private bool _finished; 9 | 10 | private Vector3D _position; 11 | private Vector3D _startPosition; 12 | 13 | public InteractionScroller() 14 | { 15 | _finished = true; 16 | } 17 | 18 | public Vector3D Position => _position; 19 | 20 | public bool Finished => _finished; 21 | 22 | public void StartInteraction(Vector3D start) 23 | { 24 | _finished = false; 25 | _startPosition = start; 26 | _position = _startPosition; 27 | } 28 | 29 | public void Move(Vector3D delta) 30 | { 31 | _position = _startPosition + delta; 32 | } 33 | 34 | public void EndInteraction() 35 | { 36 | _finished = true; 37 | } 38 | 39 | public void Shift(Vector3D shift) 40 | { 41 | _startPosition += shift; 42 | _position += shift; 43 | } 44 | } 45 | 46 | internal sealed class Scroller 47 | { 48 | private static readonly double INFLEXION = 0.35f; 49 | private static readonly double SCROLL_FRICTION = 0.010f; 50 | private static readonly double PHYSICAL_COEF = 64818f; 51 | private static readonly double DECELERATION_RATE = Math.Log(0.78) / Math.Log(0.9); 52 | 53 | private readonly ServerInteractionTracker _interactionTracker; 54 | private bool _finished; 55 | 56 | private double _durationSec; 57 | private TimeSpan _startTime; 58 | 59 | private Vector3D _startVelocity; 60 | private Vector3D _velocity; 61 | private Vector3D _acceleration; 62 | 63 | private Vector3D _position; 64 | private Vector3D _startPosition; 65 | private Vector3D _endPosition; 66 | 67 | public Scroller(ServerInteractionTracker interactionTracker) 68 | { 69 | _interactionTracker = interactionTracker; 70 | _finished = true; 71 | } 72 | 73 | public Vector3D Position => _position; 74 | 75 | public Vector3D EndPosition => _endPosition; 76 | 77 | public Vector3D Velocity => _velocity; 78 | 79 | public bool Finished => _finished; 80 | 81 | public bool Tick() 82 | { 83 | if (_finished) 84 | return false; 85 | 86 | var now = _interactionTracker.Compositor.ServerNow; 87 | 88 | var deltaT = now - _startTime; 89 | var deltaTSec = deltaT.TotalSeconds; 90 | 91 | if (deltaTSec >= _durationSec) 92 | { 93 | _position = _endPosition; 94 | _velocity = new(); 95 | _finished = true; 96 | return false; 97 | } 98 | else 99 | { 100 | _position = _startPosition + _startVelocity * deltaTSec + _acceleration * (deltaTSec * deltaTSec / 2); 101 | _velocity = _startPosition + _acceleration * deltaTSec; 102 | return true; 103 | } 104 | } 105 | 106 | public void ForcePosition(Vector3D position) 107 | { 108 | _position = position; 109 | } 110 | 111 | public void ForceFinished() 112 | { 113 | _finished = true; 114 | } 115 | 116 | public void StartInertiaTo(Vector3D target, TimeSpan duration) 117 | { 118 | _finished = false; 119 | _startPosition = _position; 120 | _endPosition = target; 121 | 122 | _startTime = _interactionTracker.Compositor.ServerNow; 123 | _durationSec = duration.TotalSeconds; 124 | 125 | _startVelocity = Vector3D.Divide(_endPosition - _startPosition, 0.5d * _durationSec); 126 | _velocity = _startVelocity; 127 | _acceleration = Vector3D.Divide(-_velocity, _durationSec); 128 | } 129 | 130 | public void StartInertia(Vector3D delta, TimeSpan duration) 131 | { 132 | if (_finished) 133 | { 134 | _finished = false; 135 | _startPosition = _position; 136 | _endPosition = _startPosition + delta; 137 | } 138 | else 139 | { 140 | _startPosition = _position; 141 | _endPosition += delta; 142 | } 143 | 144 | _startTime = _interactionTracker.Compositor.ServerNow; 145 | _durationSec = duration.TotalSeconds; 146 | 147 | _startVelocity = Vector3D.Divide(_endPosition - _startPosition, 0.5d * _durationSec); 148 | _velocity = _startVelocity; 149 | _acceleration = Vector3D.Divide(-_velocity, _durationSec); 150 | } 151 | 152 | public void StartFlingInertia(Vector3D velocity) 153 | { 154 | _finished = false; 155 | _startTime = _interactionTracker.Compositor.ServerNow; 156 | _startPosition = _position; 157 | 158 | _durationSec = getSplineFlingDuration(velocity); 159 | 160 | _endPosition = _startPosition + velocity * (_durationSec / 2); 161 | 162 | _startVelocity = velocity; 163 | _velocity = _startVelocity; 164 | _acceleration = Vector3D.Divide(-_velocity, _durationSec); 165 | } 166 | 167 | public void Shift(Vector3D shift) 168 | { 169 | _startPosition += shift; 170 | _endPosition += shift; 171 | _position += shift; 172 | } 173 | 174 | private static double getSplineDeceleration(double velocity) 175 | => Math.Log(INFLEXION * Math.Abs(velocity) / (SCROLL_FRICTION * PHYSICAL_COEF)); 176 | 177 | private static double getSplineFlingDuration(Vector3D velocity) 178 | { 179 | var lenVelocity = velocity.Length; 180 | var decel = getSplineDeceleration(lenVelocity); 181 | double decelMinusOne = DECELERATION_RATE - 1.0; 182 | return Math.Exp(decel / decelMinusOne); 183 | } 184 | } 185 | } -------------------------------------------------------------------------------- /CompositionScroll/Interactions/InteractionSource.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Input; 3 | using Avalonia.Input.GestureRecognizers; 4 | using System; 5 | 6 | namespace CompositionScroll.Interactions 7 | { 8 | public sealed class InteractionSource : GestureRecognizer 9 | { 10 | private static int Id = 1; 11 | private static int GetId() => Id++; 12 | 13 | private int _interactionId; 14 | private IPointer _tracking; 15 | private Visual _rootTarget; 16 | private Point _pointerPressedPoint; 17 | private VelocityTracker _velocityTracker; 18 | private bool _scrolling; 19 | 20 | public InteractionSource(IInputElement target) 21 | { 22 | target.PointerWheelChanged += Target_PointerWheelChanged; 23 | } 24 | 25 | private InteractionTracker Tracker { get; set; } 26 | 27 | public bool CanHorizontallyScroll { get; set; } = false; 28 | 29 | public bool CanVerticallyScroll { get; set; } = true; 30 | 31 | public bool IsScrollInertiaEnabled { get; set; } = true; 32 | 33 | public ScrollFeaturesEnum ScrollFeatures { get; set; } = ScrollFeaturesEnum.None; 34 | 35 | public double ScrollStartDistance { get; set; } = 10; 36 | 37 | private bool CanAny(ScrollFeaturesEnum feature) => (feature & ScrollFeatures) != ScrollFeaturesEnum.None; 38 | 39 | private void Target_PointerWheelChanged(object sender, PointerWheelEventArgs e) 40 | { 41 | if (Tracker == null) 42 | return; 43 | 44 | var delta = e.Delta; 45 | 46 | if (CanAny(ScrollFeaturesEnum.WheelSwapDirections)) 47 | { 48 | delta = new Vector(delta.Y, delta.X); 49 | } 50 | 51 | if (e.KeyModifiers == KeyModifiers.Shift) 52 | { 53 | delta = new Vector(delta.Y, delta.X); 54 | } 55 | 56 | delta = FitVector(delta, CanHorizontallyScroll, CanVerticallyScroll); 57 | delta = delta.Negate(); 58 | 59 | delta = delta * 50; 60 | 61 | Tracker.AnimatePositionBy(new Vector3D(delta.X, delta.Y, 0)); 62 | e.Handled = true; 63 | } 64 | 65 | protected override void PointerPressed(PointerPressedEventArgs e) 66 | { 67 | if (e.Pointer.Type == PointerType.Mouse && !CanAny(ScrollFeaturesEnum.MousePressedScroll)) 68 | return; 69 | 70 | EndGesture(); 71 | _tracking = e.Pointer; 72 | _interactionId = GetId(); 73 | _rootTarget = (Visual)((Target as Visual)?.VisualRoot); 74 | _pointerPressedPoint = e.GetPosition(_rootTarget); 75 | _velocityTracker = new VelocityTracker(); 76 | _velocityTracker.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), default); 77 | BeginUserInteraction(); 78 | } 79 | 80 | protected override void PointerMoved(PointerEventArgs e) 81 | { 82 | if (e.Pointer != _tracking) 83 | return; 84 | 85 | var rootPoint = e.GetPosition(_rootTarget); 86 | if (!_scrolling) 87 | { 88 | if (CanHorizontallyScroll && Math.Abs(_pointerPressedPoint.X - rootPoint.X) > ScrollStartDistance) 89 | _scrolling = true; 90 | if (CanVerticallyScroll && Math.Abs(_pointerPressedPoint.Y - rootPoint.Y) > ScrollStartDistance) 91 | _scrolling = true; 92 | 93 | if (_scrolling) 94 | { 95 | Capture(_tracking); 96 | } 97 | } 98 | 99 | if (_scrolling) 100 | { 101 | Vector delta = _pointerPressedPoint - rootPoint; 102 | delta = FitVector(delta, CanHorizontallyScroll, CanVerticallyScroll); 103 | 104 | _velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), delta); 105 | UserInteraction(delta); 106 | e.Handled = true; 107 | } 108 | } 109 | 110 | protected override void PointerReleased(PointerReleasedEventArgs e) 111 | { 112 | if (e.Pointer != _tracking) 113 | return; 114 | 115 | var inertia = _velocityTracker?.GetFlingVelocity().PixelsPerSecond ?? Vector.Zero; 116 | inertia = FitVector(inertia, CanHorizontallyScroll, CanVerticallyScroll); 117 | 118 | if (_scrolling && 119 | IsScrollInertiaEnabled && 120 | (e.Pointer.Type != PointerType.Mouse || CanAny(ScrollFeaturesEnum.MousePressedScrollEnertia))) 121 | { 122 | EndGesture(inertia); 123 | } 124 | else 125 | { 126 | EndGesture(); 127 | } 128 | } 129 | 130 | protected override void PointerCaptureLost(IPointer pointer) 131 | { 132 | if (pointer != _tracking) 133 | return; 134 | 135 | EndGesture(); 136 | } 137 | 138 | private void EndGesture(Vector? inertia = null) 139 | { 140 | EndUserInteraction(inertia); 141 | _tracking = null; 142 | if (_scrolling) 143 | { 144 | _scrolling = false; 145 | _interactionId = 0; 146 | _rootTarget = null; 147 | } 148 | } 149 | 150 | private static Vector FitVector(Vector vector, bool canHoriz, bool canVer) 151 | { 152 | if (!canHoriz) 153 | vector = new Point(0, vector.Y); 154 | if (!canVer) 155 | vector = new Point(vector.X, 0); 156 | 157 | return vector; 158 | } 159 | 160 | private void BeginUserInteraction() 161 | { 162 | Tracker.InteractionStart(_interactionId); 163 | } 164 | 165 | private void UserInteraction(Vector delta) 166 | { 167 | var delta3D = new Vector3D(delta.X, delta.Y, 0); 168 | Tracker.InteractionMove(_interactionId, delta3D); 169 | } 170 | 171 | private void EndUserInteraction(Vector? inertia = null) 172 | { 173 | if (inertia == null) 174 | Tracker.InteractionEnd(_interactionId); 175 | else 176 | { 177 | var inertia3D = new Vector3D(inertia.Value.X, inertia.Value.Y, 0); 178 | Tracker.InteractionEndWithInertia(_interactionId, inertia3D); 179 | } 180 | } 181 | 182 | internal void SetInteractionTracker(InteractionTracker tracker) 183 | { 184 | Tracker = tracker; 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/Pages/SnappingPage.axaml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen. 17 | 18 | 19 | 21 | 23 | 24 | 26 | 27 | 28 | 30 | 31 | 33 | 34 | 35 | 39 | 40 | Vertical Snapping 44 | 45 | 51 | 58 | 61 | 65 | 67 | 68 | 72 | 74 | 75 | 79 | 81 | 82 | 86 | 88 | 89 | 93 | 95 | 96 | 100 | 102 | 103 | 107 | 109 | 110 | 114 | 116 | 117 | 121 | 123 | 124 | 128 | 130 | 131 | 135 | 137 | 138 | 142 | 144 | 145 | 146 | 147 | 148 | Horizontal Snapping 152 | 158 | 166 | 169 | 174 | 177 | 178 | 183 | 186 | 187 | 192 | 195 | 196 | 201 | 204 | 205 | 210 | 213 | 214 | 219 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | -------------------------------------------------------------------------------- /CompositionScroll/Interactions/InteractionTracker.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Rendering.Composition; 3 | using Avalonia.Rendering.Composition.Transport; 4 | using Avalonia.Threading; 5 | using CompositionScroll.Interactions.Server; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Threading; 9 | 10 | namespace CompositionScroll.Interactions 11 | { 12 | public sealed class InteractionTracker : CompositionObject 13 | { 14 | private InteractionSource _interactionSource; 15 | 16 | private readonly IInteractionTrackerOwner _owner; 17 | private InteractionTrackerMessage _message = new(); 18 | private long _requestId = 0; 19 | 20 | internal InteractionTracker(Compositor compositor, IInteractionTrackerOwner owner) 21 | : base(compositor, new ServerInteractionTracker(compositor.Server)) 22 | { 23 | _owner = owner; 24 | } 25 | 26 | internal void Init() 27 | { 28 | var server = Server as ServerInteractionTracker; 29 | server.Init(this); 30 | } 31 | 32 | public InteractionSource InteractionSource 33 | { 34 | get => _interactionSource; 35 | set 36 | { 37 | if (_interactionSource != null) 38 | _interactionSource.SetInteractionTracker(null); 39 | _interactionSource = value; 40 | if (_interactionSource != null) 41 | _interactionSource.SetInteractionTracker(this); 42 | } 43 | } 44 | 45 | public void AnimatePositionBy(Vector3D deltaPosition) 46 | { 47 | var requestId = Interlocked.Increment(ref _requestId); 48 | var request = new InteractionTrackerRequest 49 | { 50 | Type = RequestType.AnimatePositionBy, 51 | VectorValue = deltaPosition, 52 | RequestId = requestId, 53 | }; 54 | SendHandlerMessage(request); 55 | } 56 | 57 | public void AnimatePositionTo(Vector3D offsetPosition) 58 | { 59 | var requestId = Interlocked.Increment(ref _requestId); 60 | var request = new InteractionTrackerRequest 61 | { 62 | Type = RequestType.AnimatePositionTo, 63 | VectorValue = offsetPosition, 64 | RequestId = requestId, 65 | }; 66 | SendHandlerMessage(request); 67 | } 68 | 69 | public void AnimatePositionBy(Vector3D deltaPosition, TimeSpan duration) 70 | { 71 | var requestId = Interlocked.Increment(ref _requestId); 72 | var request = new InteractionTrackerRequest 73 | { 74 | Type = RequestType.AnimatePositionBy, 75 | VectorValue = deltaPosition, 76 | TimeSpanValue = duration, 77 | RequestId = requestId, 78 | }; 79 | SendHandlerMessage(request); 80 | } 81 | 82 | internal void InteractionStart(int interactionId) 83 | { 84 | var requestId = Interlocked.Increment(ref _requestId); 85 | var request = new InteractionTrackerRequest 86 | { 87 | Type = RequestType.InteractionStart, 88 | RequestId = requestId, 89 | IntValue = interactionId 90 | }; 91 | SendHandlerMessage(request); 92 | } 93 | 94 | internal void InteractionMove(int interactionId, Vector3D delta) 95 | { 96 | var requestId = Interlocked.Increment(ref _requestId); 97 | var request = new InteractionTrackerRequest 98 | { 99 | Type = RequestType.InteractionMove, 100 | RequestId = requestId, 101 | IntValue = interactionId, 102 | VectorValue = delta, 103 | }; 104 | SendHandlerMessage(request); 105 | } 106 | 107 | internal void InteractionEnd(int interactionId) 108 | { 109 | var requestId = Interlocked.Increment(ref _requestId); 110 | var request = new InteractionTrackerRequest 111 | { 112 | Type = RequestType.InteractionEnd, 113 | RequestId = requestId, 114 | IntValue = interactionId 115 | }; 116 | SendHandlerMessage(request); 117 | } 118 | 119 | internal void InteractionEndWithInertia(int interactionId, Vector3D velocity) 120 | { 121 | var requestId = Interlocked.Increment(ref _requestId); 122 | var request = new InteractionTrackerRequest 123 | { 124 | Type = RequestType.InteractionEndWithInertia, 125 | RequestId = requestId, 126 | VectorValue = velocity, 127 | IntValue = interactionId 128 | }; 129 | SendHandlerMessage(request); 130 | } 131 | 132 | public void ShiftPositionBy(Vector3D shift) 133 | { 134 | var requestId = Interlocked.Increment(ref _requestId); 135 | SendHandlerMessage(new InteractionTrackerRequest(RequestType.ShiftPositionBy, shift, requestId)); 136 | } 137 | 138 | public long UpdatePosition(Vector3D newPosition) 139 | { 140 | var requestId = Interlocked.Increment(ref _requestId); 141 | SendHandlerMessage(new InteractionTrackerRequest(RequestType.UpdatePositionTo, newPosition, requestId)); 142 | return requestId; 143 | } 144 | 145 | public void SetMaxPosition(Vector3D maxPosition) 146 | { 147 | var requestId = Interlocked.Increment(ref _requestId); 148 | SendHandlerMessage(new InteractionTrackerRequest(RequestType.SetMaxPosition, maxPosition, requestId)); 149 | } 150 | 151 | private void SendHandlerMessage(InteractionTrackerRequest message) 152 | { 153 | _message.Requests.Add(message); 154 | RegisterForSerialization(); 155 | } 156 | 157 | public override void SerializeChangesCore(BatchStreamWriter writer) 158 | { 159 | base.SerializeChangesCore(writer); 160 | _message.Serialize(writer); 161 | _message.Clear(); 162 | } 163 | 164 | #region Owner 165 | 166 | private readonly object _valuesChangedLocker = new(); 167 | private InteractionTrackerValuesChangedArgs _valuesChangedArg; 168 | 169 | internal void ValuesChanged(Vector3D position, long requestId) 170 | { 171 | if (_owner == null) 172 | return; 173 | 174 | var arg = new InteractionTrackerValuesChangedArgs(position, requestId); 175 | bool dispatch = false; 176 | lock (_valuesChangedLocker) 177 | { 178 | dispatch = _valuesChangedArg == null; 179 | _valuesChangedArg = arg; 180 | } 181 | 182 | if (dispatch) 183 | Compositor.Dispatcher.InvokeAsync(UIValuesChanged, DispatcherPriority.Input); 184 | } 185 | 186 | private void UIValuesChanged() 187 | { 188 | InteractionTrackerValuesChangedArgs arg; 189 | lock (_valuesChangedLocker) 190 | { 191 | arg = _valuesChangedArg; 192 | _valuesChangedArg = null; 193 | } 194 | 195 | _owner.ValuesChanged(this, arg); 196 | } 197 | 198 | internal void IdleStateEntered(long requestId) 199 | { 200 | if (_owner == null) 201 | return; 202 | 203 | var arg = new InteractionTrackerIdleStateEnteredArgs(requestId); 204 | Compositor.Dispatcher.InvokeAsync(() => _owner.IdleStateEntered(this, arg), DispatcherPriority.Input); 205 | } 206 | 207 | internal void InertiaStateEntered(bool fromImpulse, Vector3D? modifiedRestingPosition, Vector3D naturalRestingPosition, Vector3D positionVelocity, long requestId) 208 | { 209 | if (_owner == null) 210 | return; 211 | 212 | var arg = new InteractionTrackerInertiaStateEnteredArgs(fromImpulse, modifiedRestingPosition, naturalRestingPosition, positionVelocity, requestId); 213 | Compositor.Dispatcher.InvokeAsync(() => _owner.InertiaStateEntered(this, arg), DispatcherPriority.Input); 214 | } 215 | 216 | internal void InteractingStateEntered(long requestId) 217 | { 218 | if (_owner == null) 219 | return; 220 | 221 | var arg = new InteractionTrackerInteractingStateEnteredArgs(requestId); 222 | Compositor.Dispatcher.InvokeAsync(() => _owner.InteractingStateEntered(this, arg), DispatcherPriority.Input); 223 | } 224 | 225 | #endregion 226 | } 227 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd 366 | 367 | ## 368 | ## Visual studio for Mac 369 | ## 370 | 371 | 372 | # globs 373 | Makefile.in 374 | *.userprefs 375 | *.usertasks 376 | config.make 377 | config.status 378 | aclocal.m4 379 | install-sh 380 | autom4te.cache/ 381 | *.tar.gz 382 | tarballs/ 383 | test-results/ 384 | 385 | # Mac bundle stuff 386 | *.dmg 387 | *.app 388 | 389 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 390 | # General 391 | .DS_Store 392 | .AppleDouble 393 | .LSOverride 394 | 395 | # Icon must end with two \r 396 | Icon 397 | 398 | 399 | # Thumbnails 400 | ._* 401 | 402 | # Files that might appear in the root of a volume 403 | .DocumentRevisions-V100 404 | .fseventsd 405 | .Spotlight-V100 406 | .TemporaryItems 407 | .Trashes 408 | .VolumeIcon.icns 409 | .com.apple.timemachine.donotpresent 410 | 411 | # Directories potentially created on remote AFP share 412 | .AppleDB 413 | .AppleDesktop 414 | Network Trash Folder 415 | Temporary Items 416 | .apdisk 417 | 418 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 419 | # Windows thumbnail cache files 420 | Thumbs.db 421 | ehthumbs.db 422 | ehthumbs_vista.db 423 | 424 | # Dump file 425 | *.stackdump 426 | 427 | # Folder config file 428 | [Dd]esktop.ini 429 | 430 | # Recycle Bin used on file shares 431 | $RECYCLE.BIN/ 432 | 433 | # Windows Installer files 434 | *.cab 435 | *.msi 436 | *.msix 437 | *.msm 438 | *.msp 439 | 440 | # Windows shortcuts 441 | *.lnk 442 | 443 | # JetBrains Rider 444 | .idea/ 445 | *.sln.iml 446 | 447 | ## 448 | ## Visual Studio Code 449 | ## 450 | .vscode/* 451 | !.vscode/settings.json 452 | !.vscode/tasks.json 453 | !.vscode/launch.json 454 | !.vscode/extensions.json 455 | -------------------------------------------------------------------------------- /CompositionScroll/Interactions/Server/ServerInteractionTracker.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Rendering.Composition.Expressions; 3 | using Avalonia.Rendering.Composition.Server; 4 | using Avalonia.Rendering.Composition.Transport; 5 | using System; 6 | using System.IO; 7 | 8 | namespace CompositionScroll.Interactions.Server 9 | { 10 | internal sealed partial class ServerInteractionTracker : ServerObject, IDisposable, IServerClockItem 11 | { 12 | internal static CompositionProperty IdOfPositionProperty = 13 | CompositionProperty.Register( 14 | "Position", 15 | obj => ((ServerInteractionTracker)obj)._position, 16 | (obj, v) => ((ServerInteractionTracker)obj)._position = v, 17 | obj => ((ServerInteractionTracker)obj)._position); 18 | internal static CompositionProperty IdOfMinPositionProperty = 19 | CompositionProperty.Register( 20 | "MinPosition", 21 | obj => ((ServerInteractionTracker)obj)._minPosition, 22 | (obj, v) => ((ServerInteractionTracker)obj)._minPosition = v, 23 | obj => ((ServerInteractionTracker)obj)._minPosition); 24 | internal static CompositionProperty IdOfMaxPositionProperty = 25 | CompositionProperty.Register( 26 | "MaxPosition", 27 | obj => ((ServerInteractionTracker)obj)._maxPosition, 28 | (obj, v) => ((ServerInteractionTracker)obj)._maxPosition = v, 29 | obj => ((ServerInteractionTracker)obj)._maxPosition); 30 | 31 | private Vector3D _position; 32 | private Vector3D _minPosition; 33 | private Vector3D _maxPosition; 34 | 35 | private enum State 36 | { 37 | Idle, 38 | Inertia, 39 | Interaction 40 | } 41 | 42 | private Scroller _scroller; 43 | private InteractionScroller _interactionScroller; 44 | private State _state = State.Idle; 45 | private InteractionTracker _interactionTracker; 46 | 47 | private bool _maxminInvalidated = false; 48 | 49 | public ServerInteractionTracker(ServerCompositor compositor) 50 | : base(compositor) 51 | { 52 | _scroller = new Scroller(this); 53 | _interactionScroller = new(); 54 | } 55 | 56 | public Vector3D Position 57 | { 58 | get => _position; 59 | private set => SetAnimatedValue(IdOfPositionProperty, out _position, value); 60 | } 61 | 62 | public Vector3D MinPosition 63 | { 64 | get => _minPosition; 65 | set => SetAnimatedValue(IdOfMinPositionProperty, out _minPosition, value); 66 | } 67 | 68 | public Vector3D MaxPosition 69 | { 70 | get => _maxPosition; 71 | set => SetAnimatedValue(IdOfMaxPositionProperty, out _maxPosition, value); 72 | } 73 | 74 | public void Init(InteractionTracker tracker) 75 | { 76 | _interactionTracker = tracker; 77 | Compositor.Animations.AddToClock(this); 78 | } 79 | 80 | public void Dispose() 81 | { 82 | Compositor.Animations.RemoveFromClock(this); 83 | } 84 | 85 | public void OnTick() 86 | { 87 | switch (_state) 88 | { 89 | case State.Idle: 90 | if (_maxminInvalidated) 91 | { 92 | SetPosition(_position, 0); 93 | _maxminInvalidated = false; 94 | } 95 | return; 96 | 97 | case State.Inertia: 98 | if (!_scroller.Tick()) 99 | { 100 | SetPosition(_scroller.Position, 0); 101 | GoToState(State.Idle); 102 | } 103 | else if (!SetPosition(_scroller.Position, 0)) 104 | { 105 | _scroller.ForceFinished(); 106 | GoToState(State.Idle); 107 | } 108 | return; 109 | case State.Interaction: 110 | SetPosition(_interactionScroller.Position, 0); 111 | if (_interactionScroller.Finished) 112 | { 113 | GoToState(State.Idle); 114 | } 115 | return; 116 | } 117 | } 118 | 119 | private void GoToState(State state, bool force = false) 120 | { 121 | if (state == _state && !force) 122 | return; 123 | 124 | var prevState = _state; 125 | _state = state; 126 | switch (state) 127 | { 128 | case State.Idle: 129 | _interactionTracker.IdleStateEntered(0); 130 | break; 131 | case State.Inertia: 132 | _interactionTracker.InertiaStateEntered(prevState != State.Interaction, null, _scroller.EndPosition, _scroller.Velocity, 0); 133 | break; 134 | case State.Interaction: 135 | _interactionTracker.InteractingStateEntered(0); 136 | break; 137 | } 138 | } 139 | 140 | private bool SetPosition(Vector3D newPosition, long requestId) 141 | { 142 | var newPositionClamp = Vector3D.Clamp(newPosition, MinPosition, MaxPosition); 143 | 144 | if (_position == newPositionClamp) 145 | return newPositionClamp == newPosition; 146 | 147 | Position = newPositionClamp; 148 | _interactionTracker.ValuesChanged(_position, requestId); 149 | return newPositionClamp == newPosition; 150 | } 151 | 152 | protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) 153 | { 154 | base.DeserializeChangesCore(reader, committedAt); 155 | var count = reader.Read(); 156 | for (var c = 0; c < count; c++) 157 | OnMessage(reader.Read()); 158 | } 159 | 160 | private void OnMessage(InteractionTrackerRequest request) 161 | { 162 | switch (request.Type) 163 | { 164 | case RequestType.ShiftPositionBy: 165 | { 166 | var shift = request.VectorValue.Value; 167 | switch (_state) 168 | { 169 | case State.Idle: 170 | SetPosition(_position + shift, 0); 171 | break; 172 | case State.Inertia: 173 | _scroller.Shift(shift); 174 | break; 175 | case State.Interaction: 176 | _interactionScroller.Shift(shift); 177 | break; 178 | } 179 | } 180 | break; 181 | case RequestType.AnimatePositionBy: 182 | if (_state != State.Interaction) 183 | { 184 | _scroller.ForcePosition(_position); 185 | _scroller.StartInertia(request.VectorValue.Value, request.TimeSpanValue ?? TimeSpan.FromMilliseconds(250)); 186 | GoToState(State.Inertia, true); 187 | } 188 | break; 189 | case RequestType.AnimatePositionTo: 190 | if (_state != State.Interaction) 191 | { 192 | _scroller.ForcePosition(_position); 193 | _scroller.StartInertiaTo(request.VectorValue.Value, request.TimeSpanValue ?? TimeSpan.FromMilliseconds(250)); 194 | GoToState(State.Inertia, true); 195 | } 196 | break; 197 | case RequestType.UpdatePositionTo: 198 | if (_state != State.Interaction) 199 | { 200 | _scroller.ForceFinished(); 201 | _interactionScroller.EndInteraction(); 202 | SetPosition(request.VectorValue.Value, request.RequestId); 203 | } 204 | break; 205 | 206 | // Interaction 207 | case RequestType.InteractionStart: 208 | _scroller.ForceFinished(); 209 | _interactionScroller.StartInteraction(_position); 210 | GoToState(State.Interaction); 211 | break; 212 | case RequestType.InteractionMove: 213 | if (_state == State.Interaction) 214 | { 215 | _interactionScroller.Move(request.VectorValue.Value); 216 | } 217 | break; 218 | case RequestType.InteractionEnd: 219 | if (_state == State.Interaction) 220 | { 221 | _interactionScroller.EndInteraction(); 222 | GoToState(State.Idle); 223 | } 224 | break; 225 | case RequestType.InteractionEndWithInertia: 226 | if (_state == State.Interaction) 227 | { 228 | _interactionScroller.EndInteraction(); 229 | _scroller.ForceFinished(); 230 | _scroller.ForcePosition(_position); 231 | _scroller.StartFlingInertia(request.VectorValue.Value); 232 | GoToState(State.Inertia); 233 | } 234 | break; 235 | 236 | case RequestType.SetMaxPosition: 237 | MaxPosition = request.VectorValue.Value; 238 | _maxminInvalidated = true; 239 | break; 240 | default: 241 | break; 242 | } 243 | } 244 | 245 | public override CompositionProperty GetCompositionProperty(string fieldName) 246 | { 247 | switch (fieldName) 248 | { 249 | case nameof(Position): 250 | return IdOfPositionProperty; 251 | case nameof(MinPosition): 252 | return IdOfMinPositionProperty; 253 | case nameof(MaxPosition): 254 | return IdOfMaxPositionProperty; 255 | } 256 | 257 | return base.GetCompositionProperty(fieldName); 258 | } 259 | } 260 | } -------------------------------------------------------------------------------- /CompositionScroll/CompositionScrollContentPresenter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Presenters; 4 | using Avalonia.Controls.Primitives; 5 | using Avalonia.Data; 6 | using Avalonia.Interactivity; 7 | using Avalonia.Layout; 8 | using Avalonia.Reactive; 9 | using Avalonia.Rendering.Composition; 10 | using Avalonia.Rendering.Composition.Animations; 11 | using Avalonia.Utilities; 12 | using Avalonia.VisualTree; 13 | using CompositionScroll.Interactions; 14 | using System; 15 | using System.Collections.Generic; 16 | using System.Linq; 17 | 18 | namespace CompositionScroll 19 | { 20 | [Flags] 21 | public enum ScrollFeaturesEnum 22 | { 23 | None = 0, 24 | MousePressedScroll = 1, 25 | MousePressedScrollEnertia = 2, 26 | WheelSwapDirections = 4, 27 | } 28 | 29 | /// 30 | /// Presents a scrolling view of content inside a . 31 | /// 32 | public sealed class CompositionScrollContentPresenter : ContentPresenter, IScrollable, IScrollAnchorProvider, IInteractionTrackerOwner 33 | { 34 | private const double EdgeDetectionTolerance = 0.1; 35 | 36 | public static readonly AttachedProperty ScrollFeaturesProperty = 37 | AvaloniaProperty.RegisterAttached("ScrollFeatures", defaultValue: ScrollFeaturesEnum.None); 38 | 39 | /// 40 | /// Defines the property. 41 | /// 42 | public static readonly StyledProperty CanHorizontallyScrollProperty = 43 | AvaloniaProperty.Register(nameof(CanHorizontallyScroll)); 44 | 45 | /// 46 | /// Defines the property. 47 | /// 48 | public static readonly StyledProperty CanVerticallyScrollProperty = 49 | AvaloniaProperty.Register(nameof(CanVerticallyScroll)); 50 | 51 | /// 52 | /// Defines the property. 53 | /// 54 | public static readonly DirectProperty ExtentProperty = 55 | ScrollViewer.ExtentProperty.AddOwner( 56 | o => o.Extent); 57 | 58 | /// 59 | /// Defines the property. 60 | /// 61 | public static readonly StyledProperty OffsetProperty = 62 | ScrollViewer.OffsetProperty.AddOwner(new(coerce: ScrollViewer.CoerceOffset)); 63 | 64 | /// 65 | /// Defines the property. 66 | /// 67 | public static readonly DirectProperty ViewportProperty = 68 | ScrollViewer.ViewportProperty.AddOwner( 69 | o => o.Viewport); 70 | 71 | /// 72 | /// Defines the property. 73 | /// 74 | public static readonly StyledProperty HorizontalSnapPointsTypeProperty = 75 | ScrollViewer.HorizontalSnapPointsTypeProperty.AddOwner(); 76 | 77 | /// 78 | /// Defines the property. 79 | /// 80 | public static readonly StyledProperty VerticalSnapPointsTypeProperty = 81 | ScrollViewer.VerticalSnapPointsTypeProperty.AddOwner(); 82 | 83 | /// 84 | /// Defines the property. 85 | /// 86 | public static readonly StyledProperty HorizontalSnapPointsAlignmentProperty = 87 | ScrollViewer.HorizontalSnapPointsAlignmentProperty.AddOwner(); 88 | 89 | /// 90 | /// Defines the property. 91 | /// 92 | public static readonly StyledProperty VerticalSnapPointsAlignmentProperty = 93 | ScrollViewer.VerticalSnapPointsAlignmentProperty.AddOwner(); 94 | 95 | /// 96 | /// Defines the property. 97 | /// 98 | public static readonly StyledProperty IsScrollChainingEnabledProperty = 99 | ScrollViewer.IsScrollChainingEnabledProperty.AddOwner(); 100 | 101 | private ScrollFeaturesEnum _scrollFeatures = ScrollFeaturesEnum.None; 102 | private InteractionTracker _interactionTracker; 103 | private ExpressionAnimation _scrollAnimation; 104 | private bool _compositionUpdate; 105 | private long? requestId; 106 | private ScrollPropertiesSource _scrollPropertiesSource; 107 | 108 | private bool _arranging; 109 | private Size _extent; 110 | private Size _viewport; 111 | private HashSet? _anchorCandidates; 112 | private Control? _anchorElement; 113 | private Rect _anchorElementBounds; 114 | private bool _isAnchorElementDirty; 115 | private bool _areVerticalSnapPointsRegular; 116 | private bool _areHorizontalSnapPointsRegular; 117 | private IReadOnlyList? _horizontalSnapPoints; 118 | private double _horizontalSnapPoint; 119 | private IReadOnlyList? _verticalSnapPoints; 120 | private double _verticalSnapPoint; 121 | private double _verticalSnapPointOffset; 122 | private double _horizontalSnapPointOffset; 123 | private CompositeDisposable? _ownerSubscriptions; 124 | private ScrollViewer? _owner; 125 | private IScrollSnapPointsInfo? _scrollSnapPointsInfo; 126 | private bool _isSnapPointsUpdated; 127 | private InteractionTrackerInertiaStateEnteredArgs _inertiaArgs; 128 | 129 | private readonly InteractionSource _interactionGesture; 130 | 131 | /// 132 | /// Initializes static members of the class. 133 | /// 134 | static CompositionScrollContentPresenter() 135 | { 136 | ClipToBoundsProperty.OverrideDefaultValue(typeof(CompositionScrollContentPresenter), true); 137 | AffectsMeasure(CanHorizontallyScrollProperty, CanVerticallyScrollProperty); 138 | } 139 | 140 | /// 141 | /// Initializes a new instance of the class. 142 | /// 143 | public CompositionScrollContentPresenter() 144 | { 145 | AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); 146 | _interactionGesture = new InteractionSource(this); 147 | GestureRecognizers.Add(_interactionGesture); 148 | } 149 | 150 | public static ScrollFeaturesEnum GetScrollFeatures(Control element) 151 | { 152 | return element.GetValue(ScrollFeaturesProperty); 153 | } 154 | 155 | public static void SetScrollFeatures(Control element, ScrollFeaturesEnum value) 156 | { 157 | element.SetValue(ScrollFeaturesProperty, value); 158 | } 159 | 160 | /// 161 | /// Gets or sets a value indicating whether the content can be scrolled horizontally. 162 | /// 163 | public bool CanHorizontallyScroll 164 | { 165 | get => GetValue(CanHorizontallyScrollProperty); 166 | set => SetValue(CanHorizontallyScrollProperty, value); 167 | } 168 | 169 | /// 170 | /// Gets or sets a value indicating whether the content can be scrolled horizontally. 171 | /// 172 | public bool CanVerticallyScroll 173 | { 174 | get => GetValue(CanVerticallyScrollProperty); 175 | set => SetValue(CanVerticallyScrollProperty, value); 176 | } 177 | 178 | /// 179 | /// Gets the extent of the scrollable content. 180 | /// 181 | public Size Extent 182 | { 183 | get => _extent; 184 | private set => SetAndRaise(ExtentProperty, ref _extent, value); 185 | } 186 | 187 | /// 188 | /// Gets or sets the current scroll offset. 189 | /// 190 | public Vector Offset 191 | { 192 | get => GetValue(OffsetProperty); 193 | set => SetValue(OffsetProperty, value); 194 | } 195 | 196 | /// 197 | /// Gets the size of the viewport on the scrollable content. 198 | /// 199 | public Size Viewport 200 | { 201 | get => _viewport; 202 | private set => SetAndRaise(ViewportProperty, ref _viewport, value); 203 | } 204 | 205 | /// 206 | /// Gets or sets how scroll gesture reacts to the snap points along the horizontal axis. 207 | /// 208 | public SnapPointsType HorizontalSnapPointsType 209 | { 210 | get => GetValue(HorizontalSnapPointsTypeProperty); 211 | set => SetValue(HorizontalSnapPointsTypeProperty, value); 212 | } 213 | 214 | /// 215 | /// Gets or sets how scroll gesture reacts to the snap points along the vertical axis. 216 | /// 217 | public SnapPointsType VerticalSnapPointsType 218 | { 219 | get => GetValue(VerticalSnapPointsTypeProperty); 220 | set => SetValue(VerticalSnapPointsTypeProperty, value); 221 | } 222 | 223 | /// 224 | /// Gets or sets how the existing snap points are horizontally aligned versus the initial viewport. 225 | /// 226 | public SnapPointsAlignment HorizontalSnapPointsAlignment 227 | { 228 | get => GetValue(HorizontalSnapPointsAlignmentProperty); 229 | set => SetValue(HorizontalSnapPointsAlignmentProperty, value); 230 | } 231 | 232 | /// 233 | /// Gets or sets how the existing snap points are vertically aligned versus the initial viewport. 234 | /// 235 | public SnapPointsAlignment VerticalSnapPointsAlignment 236 | { 237 | get => GetValue(VerticalSnapPointsAlignmentProperty); 238 | set => SetValue(VerticalSnapPointsAlignmentProperty, value); 239 | } 240 | 241 | /// 242 | /// Gets or sets if scroll chaining is enabled. The default value is true. 243 | /// 244 | /// 245 | /// After a user hits a scroll limit on an element that has been nested within another scrollable element, 246 | /// you can specify whether that parent element should continue the scrolling operation begun in its child element. 247 | /// This is called scroll chaining. 248 | /// 249 | public bool IsScrollChainingEnabled 250 | { 251 | get => GetValue(IsScrollChainingEnabledProperty); 252 | set => SetValue(IsScrollChainingEnabledProperty, value); 253 | } 254 | 255 | /// 256 | Control? IScrollAnchorProvider.CurrentAnchor 257 | { 258 | get 259 | { 260 | EnsureAnchorElementSelection(); 261 | return _anchorElement; 262 | } 263 | } 264 | 265 | /// 266 | /// Attempts to bring a portion of the target visual into view by scrolling the content. 267 | /// 268 | /// The target visual. 269 | /// The portion of the target visual to bring into view. 270 | /// True if the scroll offset was changed; otherwise false. 271 | public bool BringDescendantIntoView(Visual target, Rect targetRect) 272 | { 273 | if (Child?.IsEffectivelyVisible != true) 274 | { 275 | return false; 276 | } 277 | 278 | var control = target as Control; 279 | 280 | var transform = target.TransformToVisual(Child); 281 | 282 | if (transform == null) 283 | { 284 | return false; 285 | } 286 | 287 | var rectangle = targetRect.TransformToAABB(transform.Value).Deflate(new Thickness(Child.Margin.Left, Child.Margin.Top, 0, 0)); 288 | Rect viewport = new Rect(Offset.X, Offset.Y, Viewport.Width, Viewport.Height); 289 | 290 | double minX = ComputeScrollOffsetWithMinimalScroll(viewport.Left, viewport.Right, rectangle.Left, rectangle.Right); 291 | double minY = ComputeScrollOffsetWithMinimalScroll(viewport.Top, viewport.Bottom, rectangle.Top, rectangle.Bottom); 292 | var offset = new Vector(minX, minY); 293 | 294 | if (Offset.NearlyEquals(offset)) 295 | { 296 | return false; 297 | } 298 | 299 | var oldOffset = Offset; 300 | SetCurrentValue(OffsetProperty, offset); 301 | 302 | // It's possible that the Offset coercion has changed the offset back to its previous value, 303 | // this is common for floating point rounding errors. 304 | return !Offset.NearlyEquals(oldOffset); 305 | } 306 | 307 | /// 308 | /// Computes the closest offset to ensure most of the child is visible in the viewport along an axis. 309 | /// 310 | /// The left or top of the viewport 311 | /// The right or bottom of the viewport 312 | /// The left or top of the child 313 | /// The right or bottom of the child 314 | /// 315 | internal static double ComputeScrollOffsetWithMinimalScroll( 316 | double viewportStart, 317 | double viewportEnd, 318 | double childStart, 319 | double childEnd) 320 | { 321 | // If child is at least partially above viewport, i.e. top of child is above viewport top and bottom of child is above viewport bottom. 322 | bool isChildAbove = MathUtilities.LessThan(childStart, viewportStart) && MathUtilities.LessThan(childEnd, viewportEnd); 323 | 324 | // If child is at least partially below viewport, i.e. top of child is below viewport top and bottom of child is below viewport bottom. 325 | bool isChildBelow = MathUtilities.GreaterThan(childEnd, viewportEnd) && MathUtilities.GreaterThan(childStart, viewportStart); 326 | bool isChildLarger = (childEnd - childStart) > (viewportEnd - viewportStart); 327 | 328 | // Value if no updates is needed. The child is fully visible in the viewport, or the viewport is completely within the child's bounds 329 | var res = viewportStart; 330 | 331 | // The child is above the viewport and is smaller than the viewport, or if the child's top is below the viewport top 332 | // and is larger than the viewport, we align the child top to the top of the viewport 333 | if ((isChildAbove && !isChildLarger) 334 | || (isChildBelow && isChildLarger)) 335 | { 336 | res = childStart; 337 | } 338 | // The child is above the viewport and is larger than the viewport, or if the child's smaller but is below the viewport, 339 | // we align the child's bottom to the bottom of the viewport 340 | else if (isChildAbove || isChildBelow) 341 | { 342 | res = (childEnd - (viewportEnd - viewportStart)); 343 | } 344 | 345 | return res; 346 | } 347 | 348 | protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) 349 | { 350 | base.OnAttachedToVisualTree(e); 351 | AttachToScrollViewer(); 352 | 353 | var compositionVisual = ElementComposition.GetElementVisual(this); 354 | _interactionTracker = compositionVisual.Compositor.CreateInteractionTracker(this); 355 | _interactionTracker.InteractionSource = _interactionGesture; 356 | UpdateScrollAnimation(); 357 | UpdateInteractionOptions(); 358 | } 359 | 360 | protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) 361 | { 362 | base.OnDetachedFromVisualTree(e); 363 | _interactionTracker?.Dispose(); 364 | _interactionTracker = null; 365 | _scrollAnimation?.Dispose(); 366 | _scrollAnimation = null; 367 | _scrollPropertiesSource?.Dispose(); 368 | _scrollPropertiesSource = null; 369 | } 370 | 371 | /// 372 | /// Locates the first ancestor and binds to it. Properties which have been set through other means are not bound. 373 | /// 374 | /// 375 | /// This method is automatically called when the control is attached to a visual tree. 376 | /// 377 | internal void AttachToScrollViewer() 378 | { 379 | var owner = this.FindAncestorOfType(); 380 | 381 | if (owner == null) 382 | { 383 | _owner = null; 384 | _ownerSubscriptions?.Dispose(); 385 | _ownerSubscriptions = null; 386 | return; 387 | } 388 | 389 | if (owner == _owner) 390 | { 391 | return; 392 | } 393 | 394 | _ownerSubscriptions?.Dispose(); 395 | _owner = owner; 396 | 397 | var subscriptionDisposables = new IDisposable?[] 398 | { 399 | IfUnset(CanHorizontallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.HorizontalScrollBarVisibilityProperty, NotDisabled), BindingPriority.Template)), 400 | IfUnset(CanVerticallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.VerticalScrollBarVisibilityProperty, NotDisabled), BindingPriority.Template)), 401 | IfUnset(OffsetProperty, p => Bind(p, owner.GetBindingObservable(ScrollViewer.OffsetProperty), BindingPriority.Template)), 402 | IfUnset(IsScrollChainingEnabledProperty, p => Bind(p, owner.GetBindingObservable(ScrollViewer.IsScrollChainingEnabledProperty), BindingPriority.Template)), 403 | IfUnset(ContentProperty, p => Bind(p, owner.GetBindingObservable(ContentProperty), BindingPriority.Template)), 404 | }.Where(d => d != null).Cast().ToArray(); 405 | 406 | _ownerSubscriptions = new CompositeDisposable(subscriptionDisposables); 407 | 408 | static bool NotDisabled(ScrollBarVisibility v) => v != ScrollBarVisibility.Disabled; 409 | 410 | IDisposable? IfUnset(T property, Func func) where T : AvaloniaProperty => IsSet(property) ? null : func(property); 411 | } 412 | 413 | /// 414 | void IScrollAnchorProvider.RegisterAnchorCandidate(Control element) 415 | { 416 | if (!this.IsVisualAncestorOf(element)) 417 | { 418 | throw new InvalidOperationException( 419 | "An anchor control must be a visual descendent of the ScrollContentPresenter."); 420 | } 421 | 422 | _anchorCandidates ??= new(); 423 | _anchorCandidates.Add(element); 424 | _isAnchorElementDirty = true; 425 | } 426 | 427 | /// 428 | void IScrollAnchorProvider.UnregisterAnchorCandidate(Control element) 429 | { 430 | _anchorCandidates?.Remove(element); 431 | _isAnchorElementDirty = true; 432 | 433 | if (_anchorElement == element) 434 | { 435 | _anchorElement = null; 436 | } 437 | } 438 | 439 | /// 440 | protected override Size MeasureOverride(Size availableSize) 441 | { 442 | if (Child == null) 443 | { 444 | return base.MeasureOverride(availableSize); 445 | } 446 | 447 | var availableWithPadding = availableSize.Deflate(Padding); 448 | var constraint = new Size( 449 | CanHorizontallyScroll ? double.PositiveInfinity : availableWithPadding.Width, 450 | CanVerticallyScroll ? double.PositiveInfinity : availableWithPadding.Height); 451 | 452 | Child.Measure(constraint); 453 | 454 | if (!_isSnapPointsUpdated) 455 | { 456 | _isSnapPointsUpdated = true; 457 | UpdateSnapPoints(); 458 | } 459 | 460 | return Child.DesiredSize.Inflate(Padding).Constrain(availableSize); 461 | } 462 | 463 | /// 464 | protected override Size ArrangeOverride(Size finalSize) 465 | { 466 | if (Child == null) 467 | { 468 | return base.ArrangeOverride(finalSize); 469 | } 470 | 471 | return ArrangeWithAnchoring(finalSize); 472 | } 473 | 474 | private Size ArrangeWithAnchoring(Size finalSize) 475 | { 476 | var size = new Size( 477 | CanHorizontallyScroll ? Math.Max(Child!.DesiredSize.Inflate(Padding).Width, finalSize.Width) : finalSize.Width, 478 | CanVerticallyScroll ? Math.Max(Child!.DesiredSize.Inflate(Padding).Height, finalSize.Height) : finalSize.Height); 479 | 480 | var isAnchoring = Offset.X >= EdgeDetectionTolerance || Offset.Y >= EdgeDetectionTolerance; 481 | 482 | if (isAnchoring) 483 | { 484 | // Calculate the new anchor element if necessary. 485 | EnsureAnchorElementSelection(); 486 | 487 | // Do the arrange. 488 | ArrangeOverrideImpl(size, -Offset); 489 | 490 | // If the anchor moved during the arrange, we need to adjust the offset and do another arrange. 491 | var anchorShift = TrackAnchor(); 492 | 493 | if (anchorShift != default) 494 | { 495 | var newOffset = Offset + anchorShift; 496 | var newExtent = Extent; 497 | var maxOffset = new Vector(Extent.Width - Viewport.Width, Extent.Height - Viewport.Height); 498 | 499 | if (newOffset.X > maxOffset.X) 500 | { 501 | newExtent = newExtent.WithWidth(newOffset.X + Viewport.Width); 502 | } 503 | 504 | if (newOffset.Y > maxOffset.Y) 505 | { 506 | newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height); 507 | } 508 | 509 | Extent = newExtent; 510 | 511 | try 512 | { 513 | _arranging = true; 514 | _compositionUpdate = true; 515 | _interactionTracker?.ShiftPositionBy(new Vector3D(anchorShift.X, anchorShift.Y, 0)); 516 | SetCurrentValue(OffsetProperty, newOffset); 517 | } 518 | finally 519 | { 520 | _arranging = false; 521 | _compositionUpdate = false; 522 | } 523 | } 524 | 525 | ArrangeOverrideImpl(size, -Offset); 526 | } 527 | else 528 | { 529 | ArrangeOverrideImpl(size, -Offset); 530 | } 531 | 532 | Viewport = finalSize; 533 | Extent = ComputeExtent(finalSize); 534 | _isAnchorElementDirty = true; 535 | 536 | var scrollableHeight = Extent.Height - Viewport.Height; 537 | var scrollableWidth = Extent.Width - Viewport.Width; 538 | _interactionTracker?.SetMaxPosition(new Vector3D((float)scrollableWidth, (float)scrollableHeight, 0)); 539 | 540 | return finalSize; 541 | } 542 | 543 | private Size ComputeExtent(Size viewportSize) 544 | { 545 | var childMargin = Child!.Margin + Padding; 546 | 547 | if (Child.UseLayoutRounding) 548 | { 549 | var scale = LayoutHelper.GetLayoutScale(Child); 550 | childMargin = LayoutHelper.RoundLayoutThickness(childMargin, scale, scale); 551 | } 552 | 553 | var extent = Child!.Bounds.Size.Inflate(childMargin); 554 | 555 | if (MathUtilities.AreClose(extent.Width, viewportSize.Width, LayoutHelper.LayoutEpsilon)) 556 | extent = extent.WithWidth(viewportSize.Width); 557 | 558 | if (MathUtilities.AreClose(extent.Height, viewportSize.Height, LayoutHelper.LayoutEpsilon)) 559 | extent = extent.WithHeight(viewportSize.Height); 560 | 561 | return extent; 562 | } 563 | 564 | protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) 565 | { 566 | if (change.Property == OffsetProperty) 567 | { 568 | if (!_arranging) 569 | { 570 | InvalidateArrange(); 571 | } 572 | 573 | if (!_compositionUpdate) 574 | { 575 | var offset = change.GetNewValue(); 576 | requestId = _interactionTracker?.UpdatePosition(new Vector3D(offset.X, offset.Y, 0)); 577 | } 578 | else 579 | { 580 | requestId = null; 581 | } 582 | 583 | _owner?.SetCurrentValue(OffsetProperty, change.GetNewValue()); 584 | } 585 | else if (change.Property == ChildProperty) 586 | { 587 | ChildChanged(change); 588 | } 589 | else if (change.Property == HorizontalSnapPointsAlignmentProperty || 590 | change.Property == VerticalSnapPointsAlignmentProperty) 591 | { 592 | UpdateSnapPoints(); 593 | } 594 | else if (change.Property == ExtentProperty) 595 | { 596 | if (_owner != null) 597 | { 598 | _owner.Extent = change.GetNewValue(); 599 | } 600 | CoerceValue(OffsetProperty); 601 | } 602 | else if (change.Property == ViewportProperty) 603 | { 604 | if (_owner != null) 605 | { 606 | _owner.Viewport = change.GetNewValue(); 607 | } 608 | CoerceValue(OffsetProperty); 609 | } 610 | else if (change.Property == PaddingProperty) 611 | { 612 | _scrollAnimation = null; 613 | UpdateScrollAnimation(); 614 | } 615 | else 616 | if (change.Property == ScrollFeaturesProperty || 617 | change.Property == CanVerticallyScrollProperty || 618 | change.Property == CanHorizontallyScrollProperty) 619 | UpdateInteractionOptions(); 620 | 621 | base.OnPropertyChanged(change); 622 | } 623 | 624 | private void ScrollSnapPointsInfoSnapPointsChanged(object? sender, RoutedEventArgs e) 625 | { 626 | UpdateSnapPoints(); 627 | } 628 | 629 | private void BringIntoViewRequested(object? sender, RequestBringIntoViewEventArgs e) 630 | { 631 | if (e.TargetObject is not null) 632 | e.Handled = BringDescendantIntoView(e.TargetObject, e.TargetRect); 633 | } 634 | 635 | private void ChildChanged(AvaloniaPropertyChangedEventArgs e) 636 | { 637 | if (e.OldValue != null) 638 | { 639 | SetCurrentValue(OffsetProperty, default); 640 | var compositionVisual = ElementComposition.GetElementVisual(e.OldValue as Control); 641 | if (compositionVisual != null) 642 | { 643 | compositionVisual.ImplicitAnimations = null; 644 | } 645 | } 646 | 647 | UpdateScrollAnimation(); 648 | } 649 | 650 | private void EnsureAnchorElementSelection() 651 | { 652 | if (!_isAnchorElementDirty || _anchorCandidates is null) 653 | { 654 | return; 655 | } 656 | 657 | _anchorElement = null; 658 | _anchorElementBounds = default; 659 | _isAnchorElementDirty = false; 660 | 661 | var bestCandidate = default(Control); 662 | var bestCandidateDistance = double.MaxValue; 663 | 664 | // Find the anchor candidate that is scrolled closest to the top-left of this 665 | // ScrollContentPresenter. 666 | foreach (var element in _anchorCandidates) 667 | { 668 | if (element.IsVisible && GetViewportBounds(element, out var bounds)) 669 | { 670 | var distance = (Vector)bounds.Position; 671 | var candidateDistance = Math.Abs(distance.Length); 672 | 673 | if (candidateDistance < bestCandidateDistance) 674 | { 675 | bestCandidate = element; 676 | bestCandidateDistance = candidateDistance; 677 | } 678 | } 679 | } 680 | 681 | if (bestCandidate != null) 682 | { 683 | // We have a candidate, calculate its bounds relative to Child. Because these 684 | // bounds aren't relative to the ScrollContentPresenter itself, if they change 685 | // then we know it wasn't just due to scrolling. 686 | var unscrolledBounds = TranslateBounds(bestCandidate, Child!); 687 | _anchorElement = bestCandidate; 688 | _anchorElementBounds = unscrolledBounds; 689 | } 690 | } 691 | 692 | private Vector TrackAnchor() 693 | { 694 | // If we have an anchor and its position relative to Child has changed during the 695 | // arrange then that change wasn't just due to scrolling (as scrolling doesn't adjust 696 | // relative positions within Child). 697 | if (_anchorElement != null && 698 | TranslateBounds(_anchorElement, Child!, out var updatedBounds) && 699 | updatedBounds.Position != _anchorElementBounds.Position) 700 | { 701 | var offset = updatedBounds.Position - _anchorElementBounds.Position; 702 | return offset; 703 | } 704 | 705 | return default; 706 | } 707 | 708 | private bool GetViewportBounds(Control element, out Rect bounds) 709 | { 710 | if (TranslateBounds(element, Child!, out var childBounds)) 711 | { 712 | // We want the bounds relative to the new Offset, regardless of whether the child 713 | // control has actually been arranged to this offset yet, so translate first to the 714 | // child control and then apply Offset rather than translating directly to this 715 | // control. 716 | var thisBounds = new Rect(Bounds.Size); 717 | bounds = new Rect(childBounds.Position - Offset, childBounds.Size); 718 | return bounds.Intersects(thisBounds); 719 | } 720 | 721 | bounds = default; 722 | return false; 723 | } 724 | 725 | private Rect TranslateBounds(Control control, Control to) 726 | { 727 | if (TranslateBounds(control, to, out var bounds)) 728 | { 729 | return bounds; 730 | } 731 | 732 | throw new InvalidOperationException("The control's bounds could not be translated to the requested control."); 733 | } 734 | 735 | private bool TranslateBounds(Control control, Control to, out Rect bounds) 736 | { 737 | if (!control.IsVisible) 738 | { 739 | bounds = default; 740 | return false; 741 | } 742 | 743 | var p = control.TranslatePoint(default, to); 744 | bounds = p.HasValue ? new Rect(p.Value, control.Bounds.Size) : default; 745 | return p.HasValue; 746 | } 747 | 748 | private void UpdateSnapPoints() 749 | { 750 | var scrollable = GetScrollSnapPointsInfo(Content); 751 | 752 | if (scrollable is IScrollSnapPointsInfo scrollSnapPointsInfo) 753 | { 754 | _areVerticalSnapPointsRegular = scrollSnapPointsInfo.AreVerticalSnapPointsRegular; 755 | _areHorizontalSnapPointsRegular = scrollSnapPointsInfo.AreHorizontalSnapPointsRegular; 756 | 757 | if (!_areVerticalSnapPointsRegular) 758 | { 759 | _verticalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Orientation.Vertical, VerticalSnapPointsAlignment); 760 | } 761 | else 762 | { 763 | _verticalSnapPoints = new List(); 764 | _verticalSnapPoint = scrollSnapPointsInfo.GetRegularSnapPoints(Orientation.Vertical, VerticalSnapPointsAlignment, out _verticalSnapPointOffset); 765 | 766 | } 767 | 768 | if (!_areHorizontalSnapPointsRegular) 769 | { 770 | _horizontalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Orientation.Horizontal, HorizontalSnapPointsAlignment); 771 | } 772 | else 773 | { 774 | _horizontalSnapPoints = new List(); 775 | _horizontalSnapPoint = scrollSnapPointsInfo.GetRegularSnapPoints(Orientation.Horizontal, HorizontalSnapPointsAlignment, out _horizontalSnapPointOffset); 776 | } 777 | } 778 | else 779 | { 780 | _horizontalSnapPoints = new List(); 781 | _verticalSnapPoints = new List(); 782 | } 783 | 784 | UpdateScrollModified(); 785 | } 786 | 787 | private void UpdateScrollModified() 788 | { 789 | if (_inertiaArgs == null) 790 | return; 791 | 792 | var pos = new Vector(_inertiaArgs.NaturalRestingPosition.X, _inertiaArgs.NaturalRestingPosition.Y); 793 | 794 | Vector snapPoint; 795 | if (_inertiaArgs.IsInertiaFromImpulse) 796 | { 797 | var vel = new Vector(-_inertiaArgs.PositionVelocityInPixelsPerSecond.X, -_inertiaArgs.PositionVelocityInPixelsPerSecond.Y); 798 | snapPoint = SnapOffset(pos, vel, true); 799 | } 800 | else 801 | { 802 | snapPoint = SnapOffset(pos); 803 | } 804 | 805 | if (snapPoint == pos) 806 | return; 807 | 808 | _interactionTracker.AnimatePositionTo(new Vector3D(snapPoint.X, snapPoint.Y, 0)); 809 | } 810 | 811 | private Vector SnapOffset(Vector offset, Vector direction = default, bool snapToNext = false) 812 | { 813 | var scrollable = GetScrollSnapPointsInfo(Content); 814 | 815 | if (scrollable is null || (VerticalSnapPointsType == SnapPointsType.None && HorizontalSnapPointsType == SnapPointsType.None)) 816 | return offset; 817 | 818 | var diff = GetAlignmentDiff(); 819 | 820 | if (VerticalSnapPointsType != SnapPointsType.None && (_areVerticalSnapPointsRegular || _verticalSnapPoints?.Count > 0) && (!snapToNext || snapToNext && direction.Y != 0)) 821 | { 822 | var estimatedOffset = new Vector(offset.X, offset.Y + diff.Y); 823 | double previousSnapPoint = 0, nextSnapPoint = 0, midPoint = 0; 824 | 825 | if (_areVerticalSnapPointsRegular) 826 | { 827 | previousSnapPoint = (int)(estimatedOffset.Y / _verticalSnapPoint) * _verticalSnapPoint + _verticalSnapPointOffset; 828 | nextSnapPoint = previousSnapPoint + _verticalSnapPoint; 829 | midPoint = (previousSnapPoint + nextSnapPoint) / 2; 830 | } 831 | else if (_verticalSnapPoints?.Count > 0) 832 | { 833 | (previousSnapPoint, nextSnapPoint) = FindNearestSnapPoint(_verticalSnapPoints, estimatedOffset.Y); 834 | midPoint = (previousSnapPoint + nextSnapPoint) / 2; 835 | } 836 | 837 | var nearestSnapPoint = snapToNext ? (direction.Y > 0 ? previousSnapPoint : nextSnapPoint) : 838 | estimatedOffset.Y < midPoint ? previousSnapPoint : nextSnapPoint; 839 | 840 | offset = new Vector(offset.X, nearestSnapPoint - diff.Y); 841 | } 842 | 843 | if (HorizontalSnapPointsType != SnapPointsType.None && (_areHorizontalSnapPointsRegular || _horizontalSnapPoints?.Count > 0) && (!snapToNext || snapToNext && direction.X != 0)) 844 | { 845 | var estimatedOffset = new Vector(offset.X + diff.X, offset.Y); 846 | double previousSnapPoint = 0, nextSnapPoint = 0, midPoint = 0; 847 | 848 | if (_areHorizontalSnapPointsRegular) 849 | { 850 | previousSnapPoint = (int)(estimatedOffset.X / _horizontalSnapPoint) * _horizontalSnapPoint + _horizontalSnapPointOffset; 851 | nextSnapPoint = previousSnapPoint + _horizontalSnapPoint; 852 | midPoint = (previousSnapPoint + nextSnapPoint) / 2; 853 | } 854 | else if (_horizontalSnapPoints?.Count > 0) 855 | { 856 | (previousSnapPoint, nextSnapPoint) = FindNearestSnapPoint(_horizontalSnapPoints, estimatedOffset.X); 857 | midPoint = (previousSnapPoint + nextSnapPoint) / 2; 858 | } 859 | 860 | var nearestSnapPoint = snapToNext ? (direction.X > 0 ? previousSnapPoint : nextSnapPoint) : 861 | estimatedOffset.X < midPoint ? previousSnapPoint : nextSnapPoint; 862 | 863 | offset = new Vector(nearestSnapPoint - diff.X, offset.Y); 864 | } 865 | 866 | Vector GetAlignmentDiff() 867 | { 868 | var vector = default(Vector); 869 | 870 | switch (VerticalSnapPointsAlignment) 871 | { 872 | case SnapPointsAlignment.Center: 873 | vector += new Vector(0, Viewport.Height / 2); 874 | break; 875 | case SnapPointsAlignment.Far: 876 | vector += new Vector(0, Viewport.Height); 877 | break; 878 | } 879 | 880 | switch (HorizontalSnapPointsAlignment) 881 | { 882 | case SnapPointsAlignment.Center: 883 | vector += new Vector(Viewport.Width / 2, 0); 884 | break; 885 | case SnapPointsAlignment.Far: 886 | vector += new Vector(Viewport.Width, 0); 887 | break; 888 | } 889 | 890 | return vector; 891 | } 892 | 893 | return offset; 894 | } 895 | 896 | private static (double previous, double next) FindNearestSnapPoint(IReadOnlyList snapPoints, double value) 897 | { 898 | var point = snapPoints.BinarySearch(value, Comparer.Default); 899 | 900 | double previousSnapPoint, nextSnapPoint; 901 | 902 | if (point < 0) 903 | { 904 | point = ~point; 905 | 906 | previousSnapPoint = snapPoints[Math.Max(0, point - 1)]; 907 | nextSnapPoint = point >= snapPoints.Count ? snapPoints.Last() : snapPoints[Math.Max(0, point)]; 908 | } 909 | else 910 | { 911 | previousSnapPoint = nextSnapPoint = snapPoints[Math.Max(0, point)]; 912 | } 913 | 914 | return (previousSnapPoint, nextSnapPoint); 915 | } 916 | 917 | private IScrollSnapPointsInfo? GetScrollSnapPointsInfo(object? content) 918 | { 919 | var scrollable = content; 920 | 921 | if (Content is ItemsControl itemsControl) 922 | scrollable = itemsControl.Presenter?.Panel; 923 | 924 | if (Content is ItemsPresenter itemsPresenter) 925 | scrollable = itemsPresenter.Panel; 926 | 927 | var snapPointsInfo = scrollable as IScrollSnapPointsInfo; 928 | 929 | if (snapPointsInfo != _scrollSnapPointsInfo) 930 | { 931 | if (_scrollSnapPointsInfo != null) 932 | { 933 | _scrollSnapPointsInfo.VerticalSnapPointsChanged -= ScrollSnapPointsInfoSnapPointsChanged; 934 | _scrollSnapPointsInfo.HorizontalSnapPointsChanged -= ScrollSnapPointsInfoSnapPointsChanged; 935 | } 936 | 937 | _scrollSnapPointsInfo = snapPointsInfo; 938 | 939 | if (_scrollSnapPointsInfo != null) 940 | { 941 | _scrollSnapPointsInfo.VerticalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged; 942 | _scrollSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged; 943 | } 944 | } 945 | 946 | return snapPointsInfo; 947 | } 948 | 949 | public void ValuesChanged(InteractionTracker sender, InteractionTrackerValuesChangedArgs args) 950 | { 951 | if (args.RequestId != 0 && requestId.HasValue && args.RequestId <= requestId) 952 | return; 953 | try 954 | { 955 | _compositionUpdate = true; 956 | SetCurrentValue(OffsetProperty, new Vector(args.Position.X, args.Position.Y)); 957 | } 958 | finally 959 | { 960 | _compositionUpdate = false; 961 | } 962 | } 963 | 964 | public void IdleStateEntered(InteractionTracker sender, InteractionTrackerIdleStateEnteredArgs args) 965 | { 966 | _inertiaArgs = null; 967 | } 968 | 969 | public void InertiaStateEntered(InteractionTracker sender, InteractionTrackerInertiaStateEnteredArgs args) 970 | { 971 | _inertiaArgs = args; 972 | UpdateScrollModified(); 973 | } 974 | 975 | public void InteractingStateEntered(InteractionTracker sender, InteractionTrackerInteractingStateEnteredArgs args) 976 | { 977 | _inertiaArgs = null; 978 | } 979 | 980 | private void EnsureScrollAnimation() 981 | { 982 | if (_interactionTracker == null) 983 | return; 984 | 985 | if (_scrollAnimation == null) 986 | { 987 | var compositionVisual = ElementComposition.GetElementVisual(this); 988 | 989 | _scrollAnimation = compositionVisual.Compositor.CreateExpressionAnimation(); 990 | _scrollAnimation.Expression = "Vector3(Margin.X, Margin.Y, 0) - Vector3(Tracker.Position.X, Tracker.Position.Y, Tracker.Position.Z)"; 991 | _scrollAnimation.Target = "Offset"; 992 | _scrollAnimation.SetReferenceParameter("Tracker", _interactionTracker); 993 | var margin = Child.Margin + Padding; 994 | _scrollAnimation.SetVector2Parameter("Margin", new System.Numerics.Vector2((float)margin.Left, (float)margin.Top)); 995 | } 996 | } 997 | 998 | private void UpdateScrollAnimation() 999 | { 1000 | if (Child == null) 1001 | return; 1002 | 1003 | var vis = ElementComposition.GetElementVisual(Child); 1004 | if (vis == null) 1005 | return; 1006 | 1007 | EnsureScrollAnimation(); 1008 | if (_scrollAnimation == null) 1009 | return; 1010 | 1011 | vis.StartAnimation("Offset", _scrollAnimation); 1012 | } 1013 | 1014 | private void UpdateInteractionOptions() 1015 | { 1016 | if (_interactionTracker == null) 1017 | return; 1018 | 1019 | var source = _interactionTracker.InteractionSource; 1020 | if (source == null) 1021 | return; 1022 | 1023 | source.CanVerticallyScroll = CanVerticallyScroll; 1024 | source.CanHorizontallyScroll = CanHorizontallyScroll; 1025 | source.IsScrollInertiaEnabled = ScrollViewer.GetIsScrollInertiaEnabled(this); 1026 | source.ScrollFeatures = GetScrollFeatures(this); 1027 | } 1028 | 1029 | public ScrollPropertiesSource GetScrollPropertiesSource() => _scrollPropertiesSource ?? CreateScrollPropertiesSource(); 1030 | 1031 | private ScrollPropertiesSource CreateScrollPropertiesSource() 1032 | { 1033 | if (_scrollPropertiesSource == null && 1034 | CompositionVisual != null && 1035 | _interactionTracker != null) 1036 | { 1037 | _scrollPropertiesSource = ScrollPropertiesSource.Create(this, _interactionTracker); 1038 | } 1039 | 1040 | 1041 | return _scrollPropertiesSource; 1042 | } 1043 | } 1044 | } -------------------------------------------------------------------------------- /CompositionScroll.DesktopExample/VM/TextViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | namespace CompositionScroll.DesktopExample.VM 4 | { 5 | public sealed class TextViewModel : ObservableObject 6 | { 7 | public string ExampleText => 8 | @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer odio mi, pretium a volutpat porttitor, tincidunt et orci. Duis nec arcu eu odio faucibus iaculis eget scelerisque ex. Donec in risus vel dolor lacinia egestas sit amet ut odio. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas fringilla, velit sit amet placerat lacinia, orci tellus dignissim mi, at porta elit justo et velit. Vivamus tristique elit ut orci faucibus euismod. Pellentesque faucibus nunc eu elementum pharetra. In hac habitasse platea dictumst. Fusce lacus mauris, tincidunt vitae arcu quis, aliquam pretium est. Praesent ante orci, elementum id nulla non, molestie iaculis orci. Nam non gravida diam. Curabitur erat sem, feugiat sed ullamcorper vitae, finibus at ipsum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum eros magna, varius ut tristique at, porttitor nec erat. Integer id facilisis orci, non bibendum metus. Donec fermentum lectus eu est viverra, at maximus felis maximus. 9 | 10 | Donec rhoncus tempor lacus sit amet hendrerit. Curabitur tempor, nunc quis laoreet sodales, ex mauris dignissim libero, sit amet ultricies erat ipsum vitae mi. Vestibulum aliquam quam in urna ultricies efficitur. Proin consectetur pellentesque ex ac molestie. Donec id velit pellentesque enim mattis placerat et non ex. Nunc commodo mi eros, nec iaculis sem imperdiet vitae. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed justo leo, tempus vel sem eget, pulvinar dictum nunc. Integer sed felis eget magna mollis luctus sed ac elit. Vestibulum sed finibus velit. Etiam egestas nisi vitae laoreet aliquet. Pellentesque laoreet non felis eu posuere. Donec facilisis mi eu sagittis sagittis. 11 | 12 | Phasellus aliquam neque iaculis lectus porta, maximus suscipit tortor posuere. Donec semper tristique vulputate. Etiam suscipit urna lacus, sed molestie quam finibus in. Mauris maximus non orci eu luctus. Praesent mauris ipsum, ultricies et elit eget, porta accumsan est. Nulla nec feugiat est. Vestibulum vel dolor quis magna tincidunt malesuada id quis nulla. Integer porta turpis arcu, vel fringilla ligula maximus et. Sed ac diam lacinia, hendrerit eros nec, fermentum nibh. Maecenas feugiat aliquam ex ut ornare. Duis a luctus nisi. 13 | 14 | Ut semper in massa at scelerisque. Nam enim leo, dignissim in ante quis, rhoncus tempus odio. Praesent dapibus nulla sem, efficitur placerat lorem porta ut. Donec urna est, faucibus eu feugiat ut, ultrices nec tellus. Aliquam malesuada arcu ac nulla rhoncus venenatis. Vestibulum luctus vitae mauris id porttitor. In hac habitasse platea dictumst. Vestibulum semper dictum felis. Duis ornare augue vitae augue malesuada, non blandit lectus hendrerit. Phasellus sed laoreet metus. Aliquam vehicula, arcu a auctor tincidunt, arcu lacus gravida dui, et luctus lorem nisl vitae mi. Nullam a nibh tristique, ullamcorper felis a, porta arcu. Nam tincidunt hendrerit lobortis. 15 | 16 | Donec id molestie purus. Maecenas pretium lobortis bibendum. Sed tincidunt ex eu urna volutpat, nec pulvinar odio convallis. Duis consectetur, diam id eleifend euismod, quam diam fermentum mauris, at dictum quam purus et lacus. Aliquam dapibus congue pulvinar. Nullam ante lacus, placerat eget maximus ac, tristique eget ex. In ac neque nisi. Pellentesque aliquet lectus id dignissim faucibus. Maecenas faucibus magna id neque ultrices tincidunt. Vivamus ut urna cursus, tristique nisi vel, ornare metus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 17 | 18 | Aenean imperdiet hendrerit nisl sed lobortis. Ut eu urna ac arcu scelerisque aliquet ut nec lectus. Maecenas elit ante, consectetur nec dolor vitae, porttitor maximus mauris. Maecenas mi leo, viverra non imperdiet sit amet, pellentesque nec felis. Aliquam id efficitur lectus. Duis nec consequat urna. Integer mattis rutrum neque, in scelerisque justo pharetra porttitor. Integer eget dictum est. Integer hendrerit ipsum quam, vitae sagittis tellus ultricies at. Nunc ultrices nulla mauris, in faucibus tellus rhoncus vitae. Donec faucibus felis ut sapien porta, vel faucibus magna commodo. Maecenas finibus velit risus, et pharetra orci dapibus ac. 19 | 20 | Aliquam vitae hendrerit erat. Curabitur ut mauris sit amet tellus scelerisque auctor. Nullam vel tincidunt magna. Nulla fermentum volutpat tellus sed viverra. Ut a sem varius, accumsan felis at, pretium risus. Nunc rhoncus ligula sit amet bibendum egestas. In ornare, erat gravida condimentum congue, risus ipsum mattis diam, at rhoncus turpis turpis sed leo. Proin luctus mi sem, a vulputate tortor elementum eu. Morbi sit amet urna ante. Aenean non eros ullamcorper, aliquet tellus eget, facilisis ligula. Pellentesque vel fringilla eros, et ullamcorper ligula. Donec pretium sollicitudin tincidunt. Suspendisse varius aliquam massa, ut euismod ante porta nec. Nulla dignissim nec tellus vel pellentesque. 21 | 22 | Pellentesque tincidunt arcu tortor, euismod luctus nibh cursus sit amet. Nunc commodo tortor eget leo laoreet ultricies. Aenean suscipit lectus in nibh accumsan, sit amet maximus nulla hendrerit. Phasellus sit amet congue sapien. Morbi ac nunc at mi porta dignissim. Suspendisse diam enim, ultricies et nisi eget, consectetur feugiat enim. Donec rutrum commodo suscipit. Nam ut pellentesque mi. Donec neque dolor, congue tempor facilisis non, dictum vel sem. Pellentesque vel lacus sed augue tincidunt blandit eu sodales ante. Vestibulum eget venenatis lorem. 23 | 24 | Nam id vehicula nisl, sed feugiat nunc. Sed ex lectus, malesuada sed tincidunt in, feugiat non orci. Maecenas luctus sollicitudin lectus sed venenatis. Phasellus vitae metus at augue auctor efficitur. Phasellus pulvinar ex pulvinar lorem cursus lacinia. Sed vel aliquam est. Morbi ex lorem, sagittis sed mattis id, accumsan ut nisi. 25 | 26 | Aliquam vulputate mi nec dignissim volutpat. Aliquam non eros quis tellus fermentum tincidunt vitae at felis. Sed ultricies vestibulum risus in varius. In sit amet mi porttitor, tincidunt lacus et, rhoncus diam. Sed pulvinar consectetur enim in eleifend. Fusce laoreet auctor neque, sed elementum erat. Cras ornare, felis auctor volutpat pulvinar, nibh nibh interdum nisl, a cursus dolor libero eget augue. Nunc a hendrerit arcu, vitae dignissim magna. In et nunc ultrices, pulvinar tortor eu, malesuada nibh. Nulla justo est, bibendum sed ante eget, tincidunt ultricies mauris. Duis dapibus ligula posuere erat convallis, nec varius velit venenatis. Aenean ante massa, pulvinar vel velit viverra, rhoncus placerat massa. Morbi interdum mi urna, sit amet venenatis lectus consequat at. 27 | 28 | Ut et ex hendrerit, mollis nunc eu, tempor neque. Donec quis nisl nec mauris mattis iaculis. In risus lectus, suscipit id felis maximus, elementum commodo tortor. Integer laoreet metus eget maximus pretium. Phasellus eget sollicitudin odio, eu mollis nulla. Pellentesque vitae convallis velit, sed hendrerit ligula. Fusce sem massa, ornare sit amet finibus at, eleifend vitae ligula. Phasellus facilisis fringilla turpis, vel fringilla metus dapibus id. Fusce sagittis non felis non gravida. Quisque varius, nisl eu vehicula venenatis, eros mi viverra enim, eget semper lacus nulla in tellus. Sed bibendum felis ultrices elit egestas porta. Praesent euismod, est vitae sagittis consectetur, libero metus ultrices ex, quis dictum libero nisl ut libero. Nulla commodo nec ligula at malesuada. 29 | 30 | Cras sit amet est vestibulum, imperdiet felis ut, hendrerit lorem. Morbi id nunc scelerisque tellus maximus consequat. Donec euismod, arcu ut pellentesque molestie, augue nibh tincidunt nibh, quis vehicula sem dolor at tortor. Donec condimentum pulvinar diam, sed dapibus tortor gravida sed. Nulla facilisi. Etiam ac elementum orci. Etiam vestibulum consequat neque non pulvinar. Curabitur vel tristique lacus, sed mollis nibh. Nulla pharetra elit a scelerisque pretium. Duis fermentum mi ligula, a aliquet diam pellentesque nec. Ut placerat suscipit sem, et tempus dolor tincidunt nec. Quisque fermentum dolor a aliquam ullamcorper. Praesent id tellus eget nunc viverra ultricies. 31 | 32 | Pellentesque ac turpis in diam dapibus dictum. Cras quis sagittis nibh. In hac habitasse platea dictumst. Integer eget odio porttitor, scelerisque justo ut, laoreet sapien. Aenean metus turpis, faucibus maximus eros ac, dapibus iaculis lacus. Nulla semper malesuada hendrerit. Integer at nisi mauris. Vestibulum est urna, facilisis quis ligula a, facilisis suscipit odio. Integer id ex nulla. Quisque ut elit et odio faucibus auctor. Fusce ut mattis nisl, sit amet dapibus turpis. 33 | 34 | Nullam blandit varius nunc et dictum. In dignissim pulvinar semper. Proin tortor felis, mattis id lacus ut, tincidunt vehicula arcu. Sed a cursus mauris, eu ullamcorper ipsum. Nulla vel feugiat neque. Fusce mi velit, convallis non accumsan quis, condimentum nec orci. Proin eu efficitur magna, nec fringilla diam. Aenean eget orci eget elit tristique mollis. Etiam nisi ipsum, gravida sit amet venenatis facilisis, efficitur id quam. Fusce accumsan scelerisque magna, a semper nibh vestibulum at. Duis posuere dolor at felis egestas, quis tincidunt elit bibendum. Curabitur elementum dui turpis, sed ultricies mi fermentum vitae. Nulla a convallis justo. 35 | 36 | Praesent id lacinia diam. Nullam magna leo, sagittis ut nisl at, lobortis interdum est. Vestibulum sed risus nisi. Mauris hendrerit eleifend risus, sed pulvinar lectus iaculis ut. Vivamus in rutrum felis. Sed ultrices egestas ex eget semper. Aenean ut tempus lectus. Donec efficitur dolor urna, ac consequat arcu elementum eu. Nunc non fermentum urna. Cras tellus est, dictum eget lacus id, accumsan feugiat magna. 37 | 38 | Aliquam erat volutpat. Nunc mollis euismod arcu. Vivamus laoreet, purus ut congue lobortis, odio felis sodales justo, sit amet elementum nisl dui quis neque. Sed quis dui porttitor, volutpat felis et, ultrices lorem. Proin at vestibulum nulla, vel pellentesque magna. Curabitur suscipit tincidunt porttitor. Vestibulum malesuada lectus non leo consectetur consequat. Sed pellentesque metus et finibus commodo. Duis congue tellus ut faucibus rhoncus. Mauris id nisi finibus tellus gravida fringilla. Morbi vel ex ut justo eleifend lobortis nec at enim. Nulla ac feugiat urna. Aliquam at lacus magna. Proin nibh leo, pulvinar eget eleifend et, imperdiet id quam. Nullam mollis varius mauris, eget dapibus nunc aliquet sit amet. 39 | 40 | Integer libero augue, accumsan ac suscipit ac, ultricies sit amet enim. Cras efficitur mi vel nisl volutpat, id porttitor tellus consectetur. Sed dapibus, augue vel sagittis sollicitudin, neque enim tristique lacus, ut condimentum sapien urna id nisl. Aenean in egestas tellus. Donec ac odio et magna venenatis aliquam. Morbi ipsum sem, mattis et porttitor vulputate, sagittis a est. Donec et porttitor augue. Fusce vitae malesuada risus. Phasellus sodales justo arcu, hendrerit viverra nibh molestie sit amet. Duis eget metus turpis. Nam vehicula justo vel massa vestibulum congue. Ut hendrerit metus ipsum, vitae rhoncus tortor sollicitudin eget. Cras justo leo, blandit vitae lacus nec, congue pharetra lorem. Etiam in risus et diam semper finibus et scelerisque eros. Etiam condimentum erat sit amet dolor ultricies, eu pharetra justo faucibus. 41 | 42 | Phasellus eget suscipit magna. Sed maximus pharetra augue, lacinia vehicula nibh malesuada ut. Donec mollis justo dolor, eget ullamcorper nisi condimentum eu. Aliquam sagittis ex ut lorem facilisis, vitae dictum tellus facilisis. Ut ac elit metus. Sed dapibus, ligula sit amet consequat pellentesque, urna urna feugiat ipsum, quis rutrum lectus est ut ante. Maecenas pellentesque libero vitae nunc iaculis condimentum. Morbi facilisis nulla turpis, id facilisis urna tincidunt eget. Etiam condimentum, felis et faucibus tempor, diam velit semper erat, et feugiat magna felis id orci. Sed sollicitudin gravida sollicitudin. Donec erat augue, convallis in ornare vitae, tempor et eros. 43 | 44 | Vestibulum rutrum pellentesque neque, vitae mattis nisl ultricies ut. Praesent ornare interdum neque, a consequat nisl porta ut. Mauris molestie augue eu diam porta, in sodales ipsum ultricies. Morbi rutrum nulla eu nulla tincidunt, sed sodales orci rutrum. Pellentesque sed nulla et lacus auctor volutpat. Vivamus eget dui vestibulum, sagittis massa malesuada, commodo urna. Curabitur mattis tempor tristique. Nam consectetur urna ac odio faucibus, in congue sem mollis. Nam consequat placerat augue, quis ultricies enim pulvinar nec. Morbi vestibulum nibh enim, quis sodales lectus imperdiet non. Morbi eget massa sit amet urna iaculis mollis eget vel massa. 45 | 46 | Maecenas auctor tempor congue. Suspendisse facilisis volutpat augue. Nullam metus justo, elementum venenatis tellus eu, tincidunt gravida tortor. Cras rhoncus semper ipsum, quis luctus erat porttitor id. Etiam euismod leo sit amet congue convallis. Maecenas non mi bibendum, rhoncus urna eget, placerat erat. Duis euismod urna at sem consequat gravida eu non turpis. Ut aliquet ligula ac mauris volutpat convallis. Curabitur pretium ligula vitae accumsan tincidunt. Fusce turpis nulla, aliquet quis tortor non, gravida fermentum quam. Curabitur ullamcorper enim nisi, eu imperdiet metus finibus vel. Fusce lectus tortor, auctor sit amet efficitur quis, dictum ultrices neque. Sed elit lacus, egestas eget est sed, dapibus pharetra eros. 47 | 48 | In eleifend fringilla lacus at lobortis. Donec rutrum rhoncus dignissim. Suspendisse consectetur, enim non dapibus bibendum, sem lacus aliquam ipsum, id suscipit est metus ac sem. Sed bibendum neque purus, facilisis pharetra mauris pharetra eget. Proin in tincidunt nunc, at tincidunt eros. Cras nec congue ligula, ut eleifend lacus. Mauris a sapien sit amet nisi euismod lobortis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin mattis vulputate massa ut pulvinar. Nulla luctus id mi sit amet dictum. Morbi molestie scelerisque magna, sed rhoncus lorem vehicula bibendum. Proin arcu sapien, placerat et quam ut, imperdiet fringilla nulla. 49 | 50 | Praesent at dapibus nisl, quis gravida enim. Proin nec varius erat. Pellentesque finibus facilisis facilisis. Aenean vitae ex at erat euismod commodo. Maecenas purus leo, elementum eget ligula eget, consectetur pharetra ipsum. Duis odio ex, volutpat et aliquam sed, interdum id ipsum. Donec sagittis nulla est, nec iaculis urna dictum eget. Fusce efficitur volutpat mattis. Quisque eu dolor a elit dictum tincidunt sit amet eget tortor. 51 | 52 | Quisque eget tortor sit amet diam molestie imperdiet. In venenatis ullamcorper dui, nec feugiat est consequat id. Nunc vel justo nulla. Morbi turpis odio, finibus in justo eget, blandit ornare eros. Aenean sit amet ante vel mi tincidunt ultrices quis a sem. Nunc interdum turpis sed dignissim viverra. Cras tellus justo, elementum nec elit eget, feugiat congue lacus. Quisque eu dictum lectus. Aenean sollicitudin sollicitudin risus, id congue ante lacinia in. 53 | 54 | Vestibulum nulla sapien, luctus eget risus ut, interdum accumsan arcu. Donec rhoncus ligula vitae ante rhoncus, eu ornare neque lacinia. Cras sed nisl elementum, malesuada lacus id, efficitur leo. Morbi a placerat felis. Nunc ornare porta tortor dictum aliquet. Nulla sit amet tellus at quam rhoncus eleifend vel ac dui. Quisque in arcu placerat, malesuada diam nec, pellentesque diam. Sed vestibulum tortor ut nunc tempor vestibulum. Nunc blandit bibendum lorem, sed accumsan nunc posuere semper. Mauris venenatis nisi eget orci porttitor, nec fringilla nibh malesuada. Proin libero nulla, hendrerit et nisi malesuada, vulputate cursus mi. Aliquam ante sapien, lacinia nec auctor eget, dapibus ut lectus. In hac habitasse platea dictumst. Nunc mi augue, fringilla vitae ultricies id, viverra in ipsum. Sed pulvinar sodales diam et facilisis. 55 | 56 | Praesent iaculis eget lorem sed pretium. Nulla ex ex, viverra non neque vel, pulvinar dignissim mi. Fusce efficitur ultricies pulvinar. Vestibulum posuere tempor ex in fringilla. Aenean lobortis eleifend sapien facilisis sollicitudin. Donec sit amet sollicitudin turpis, volutpat venenatis sem. Integer in elit rhoncus urna venenatis sollicitudin auctor non quam. In hac habitasse platea dictumst. Sed tincidunt tristique nisl, id accumsan nibh. Pellentesque lectus nisl, sodales a ex a, faucibus ullamcorper eros. Vestibulum in eros sed erat imperdiet ultricies vel vitae turpis. 57 | 58 | Ut porta est nec ex commodo ornare. Sed eget tortor orci. Vivamus venenatis turpis erat, sed vulputate metus ornare eget. Aliquam et fringilla velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis eget purus quis ipsum feugiat vehicula a a augue. Quisque viverra luctus volutpat. Proin ac risus et nulla viverra posuere. Sed tincidunt quam eu hendrerit fringilla. Mauris convallis leo et ipsum pellentesque, non egestas elit cursus. Sed vitae dolor malesuada, varius mauris maximus, auctor enim. Nam congue malesuada libero in lacinia. Maecenas eget arcu in turpis congue placerat eget in turpis. Maecenas quis leo sit amet erat pellentesque mattis. Cras venenatis, ex vel feugiat gravida, erat dui sollicitudin mauris, nec aliquet erat est quis odio. Nullam in gravida tellus. 59 | 60 | Nulla id finibus velit, id consectetur elit. Proin egestas purus quam, eu porttitor purus lobortis pellentesque. Morbi eu pretium lorem. Vestibulum posuere pellentesque quam in posuere. Nam et nisi ac odio consectetur dignissim. In hac habitasse platea dictumst. Quisque odio velit, hendrerit in ullamcorper sit amet, dictum ut nibh. Praesent elementum arcu est, sed ornare nunc semper sed. Sed volutpat facilisis massa hendrerit luctus. Fusce euismod sapien eu enim mollis, non finibus nulla mattis. Suspendisse tellus sapien, efficitur et rhoncus accumsan, faucibus at diam. Vivamus nec ullamcorper nisl. Sed ligula turpis, blandit eget diam eu, laoreet imperdiet urna. Nam tempus pretium vulputate. 61 | 62 | Fusce vitae mollis ipsum. Vestibulum erat lacus, sollicitudin ac nulla a, viverra interdum sapien. Pellentesque efficitur lectus vitae libero aliquet hendrerit. Praesent lacinia hendrerit mauris non placerat. Interdum et malesuada fames ac ante ipsum primis in faucibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vestibulum gravida, ex ut efficitur placerat, ipsum lectus fermentum neque, non condimentum magna lorem nec ligula. Morbi ullamcorper, enim a pharetra luctus, est velit consequat ante, eget tristique erat elit eget augue. Nunc convallis tempus ipsum, in finibus risus. Cras vel ultricies leo, in accumsan eros. Nam iaculis sed orci eu ullamcorper. Praesent sed leo at orci molestie vehicula. Aenean faucibus felis tellus, quis facilisis urna viverra eu. Proin vitae viverra velit. Duis pharetra, metus at ornare mollis, nisi augue aliquet nisl, finibus euismod ipsum sem ac nunc. 63 | 64 | Duis ultrices, ex ac volutpat feugiat, velit nisi mollis turpis, a eleifend enim mauris eu neque. Sed vel augue neque. Phasellus dictum magna arcu, vel ultricies leo congue quis. Donec vitae nulla id nibh pellentesque mollis sed elementum tortor. Proin varius nisi ut turpis sollicitudin, rutrum egestas nisi pellentesque. Pellentesque ac ex ullamcorper, aliquet ligula a, mollis diam. Curabitur venenatis lacus ac ipsum gravida dapibus a in eros. Aliquam diam dolor, pellentesque a turpis ac, molestie tempus lacus. Nunc sapien mauris, fringilla id elit quis, gravida consequat arcu. Sed malesuada eros tortor, vel mollis risus sollicitudin nec. Ut sem felis, accumsan ut porta sit amet, iaculis id velit. 65 | 66 | Ut condimentum magna a suscipit maximus. In venenatis fermentum nunc in finibus. Aenean hendrerit justo purus, vitae rutrum odio tempus ac. Morbi eleifend molestie erat a faucibus. Etiam quis turpis auctor, scelerisque massa a, aliquet risus. Phasellus sollicitudin non risus at dignissim. Phasellus porta dictum lacinia. Fusce at risus justo. Morbi in quam nec mi porttitor porttitor euismod sed dui. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vestibulum orci ut condimentum hendrerit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nulla in odio sapien. Etiam nec arcu metus. Pellentesque sit amet tristique velit. Integer placerat tellus in ultrices finibus. 67 | 68 | Nulla lacinia felis quis venenatis semper. Proin metus libero, scelerisque vel maximus sit amet, pretium in leo. Fusce vel eros quis elit placerat hendrerit a at sapien. Aliquam accumsan pharetra mi, eu vehicula erat condimentum consectetur. Curabitur a enim nec lacus suscipit facilisis et non magna. Curabitur faucibus arcu a enim finibus, ut consectetur augue fringilla. Curabitur efficitur dolor non libero efficitur feugiat. Aenean gravida scelerisque risus, eget pharetra elit pharetra non. Maecenas rutrum, quam et volutpat pharetra, sapien enim tincidunt turpis, a tincidunt velit velit at orci. Donec vitae porta lectus. Vivamus ullamcorper gravida dapibus. Aliquam tellus eros, ultricies ut lacinia sit amet, bibendum in mi. Pellentesque eget nisl erat. 69 | 70 | Donec fermentum semper arcu et consectetur. In hac habitasse platea dictumst. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Phasellus mi felis, faucibus at pellentesque ut, pharetra porttitor nisi. Vestibulum nunc enim, facilisis a auctor vel, volutpat quis ipsum. Ut laoreet tellus dolor, eu lobortis enim vehicula nec. Vestibulum malesuada quis leo quis lacinia. Sed sollicitudin justo sit amet orci hendrerit fringilla. 71 | 72 | Ut accumsan, ipsum eu eleifend iaculis, turpis arcu efficitur nulla, at sagittis elit lorem eget diam. Fusce tellus sapien, laoreet ut lorem a, fermentum dapibus metus. Donec condimentum vulputate leo, et consectetur lectus blandit nec. Duis nec justo eu augue porttitor scelerisque non vitae enim. Sed non tincidunt neque, ac aliquam dui. Suspendisse potenti. Nunc nec urna at ipsum tincidunt rhoncus. Quisque rutrum efficitur nunc, et eleifend mi fringilla nec. Vestibulum eleifend quam et sodales scelerisque. Duis commodo nulla vel fringilla vestibulum. 73 | 74 | Vivamus varius leo metus, ac placerat nibh dictum ut. Sed faucibus orci ut porta consequat. Donec auctor convallis sagittis. Pellentesque et vulputate sapien. Nulla scelerisque arcu at felis dapibus, a pharetra nisl sodales. Etiam et lorem eros. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras posuere dolor quis arcu volutpat, fermentum euismod elit consectetur. Vivamus in condimentum quam. Donec at sapien nisl. Suspendisse nec convallis tortor, quis mollis ex. Suspendisse pretium tincidunt diam, non condimentum lacus suscipit sit amet. Mauris a imperdiet enim. 75 | 76 | Fusce a finibus lectus. Pellentesque hendrerit urna ligula, ac ultrices felis pellentesque sit amet. Vestibulum semper, purus vel dapibus egestas, lorem justo euismod magna, non porttitor ex enim sodales magna. Pellentesque pretium ullamcorper diam non molestie. In ultricies tellus justo, ut maximus justo viverra nec. Sed feugiat iaculis nunc vitae cursus. Aenean elementum gravida tortor, vitae tincidunt lacus sollicitudin quis. Mauris lacinia viverra dui ullamcorper ornare. Pellentesque quis fringilla justo. Sed commodo mollis neque, quis sodales quam placerat ut. Quisque rhoncus purus tellus, quis laoreet nisi aliquam id. Mauris tempus facilisis interdum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aliquam erat volutpat. Donec non euismod ligula. Ut a velit commodo, sollicitudin metus quis, semper leo. 77 | 78 | Nam felis mi, porttitor id placerat eu, dignissim fermentum massa. Fusce sollicitudin lorem eu dui fringilla, ut hendrerit turpis ornare. Sed mollis, ante non auctor ultricies, ligula urna gravida ex, sed imperdiet elit metus ac nulla. Ut velit urna, finibus eu libero eget, tempor iaculis felis. Ut imperdiet turpis eget enim rhoncus fringilla. Nam faucibus magna id neque molestie posuere. Fusce sed tincidunt risus. Fusce suscipit ante ligula, et consequat arcu suscipit eu. Donec elit ligula, posuere sed laoreet pellentesque, faucibus nec eros. 79 | 80 | Suspendisse maximus justo ac laoreet hendrerit. Donec ut sem ultrices, congue nulla non, facilisis mi. Quisque ultricies ex a mauris iaculis, non efficitur libero blandit. Suspendisse efficitur neque vel diam vehicula sollicitudin. Ut auctor semper pellentesque. Suspendisse est erat, molestie at velit vitae, fermentum aliquam lacus. Nulla feugiat felis lectus, vitae imperdiet ante pretium vel. Vivamus in pretium mi, nec elementum purus. Nullam sed ultricies felis, sed vestibulum libero. Pellentesque sed interdum lectus. Etiam quis magna quis ante elementum suscipit eu nec orci. Duis ultricies hendrerit venenatis. Etiam consequat massa quis eros molestie malesuada. Mauris fringilla erat sed arcu eleifend volutpat. Morbi viverra lorem eget urna finibus, id maximus mauris tristique. Quisque mollis orci augue, a tincidunt dolor venenatis eu. 81 | 82 | Maecenas eget lorem a elit ullamcorper pretium in non justo. Ut sapien erat, commodo non nibh quis, auctor fringilla mi. Nullam rutrum urna quis leo gravida hendrerit. Integer vestibulum mattis diam, vel imperdiet augue pellentesque a. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis sit amet sapien sit amet ante tincidunt lobortis. Proin lectus magna, dapibus sit amet sapien sodales, cursus porta sapien. 83 | 84 | Nullam elit ante, sagittis ac libero vitae, ultrices tristique turpis. Etiam interdum pellentesque lectus, a pulvinar felis congue eu. Phasellus dignissim arcu ac nisl rutrum, at vestibulum nunc congue. Aenean semper nunc nec orci suscipit, et ultrices ex ultrices. Aenean ante dolor, interdum quis auctor sit amet, vulputate et odio. Cras scelerisque velit lacus, in pulvinar leo vulputate et. Fusce non quam in diam tristique convallis. Phasellus interdum ut est id auctor. Sed orci lorem, molestie sit amet massa ut, tincidunt bibendum odio. Mauris ac nunc congue, sagittis velit sit amet, finibus nisl. Morbi luctus molestie facilisis. 85 | 86 | Aliquam auctor scelerisque turpis eu vulputate. In dapibus fringilla sodales. Aenean at dictum leo. Integer eget nulla mi. Phasellus sodales luctus purus et congue. Etiam malesuada tincidunt eros eu feugiat. In hac habitasse platea dictumst. Integer egestas, nulla in euismod ultricies, leo sapien scelerisque tortor, id imperdiet ligula nisi a massa. Praesent metus nibh, gravida vitae eros sit amet, convallis varius nisi. Vestibulum dictum convallis hendrerit. 87 | 88 | Vestibulum pulvinar viverra nisi id consequat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec nec posuere urna. Suspendisse placerat risus ac nisl congue dignissim. Aenean sollicitudin sapien urna, sit amet volutpat quam gravida eu. Sed sagittis felis nunc, ut dignissim dolor rutrum id. Ut ornare dignissim risus. Duis nunc urna, ultricies sed viverra sit amet, elementum at nulla. Vestibulum at tempus justo. Cras faucibus eros sit amet neque lobortis hendrerit. Ut tristique facilisis ipsum, eleifend vulputate turpis vestibulum ac. Nunc facilisis, quam non commodo facilisis, libero est convallis sapien, eget mattis lorem neque mollis eros. 89 | 90 | Sed diam libero, dignissim quis varius eget, interdum eu eros. Proin non dui sit amet mauris semper semper. Nulla quis justo ligula. Duis nec sapien metus. Nullam facilisis nunc quis tincidunt consequat. Donec ligula metus, accumsan nec aliquet sit amet, aliquet id massa. Vestibulum ultrices rutrum velit sed consectetur. Nunc eleifend accumsan tellus, id fringilla neque gravida eget. Maecenas sed tortor orci. Nullam eget efficitur nisl, eu vestibulum mi. Maecenas ipsum orci, efficitur et efficitur in, vestibulum vitae urna. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec at aliquet nulla. Aenean aliquam, magna vitae ultricies luctus, sapien nisi vulputate eros, non convallis urna leo id sem. Sed venenatis congue interdum. Integer congue arcu nec sem mattis vulputate id quis diam. 91 | 92 | Etiam feugiat arcu at tortor aliquam, in hendrerit velit pretium. Morbi dolor dui, ultrices sed suscipit a, gravida at arcu. Nulla sagittis sed nisi aliquet imperdiet. Curabitur malesuada, enim a vulputate aliquam, enim arcu tempor urna, sed sollicitudin purus nisi sit amet odio. Fusce eu posuere eros. Morbi suscipit nec nisi ut vulputate. Ut et luctus tortor. Suspendisse imperdiet neque id odio vehicula, ut aliquet velit sollicitudin. Nulla accumsan lectus ac sapien porttitor tincidunt. Nunc vitae feugiat orci. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Proin a cursus dolor, in pulvinar urna. In pharetra odio magna, ac mattis tellus sodales nec. Aliquam laoreet purus nunc, in scelerisque nunc dignissim nec. 93 | 94 | Aliquam eu pellentesque massa, vitae dictum urna. Duis faucibus efficitur quam nec dictum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sodales ipsum vitae sodales tempus. Etiam lorem ligula, fringilla in ornare nec, suscipit imperdiet lorem. Mauris convallis lectus ornare elit accumsan, at volutpat odio accumsan. Duis sagittis purus molestie lectus dapibus ornare. Phasellus tristique maximus erat, vitae eleifend sapien cursus id. Sed sodales velit eget viverra sollicitudin. Curabitur tincidunt arcu nisl, a maximus nulla congue vitae. Cras ultricies, nunc tincidunt sollicitudin tristique, magna velit tincidunt diam, vulputate ultricies nulla magna ut est. Sed finibus fringilla lectus in laoreet. 95 | 96 | Vivamus a erat vitae ligula ullamcorper pretium. Aliquam vitae eleifend turpis, nec venenatis dui. Integer nulla dui, pharetra egestas vestibulum ac, sagittis sit amet magna. Quisque sed gravida risus, vitae dignissim est. Curabitur vel congue massa. Maecenas vel massa elit. Fusce vitae erat iaculis, malesuada lorem eget, imperdiet justo. Aenean id vehicula turpis. Duis eleifend vestibulum tellus, vel pharetra libero viverra vitae. Maecenas rhoncus nec lacus vel congue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent rhoncus ligula nulla, mattis ultrices dui varius et. Nulla cursus est feugiat, ultrices odio sed, blandit lacus. Sed eros dui, viverra vitae fermentum in, mollis eu libero. 97 | 98 | Quisque gravida mauris ut purus dictum, et vestibulum turpis sagittis. Nulla iaculis laoreet augue, sit amet maximus enim. Quisque id porttitor urna. Nulla ligula eros, sagittis vitae varius nec, placerat et ante. Nullam accumsan eu ligula eget suscipit. Suspendisse vestibulum, dolor quis euismod tempor, turpis nisl iaculis nisl, a semper justo urna ut tellus. Morbi eu ante fringilla, laoreet ex quis, venenatis eros. Vivamus augue elit, tincidunt eget varius eu, luctus a enim. Aliquam lacinia lacus libero, vitae porttitor neque tristique tincidunt. Pellentesque sed sagittis odio, ut porttitor nisl. Fusce quis odio sollicitudin, bibendum sapien at, blandit tortor. 99 | 100 | Nullam gravida, felis sed rhoncus blandit, dolor ex lobortis nulla, nec molestie massa augue sed enim. Phasellus in fermentum elit. Sed accumsan scelerisque tellus, accumsan commodo magna consectetur eu. Nulla vel diam at turpis dictum elementum. Nam tincidunt velit libero, id dignissim odio elementum sit amet. Suspendisse lorem mi, cursus sed laoreet vitae, vestibulum non sapien. Nullam sagittis suscipit diam vel tincidunt. 101 | 102 | Donec pretium mi ac tellus interdum, eu feugiat diam porttitor. Ut suscipit, leo at tempus gravida, orci libero tincidunt lectus, ut tristique arcu nisl vitae tortor. Pellentesque nec nisl viverra, rhoncus mauris et, sodales lacus. Morbi et sem quam. Nam at nisl id risus vulputate tincidunt eu eget lorem. Aenean gravida purus id blandit aliquam. Sed enim turpis, condimentum et iaculis ut, volutpat ut felis. Morbi sapien ex, aliquet hendrerit varius nec, facilisis non ante. Duis tristique facilisis consequat. Morbi quis porta dolor, at mollis nisl. Maecenas in ipsum at nibh elementum consequat. Fusce rutrum ullamcorper varius. Nullam eleifend semper ligula at gravida. Aenean ut augue lobortis, eleifend libero quis, tincidunt neque. Etiam viverra sit amet lorem ut ultricies. Proin tristique dui sit amet dolor tempor, lobortis suscipit odio fermentum. 103 | 104 | Curabitur finibus est eget tellus sodales dignissim. Nunc dapibus arcu in nulla congue, sit amet varius neque dapibus. Donec sit amet lacus suscipit mauris vestibulum ultricies. Aenean posuere orci diam, eu accumsan nunc volutpat at. Nullam viverra lorem et sem vestibulum, ut tempor diam feugiat. Vestibulum blandit ex sit amet lorem pellentesque scelerisque. Duis aliquam vestibulum mauris, sed pretium lacus luctus eget. 105 | 106 | Nunc eleifend sit amet justo vel commodo. Sed cursus tortor eu dolor pulvinar ornare. Etiam lacinia erat consectetur orci auctor, vitae venenatis elit condimentum. Nunc et faucibus lectus. Ut arcu dui, ornare ullamcorper varius quis, suscipit eu lectus. Suspendisse non risus auctor, aliquet velit at, efficitur enim. Cras maximus, odio ut interdum imperdiet, orci elit accumsan enim, ac efficitur ex leo ut orci. Morbi eget aliquet augue. Sed ullamcorper leo ut pulvinar eleifend. 107 | 108 | Integer ut felis nec leo aliquet blandit non commodo mauris. Phasellus tincidunt, felis elementum egestas porta, lacus lorem aliquet enim, sed luctus dui ante eu ante. Phasellus rhoncus elementum nibh, mattis vestibulum lacus vehicula eget. Cras ac turpis a orci finibus fringilla. In lacus risus, ornare sed elit at, tincidunt imperdiet magna. Aenean eget libero felis. Maecenas ante justo, gravida vitae enim quis, scelerisque fermentum tortor. Donec tincidunt mi vestibulum, elementum tortor sed, dictum sem. Morbi sed arcu quis sem elementum aliquet. 109 | 110 | Nunc vulputate pellentesque sem. Duis fringilla facilisis leo in eleifend. Integer vulputate lacus eget pretium iaculis. Morbi elementum, turpis non convallis tincidunt, nunc lectus tristique ligula, id hendrerit nunc dolor at ex. Etiam commodo nisi aliquet ultrices eleifend. Pellentesque sed mi vitae ex fermentum consequat. Integer tempor pharetra elementum. Aenean mattis finibus urna, a cursus massa viverra eget. Donec arcu mauris, feugiat lobortis mi porttitor, malesuada mollis erat. Vivamus feugiat nulla in blandit accumsan. In elit odio, semper nec est in, dignissim interdum dolor. 111 | 112 | Proin ipsum nulla, consectetur sit amet sodales ultrices, tincidunt non nisi. Morbi erat risus, bibendum non sem vel, porta hendrerit mi. Nam odio dui, lobortis et ex vel, mattis lacinia libero. Sed nisi elit, eleifend vel porttitor vel, eleifend non enim. Maecenas non nunc quis nulla elementum venenatis vel eget libero. In congue elit non lacus semper sagittis. Etiam leo sem, sollicitudin nec rutrum quis, blandit id nisi. Vivamus semper felis ex, tristique ullamcorper massa luctus quis. In mauris metus, vestibulum at ligula eget, fermentum sagittis nisi. Aenean erat mauris, tincidunt rhoncus pharetra vitae, dignissim eu magna. 113 | 114 | Ut tempus finibus turpis sed commodo. Donec vel quam lorem. Ut est nibh, ultricies ac neque non, consectetur gravida risus. Nullam efficitur augue eget massa tristique iaculis. Nulla faucibus odio elementum elit scelerisque, id blandit neque congue. Aliquam ut ex ut arcu porttitor ullamcorper sed nec metus. Integer quis nulla egestas, lobortis nisi a, placerat sem. Curabitur sit amet vehicula enim, quis efficitur leo. Praesent auctor neque arcu, non finibus ex tempus vel. Mauris in turpis rhoncus, egestas enim ut, lacinia ipsum. 115 | 116 | Quisque a maximus est, ac scelerisque tortor. Cras volutpat sodales urna sit amet suscipit. Etiam molestie ullamcorper dignissim. Maecenas elementum turpis eu est volutpat, in tincidunt enim posuere. In venenatis nibh non diam varius, quis molestie neque imperdiet. Suspendisse potenti. Maecenas id iaculis dolor. Phasellus fringilla mauris vel fringilla porta. 117 | 118 | Morbi molestie placerat sem sed viverra. Vestibulum ac magna purus. Aliquam odio nibh, fringilla in eleifend a, semper vel orci. Donec tincidunt odio scelerisque lacus sollicitudin sollicitudin et sodales nulla. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Quisque ut finibus justo, vel sagittis arcu. Sed vitae vulputate justo, id commodo lectus. Aenean at urna suscipit, pretium elit sed, efficitur leo. Nulla aliquet, erat in finibus scelerisque, leo lectus feugiat metus, sed eleifend erat orci quis ipsum. Pellentesque ac pulvinar ante. Curabitur eget ullamcorper lacus. Duis sit amet convallis nulla. 119 | 120 | Donec at molestie nibh. Nulla ultrices finibus maximus. Pellentesque sit amet ex at ante sodales consectetur sed vitae diam. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In arcu turpis, imperdiet eu finibus nec, semper eu felis. Aliquam metus nibh, maximus sit amet convallis nec, maximus non ipsum. Nam sit amet tellus sit amet tortor semper volutpat. Maecenas ante magna, ultricies id tortor et, interdum auctor enim. 121 | 122 | Maecenas suscipit rutrum massa vel accumsan. Aenean sem tortor, fermentum at augue at, convallis ullamcorper lacus. Nunc elementum imperdiet justo, vel gravida ante tempor vitae. Maecenas eget pharetra tellus, et pharetra purus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris dignissim felis pellentesque lacus vestibulum accumsan. Vestibulum varius finibus ex, sed elementum tellus vehicula eget. Curabitur commodo sodales massa in imperdiet. 123 | 124 | Nam congue, felis nec fringilla tincidunt, nunc turpis rutrum tellus, sed commodo sapien tortor id nisi. Vivamus quis venenatis arcu, et interdum mi. In rhoncus elit quis finibus rhoncus. Sed euismod, neque at ultrices dignissim, sapien enim ultrices libero, nec condimentum tortor ex at erat. Sed ac metus leo. Pellentesque tincidunt nisl a congue pharetra. Donec ultricies dolor a sem condimentum ultrices. Ut sit amet ligula nec felis viverra tempus. Duis fermentum elementum dui sit amet ultrices. Cras efficitur odio est, sed dignissim dui pretium eget. Sed scelerisque eget metus id aliquet. 125 | 126 | Quisque vitae accumsan sapien, nec dapibus ante. Sed in ante ac quam facilisis scelerisque quis nec risus. Pellentesque cursus, est a vestibulum interdum, augue purus egestas lectus, gravida vulputate erat nibh vel est. Cras ac consequat arcu. Vestibulum in tristique augue, consectetur rhoncus ex. Pellentesque posuere elementum eleifend. Pellentesque in eros luctus, laoreet purus ut, dignissim justo. Duis tempus ante vitae sollicitudin cursus. Ut lacinia dui id volutpat commodo. Aliquam erat volutpat. Maecenas eleifend ipsum metus, quis feugiat odio tincidunt tristique. Praesent non leo a eros interdum condimentum gravida vitae odio. Vestibulum dictum, mi a consequat venenatis, tellus nulla cursus tellus, vel faucibus urna erat ac risus. Nam tincidunt massa vel finibus vehicula. 127 | 128 | Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Ut ac turpis quam. Vivamus dapibus volutpat luctus. Sed vel efficitur sapien. Suspendisse tempor leo sit amet cursus hendrerit. Quisque nulla nulla, elementum a dignissim quis, pulvinar in tortor. In euismod leo sit amet arcu faucibus, mollis gravida mi fermentum. Donec elementum eleifend finibus. Proin risus lacus, scelerisque porta scelerisque finibus, viverra et nisi. Sed interdum neque eu orci ornare molestie. Phasellus consequat gravida lorem. 129 | 130 | Quisque molestie eleifend lacinia. Nunc tempor, enim quis luctus volutpat, justo dui elementum diam, eu pharetra magna risus in libero. Praesent lobortis accumsan ipsum eu molestie. Proin nisi erat, pellentesque non eleifend ac, faucibus in turpis. Sed pharetra interdum dui, vel ornare mauris tristique a. Praesent vehicula, felis ullamcorper ultricies tristique, tortor est feugiat nisi, vitae consequat neque neque vitae lacus. Nunc et nisi dolor. Phasellus molestie id ipsum vel laoreet. Donec auctor purus a dolor auctor, sit amet fermentum sem pulvinar. Etiam ultricies nisl vulputate, auctor nunc eu, viverra risus. Quisque aliquet, urna ac congue ornare, arcu nibh volutpat mi, eu fringilla velit dui sed lacus. Praesent faucibus egestas ex, eget tincidunt velit. Nullam at tortor magna. Fusce sodales placerat pellentesque. Vestibulum eu nisi id risus hendrerit condimentum laoreet a purus. 131 | 132 | Pellentesque vel massa nec ante rhoncus tempus. Vestibulum rhoncus cursus tortor quis sodales. Fusce finibus vel nunc in cursus. Cras pellentesque eget orci eu malesuada. Fusce sed est vitae ipsum mollis facilisis. In tempor porta felis, eget molestie erat ultricies in. In tincidunt tempus turpis, dictum dictum arcu accumsan in. Sed tempus quam eget lacinia tincidunt. Suspendisse pulvinar odio vel varius convallis. Donec vel interdum eros. Suspendisse et vehicula magna, in sagittis turpis. Vestibulum orci urna, efficitur at rhoncus ut, facilisis vitae sem. Donec sed ex egestas orci accumsan ultricies. Nullam et leo lacus. 133 | 134 | Morbi ac mi a odio eleifend placerat. Maecenas sodales nunc porttitor nisl porttitor, non blandit magna vestibulum. Nunc luctus laoreet dolor, a eleifend mi commodo vehicula. Phasellus ipsum leo, dapibus nec purus in, viverra lobortis sem. Morbi vitae dolor quam. Phasellus libero ipsum, ultrices at ligula ut, rutrum interdum nunc. Mauris commodo mauris bibendum, ultricies nulla et, pretium sem. Aliquam ac lectus felis. Donec sed nulla eget lorem euismod efficitur nec id nulla. Maecenas eleifend est eu tempor auctor. Sed ac sem non mauris dictum dictum eget ac elit. Fusce mollis pulvinar porttitor. Nunc auctor sed est ut placerat. Suspendisse molestie nibh non lorem laoreet ullamcorper. Mauris vulputate justo elit, vel imperdiet est fermentum sit amet. 135 | 136 | Aliquam eu sem venenatis, cursus urna quis, euismod mi. Nullam ultrices tristique commodo. Nunc lobortis, est sit amet interdum laoreet, leo erat porta tellus, eget finibus odio tortor quis augue. Vivamus vestibulum urna vitae ligula laoreet faucibus. Quisque sagittis est mauris, et sodales lacus sagittis sit amet. Cras tincidunt purus in lacus iaculis finibus. Fusce vitae nibh rhoncus, accumsan turpis eu, aliquam orci. Nulla finibus risus eu nibh ultricies aliquet. Donec mi enim, sollicitudin ornare dictum et, finibus non neque. 137 | 138 | Suspendisse quis velit id orci sodales fermentum eu ut purus. Integer consectetur feugiat fringilla. Duis tellus est, fringilla sit amet orci vel, fringilla rutrum turpis. Mauris ac sagittis tellus. Morbi augue felis, aliquet sed suscipit sit amet, fringilla id turpis. Mauris placerat mi non magna iaculis tristique. Phasellus congue lorem quis nunc mattis vestibulum eget vel nunc. Nam tristique luctus iaculis. Nunc nec tincidunt leo. 139 | 140 | Vivamus pretium a felis et hendrerit. Nulla et condimentum neque. In at mi luctus, aliquet mi quis, commodo elit. Suspendisse aliquet purus vestibulum sapien fringilla, in blandit augue tempor. Cras fringilla facilisis felis, ut pellentesque arcu vulputate sit amet. In tincidunt tincidunt risus, vitae pharetra tortor vestibulum quis. Nullam in arcu et est vulputate pretium. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. 141 | 142 | Fusce efficitur lectus in placerat iaculis. Aliquam fringilla turpis eu condimentum pulvinar. Proin euismod est vitae tincidunt semper. Pellentesque scelerisque erat a lectus hendrerit tincidunt. Etiam pellentesque ipsum in risus egestas imperdiet. In aliquet leo ac est varius maximus. Fusce blandit tincidunt bibendum. Phasellus sodales sagittis tincidunt. Etiam luctus pellentesque magna, sed luctus tortor vulputate ut. Vivamus mattis et purus ac fermentum. Quisque et sapien efficitur, semper elit id, tristique risus. Praesent aliquet facilisis luctus. 143 | 144 | Proin dictum dolor et lorem sollicitudin, eget sodales tortor viverra. Cras gravida, urna a pretium aliquam, elit ex interdum erat, in condimentum nisi lectus et velit. Suspendisse hendrerit aliquet urna a rhoncus. Proin nisl est, tempus vitae hendrerit et, porta vitae sapien. Duis luctus interdum mi id rhoncus. Curabitur rutrum augue dapibus aliquet vestibulum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam tempus nunc id turpis pretium mollis. Phasellus quis turpis posuere, tincidunt tortor ut, fermentum diam. Morbi purus odio, efficitur ut elit non, facilisis auctor erat. 145 | 146 | In sed turpis sit amet mauris condimentum tincidunt. Praesent nec accumsan sem, vel laoreet lacus. Cras ac porttitor ligula. Vivamus nec dui turpis. Nulla ligula sem, vehicula at tristique sed, imperdiet a velit. Morbi rhoncus, quam posuere convallis scelerisque, dui ex tempus urna, ut egestas neque libero nec nunc. Donec venenatis massa eget justo efficitur, eget rutrum turpis varius. Sed efficitur ligula arcu, in auctor arcu sodales eget. Maecenas laoreet, dolor ut rutrum cursus, nibh neque ullamcorper elit, et vestibulum nisl ante et nisl. Maecenas at consequat justo, sit amet fermentum quam. Ut tristique ipsum at pulvinar iaculis. Nullam at venenatis purus. Vestibulum lacus leo, malesuada sit amet dapibus eu, viverra quis diam. 147 | 148 | Suspendisse potenti. Nunc vestibulum semper urna, ut fringilla urna congue et. Duis id erat maximus, pellentesque libero id, eleifend eros. Aenean dignissim aliquam dui, ac vulputate tortor sagittis eget. Aenean laoreet id augue non pharetra. Aenean sed bibendum orci. Mauris neque lectus, bibendum quis placerat ac, dapibus vel mi. Phasellus ut velit at libero suscipit lacinia. Donec congue odio mauris, eget ullamcorper urna scelerisque bibendum. Etiam viverra enim velit, et blandit nibh suscipit at. Nulla luctus tellus ut convallis laoreet. In hac habitasse platea dictumst. Aenean vulputate id nunc eget laoreet. Suspendisse non orci felis. 149 | 150 | Sed eu tortor in eros rhoncus viverra nec vitae turpis. Aliquam ac nisi id dolor euismod consequat non in lorem. Cras tellus tellus, posuere in sagittis vitae, placerat id quam. Nam tempor finibus metus non tincidunt. Vestibulum ultrices, lacus ac facilisis fermentum, nisi arcu feugiat mauris, vel auctor quam mi quis massa. Etiam non turpis malesuada, sollicitudin tortor sed, elementum orci. Ut mattis at nulla sit amet pharetra. Aliquam cursus non massa quis bibendum. Praesent tincidunt nunc id felis sollicitudin, non interdum nisl malesuada. Aenean commodo tortor non tellus maximus, vel dapibus nulla tincidunt. Quisque maximus, turpis lobortis placerat accumsan, ante metus ornare sem, in ullamcorper sapien felis vel ipsum. Vivamus pulvinar neque id felis lacinia, eget auctor enim rhoncus. 151 | 152 | Sed id erat maximus, mollis ante et, interdum odio. Duis ut turpis sit amet risus efficitur consequat. Duis luctus, ante id congue posuere, magna est laoreet libero, convallis dictum velit neque sit amet tortor. Maecenas sed semper magna. Mauris placerat hendrerit nibh, eget dictum nisi tincidunt sed. Fusce hendrerit rutrum turpis ut accumsan. Ut laoreet pharetra egestas. Fusce ullamcorper mi ut sodales ullamcorper. Etiam nec turpis nunc. Donec quis urna sit amet justo consequat sodales. Maecenas bibendum facilisis massa ac porta. Proin vitae dapibus orci. In eget diam imperdiet nulla finibus tempor. Duis diam sem, volutpat vitae condimentum id, iaculis eu sem. Sed blandit elit purus, sit amet fringilla velit fringilla eget. In hac habitasse platea dictumst. 153 | 154 | Sed sit amet elementum velit. In sapien sapien, sodales at facilisis sit amet, posuere at libero. Aenean pellentesque pretium orci, nec accumsan nisl. Aliquam magna sapien, vestibulum in nunc a, commodo malesuada orci. Pellentesque dui nulla, volutpat sit amet consectetur vel, commodo at arcu. Cras a sem urna. In hac habitasse platea dictumst. Etiam vel neque eu ipsum dictum fermentum. Nulla facilisi. Praesent arcu ligula, luctus ut enim vitae, finibus gravida nulla. Interdum et malesuada fames ac ante ipsum primis in faucibus. Vivamus dictum tellus in diam maximus fringilla eget nec magna. Suspendisse blandit fermentum ipsum sollicitudin cursus. 155 | 156 | Vivamus nec ultrices ligula, lacinia cursus eros. Nullam a dui hendrerit, condimentum eros quis, hendrerit magna. Pellentesque vulputate leo ac ex lobortis, non maximus ante faucibus. Quisque semper ex vestibulum orci posuere, porttitor finibus orci scelerisque. Curabitur scelerisque nibh eu faucibus dignissim. Etiam venenatis mi ipsum, et blandit neque volutpat in. Duis vitae urna congue, varius justo in, convallis quam. In gravida et est quis interdum. Sed blandit bibendum mi. Nulla facilisi. 157 | 158 | Praesent gravida convallis nisl sed tempus. Mauris id viverra sem, eget interdum lorem. Integer sollicitudin tincidunt justo eget volutpat. Nunc euismod eu ex sit amet fermentum. Maecenas nulla ipsum, euismod ut ante in, pulvinar laoreet ipsum. Vestibulum rhoncus ornare ex nec lobortis. Aliquam molestie malesuada arcu, vitae tincidunt enim lacinia vel. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam tincidunt tristique quam. Praesent bibendum ultricies quam. Aliquam ut hendrerit ante, tempus dapibus lorem. Proin gravida odio lorem. Nullam nisi ipsum, fermentum fermentum ultrices blandit, scelerisque sit amet nunc. 159 | 160 | Integer porttitor purus ut lorem euismod scelerisque. Integer scelerisque accumsan libero, quis sollicitudin ipsum. Duis non ultricies odio. Nam egestas ornare ornare. Aenean erat justo, venenatis non enim tempor, varius ultricies quam. Nunc tempus mollis consectetur. Nam finibus arcu ut justo mattis, ut feugiat urna blandit. Nam aliquam mi purus. Sed tempus ornare velit, a fermentum sapien sagittis ac. Aenean laoreet sit amet magna nec cursus. 161 | 162 | Etiam sodales, metus ac lobortis vehicula, diam justo vulputate odio, nec ullamcorper velit mi nec libero. Aenean finibus cursus congue. Praesent sed tempor est. Aenean non arcu posuere, congue sem et, accumsan massa. Duis ac lacus gravida, tristique enim non, ultricies urna. Fusce sodales vitae augue nec facilisis. Vestibulum sem lectus, viverra vulputate congue at, blandit eu velit. Nam et tellus sit amet nibh tincidunt consequat. Sed cursus auctor eleifend. Nullam urna est, aliquet at semper id, pharetra a lacus. Donec vulputate cursus augue a feugiat. Curabitur sed vehicula sapien, sed condimentum odio. Aenean pellentesque sem dolor, a porttitor magna tristique sit amet. In ullamcorper consectetur mauris. Nulla quis ipsum neque. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. 163 | 164 | Fusce vitae tempus dui. Vestibulum mollis commodo eros. Nunc consequat urna in tortor ultricies ullamcorper. Nam quis convallis dui. Aliquam varius et nibh at vehicula. Mauris dignissim elit enim, sed vehicula metus volutpat et. Vestibulum non imperdiet odio, mollis ultrices lectus. Sed in turpis eu ligula blandit viverra eu et metus. Nullam non ante velit. Duis dignissim efficitur semper. In et consectetur neque, vel vulputate erat. Sed a consectetur lacus. Vestibulum faucibus sagittis sapien vel semper. Aliquam viverra dictum placerat. Morbi nunc eros, rhoncus vel odio ac, varius porta elit. Mauris vel nunc vitae orci aliquet vehicula non non ligula. 165 | 166 | Donec auctor urna ut faucibus semper. Phasellus posuere luctus est non feugiat. Vestibulum finibus, quam vel malesuada tincidunt, lorem justo ultricies velit, ac laoreet magna enim sed urna. Quisque a maximus libero, id semper tortor. Integer luctus dignissim venenatis. Mauris aliquam dictum tristique. Suspendisse condimentum neque at diam vulputate mollis. Integer fringilla nunc sit amet magna pulvinar tristique. Cras varius risus nisl, ac ullamcorper urna efficitur ut. 167 | 168 | Aenean luctus nec urna vel tincidunt. Duis at varius arcu. Nam sed malesuada augue. Etiam sagittis finibus facilisis. Pellentesque mollis eget nisi ac vulputate. Donec blandit ipsum diam, sed iaculis massa blandit ut. Curabitur quis sagittis sapien, sit amet vulputate neque. Sed rutrum, urna ac tempor suscipit, magna ante elementum enim, vel fermentum tortor felis et odio. Praesent eget urna sed velit malesuada semper quis ut nunc. Mauris pellentesque in ipsum ut tempus. 169 | 170 | Quisque ut ipsum vel elit malesuada pellentesque. Integer nec ligula varius, dictum nunc eu, eleifend est. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla purus lacus, vestibulum sed mattis in, ultricies et nisl. Nunc rhoncus tortor est, egestas tempus turpis suscipit nec. Ut blandit maximus enim, nec commodo enim semper vitae. Pellentesque quis cursus risus. 171 | 172 | Donec et leo mauris. Sed consectetur malesuada ex, eget tincidunt odio facilisis vel. Mauris vestibulum nisi at mi lacinia, nec mattis quam fringilla. Suspendisse facilisis tempus sem, vitae tristique arcu consequat nec. Vestibulum commodo consequat urna eu finibus. Aenean aliquet rhoncus libero, id volutpat lectus finibus a. Nulla facilisi. Aliquam in mi eu urna finibus luctus a ut augue. Pellentesque leo enim, accumsan at scelerisque sed, iaculis id diam. Cras elementum nisi nec orci euismod feugiat ut eget nibh. Aenean fringilla nulla a neque pellentesque tincidunt. Nullam ut dui eu eros eleifend efficitur et eu eros. Sed sagittis urna sit amet quam suscipit fermentum. 173 | 174 | Nulla bibendum nunc sapien. Nam mattis nulla ipsum, nec ultrices justo sagittis nec. Curabitur facilisis auctor massa. Quisque vitae ante id justo ornare volutpat non et justo. Vivamus dapibus sem a suscipit tempus. Pellentesque tincidunt dolor a ligula blandit dictum sit amet quis justo. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras tristique dictum ultrices. 175 | 176 | Vivamus elementum ipsum sed feugiat tempus. Aliquam imperdiet, purus eget commodo fermentum, leo nisl faucibus orci, ac maximus magna leo a augue. Curabitur sit amet ornare ligula. Donec vitae faucibus dui. Praesent lobortis leo vitae massa egestas, quis tempus tellus hendrerit. Sed scelerisque pellentesque ullamcorper. Sed dapibus lorem nec ante aliquam aliquet. Aliquam erat volutpat. Nulla auctor turpis a mauris pulvinar condimentum. Proin urna augue, interdum ut elit sed, sodales porta sem. Etiam ut sem blandit est feugiat molestie. Pellentesque varius dignissim commodo. Aenean euismod luctus tempor. Curabitur bibendum odio sem, at tincidunt elit ullamcorper eget. 177 | 178 | Curabitur consectetur porttitor justo, ut pulvinar sapien bibendum in. Sed euismod convallis elit vel hendrerit. Phasellus quis volutpat lacus. Pellentesque commodo vestibulum ligula, sit amet facilisis nunc iaculis vitae. Ut eleifend gravida mi. Quisque consectetur vehicula fermentum. Integer metus sapien, finibus sit amet dolor nec, suscipit feugiat nulla. 179 | 180 | Pellentesque vitae mollis leo. Nunc eleifend erat eu velit euismod posuere. Proin dignissim nec est ornare euismod. Praesent lobortis consectetur velit quis hendrerit. Pellentesque viverra tempor arcu vitae accumsan. Integer tellus felis, fermentum vel velit non, dictum cursus neque. Fusce tempor sem vitae ligula maximus vulputate. 181 | 182 | Proin bibendum justo non risus vestibulum, a vulputate tortor euismod. In venenatis augue a est sollicitudin, sed congue justo dapibus. Nullam faucibus commodo nulla, vel interdum enim cursus eget. Curabitur diam est, faucibus eu mattis a, lacinia eget elit. Nunc sit amet justo ultrices, congue metus eget, bibendum mauris. Proin semper bibendum lorem. Integer congue pulvinar nisl ut rhoncus. Vivamus tristique tempor venenatis. Maecenas auctor arcu quam, quis pharetra nisi viverra ac. Nullam ac consectetur leo. Morbi ullamcorper odio vitae lorem tristique, at mattis lorem tristique. Nulla non magna dapibus, mattis nibh eget, tincidunt ante. 183 | 184 | Suspendisse non augue ornare, hendrerit nulla in, facilisis ex. Etiam augue neque, sagittis et egestas eget, venenatis imperdiet orci. Ut tempus cursus magna blandit egestas. Nullam efficitur ipsum sed mi elementum posuere. Vivamus cursus ultrices elit nec tempor. Fusce vel laoreet sapien, sit amet placerat justo. Vivamus lacinia, lectus sed mattis semper, dolor leo egestas ante, et varius diam quam quis ligula. Mauris id turpis imperdiet, vulputate elit egestas, suscipit dolor. Aliquam semper posuere elit facilisis sagittis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Cras facilisis ex a sem placerat, id tincidunt tellus aliquet. Curabitur eu urna felis. Fusce pulvinar risus eu ipsum sodales, et facilisis dolor sollicitudin. 185 | 186 | Sed sit amet facilisis nisi. Nulla dignissim vitae libero sed fringilla. Ut non turpis orci. Quisque et mauris sed lorem aliquet vestibulum. Curabitur varius volutpat orci in porttitor. Donec sit amet magna ipsum. Donec tincidunt pharetra arcu in feugiat. Aliquam imperdiet tempus semper. Pellentesque id pharetra urna, non lacinia tortor. 187 | 188 | Vivamus interdum libero sed rutrum semper. Sed eget nisi rhoncus, malesuada nisl id, molestie tortor. Mauris malesuada orci ex, vitae feugiat mi ornare in. Curabitur volutpat rutrum velit, ac malesuada lectus aliquam ut. Duis eu accumsan diam. Vivamus vel pellentesque sapien. Aenean quis libero non odio cursus mattis. Etiam eget nulla blandit, aliquam erat sit amet, sagittis nibh. 189 | 190 | Sed id dui augue. Mauris leo nisi, dapibus nec porta eget, tempor eu magna. Nam bibendum ac dui sed finibus. Quisque accumsan commodo arcu ut suscipit. Suspendisse non erat id nulla fringilla pulvinar. Aenean hendrerit dui eget elementum rutrum. Vivamus feugiat at dolor nec euismod. Maecenas feugiat urna sem, nec dignissim nisi vulputate eget. Mauris quis ligula lorem. Nullam nec tortor vitae massa feugiat interdum. Sed luctus feugiat ligula, nec condimentum odio fringilla ut. Morbi viverra ac nunc quis finibus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. 191 | 192 | Praesent sodales id nunc non placerat. Donec a tempor mauris, suscipit congue dui. Ut vitae lorem non libero bibendum porta elementum ac arcu. Vivamus nec diam pulvinar, gravida felis eu, molestie enim. Pellentesque elementum orci ac dapibus sollicitudin. Praesent vitae fermentum tortor, vel sagittis nunc. Maecenas nisi neque, semper condimentum massa eget, sodales iaculis nulla. Phasellus molestie ut tortor vel aliquet. Sed eget massa scelerisque justo faucibus malesuada. Curabitur quis lorem vulputate, luctus augue ac, laoreet quam. Cras volutpat sem eu auctor sodales. Donec vitae velit ut lectus consectetur finibus sed tempor metus. Pellentesque pulvinar vestibulum tempor. Maecenas bibendum at orci ac maximus. 193 | 194 | Ut consectetur vel eros eget finibus. Suspendisse euismod purus a augue varius, eget fringilla enim posuere. Nam lacinia rutrum risus, non bibendum quam lacinia eget. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec faucibus diam lacus, at ullamcorper enim congue a. Aliquam sit amet nisl erat. Duis facilisis enim vitae mattis ullamcorper. Maecenas massa nisi, feugiat vel vulputate non, tempor ac libero. Donec ultrices malesuada risus, quis sodales ex mattis sit amet. Sed sit amet enim quis mi auctor malesuada. Phasellus tristique mauris suscipit nulla accumsan, ut malesuada tellus commodo. Vivamus efficitur et neque ac congue. Vestibulum gravida placerat diam, non euismod odio tristique sit amet. 195 | 196 | Quisque vel dolor eu mauris bibendum ornare ac tincidunt leo. Nam ut neque nec nisi consectetur fermentum. Donec id ex a ligula tincidunt sodales. Quisque sollicitudin consectetur volutpat. Phasellus imperdiet lobortis libero, vitae vestibulum metus suscipit eget. Aliquam sit amet sodales urna. Nulla vel metus massa. Cras id mattis magna, et molestie eros. 197 | 198 | Nunc id quam et velit porttitor tincidunt eget ac orci. Nam nec gravida nulla. Cras euismod ac sapien sit amet tristique. Praesent consectetur porttitor erat, eget tempus quam. Mauris bibendum iaculis metus. Duis blandit euismod egestas. Donec porttitor purus leo, non volutpat sem malesuada quis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Phasellus sit amet dolor quis lectus finibus interdum nec sit amet elit. Duis dapibus felis non condimentum vehicula. Fusce dui sapien, placerat at tortor tincidunt, ullamcorper congue ipsum. In eu varius lorem. Morbi purus lectus, consequat sed varius non, aliquet ut risus. Suspendisse semper ac elit at egestas. Quisque sollicitudin nisi a lorem mattis lobortis. 199 | 200 | Etiam ante nulla, facilisis eget rutrum nec, tempor vitae metus. Phasellus imperdiet nisl interdum purus tempor viverra. Nulla id imperdiet purus, ac efficitur lacus. Sed feugiat turpis nec dui feugiat facilisis. Nunc convallis est eu auctor fermentum. Maecenas lobortis porttitor sagittis. Morbi id lorem vitae leo hendrerit pharetra. Vestibulum ex leo, interdum eget commodo sed, dignissim vel justo. Curabitur tempor turpis in diam pharetra, eget interdum magna sodales. Morbi ut lobortis magna. Phasellus consectetur elementum purus commodo gravida. Maecenas urna lorem, tincidunt at lectus id, commodo lacinia est. Praesent porta non nunc ut pharetra. Duis dignissim accumsan risus. Duis eget ipsum augue. 201 | 202 | Phasellus dui neque, venenatis vel vestibulum id, sodales at leo. Pellentesque justo dui, lobortis sit amet lorem a, ullamcorper accumsan nunc. Donec nisi urna, sagittis vel sodales non, ultrices at arcu. Vivamus tellus massa, ultrices et ex quis, scelerisque pretium nibh. Aliquam accumsan tortor quis malesuada lobortis. Vestibulum et lorem sed mi vehicula malesuada. Sed id lorem a odio hendrerit sodales. Morbi ultrices ligula nec est pharetra, ac luctus enim sodales. Phasellus ac dignissim eros, eu auctor ante. 203 | 204 | Donec accumsan mi tincidunt, varius velit eu, rutrum risus. Vivamus aliquam rutrum augue in lobortis. In tellus lorem, consectetur nec pellentesque sed, malesuada eu sem. Sed sed ornare ante. Nullam sed maximus dui. Maecenas a sollicitudin nunc, sed tristique purus. Praesent nec odio pulvinar, porttitor quam at, blandit tellus. Vivamus mi velit, laoreet sed erat ut, rhoncus aliquam tortor. Morbi quis tortor et sapien blandit pulvinar quis fermentum justo. Suspendisse a sem et augue dapibus ultrices. Proin ac eros id felis faucibus dapibus at sit amet dolor. 205 | 206 | Cras fringilla quis nisi ac interdum. Suspendisse lobortis velit hendrerit odio pellentesque luctus. Nam mollis lorem dolor, eu luctus sapien finibus nec. Aenean commodo pellentesque urna, vitae semper ante placerat sit amet. Cras at aliquam justo. Praesent vulputate elit at hendrerit blandit. Nullam scelerisque sapien vitae augue imperdiet pulvinar et id neque. Fusce elementum neque et nisl egestas rhoncus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam iaculis nunc et cursus malesuada. Aenean a blandit dolor, nec lobortis purus. 207 | 208 | Nunc a justo dui. Pellentesque id elementum elit, vitae auctor turpis. In hac habitasse platea dictumst. Fusce justo lectus, fringilla ut placerat bibendum, posuere id est. Donec sit amet interdum felis, et tempor turpis. Pellentesque sit amet laoreet ante. Nullam accumsan mollis urna. Nullam lacinia egestas velit quis tempor. Maecenas iaculis quam eu ullamcorper pellentesque. 209 | 210 | Donec aliquam malesuada dolor. Cras lacus orci, gravida a dapibus non, pretium vitae sapien. Phasellus mattis sit amet mi sed mollis. Donec facilisis urna quis justo varius, ac lacinia nisl faucibus. In ultricies felis nec magna feugiat bibendum. Pellentesque pretium arcu sed dui aliquet ultricies. Vestibulum ultricies, tortor eget eleifend rhoncus, nibh eros venenatis lorem, sed faucibus leo velit ut diam. Nullam id tortor dictum, lacinia dolor vitae, lobortis dui. Curabitur tristique, ante ut pharetra suscipit, nulla ligula fermentum augue, cursus imperdiet mauris magna id mi. Maecenas mauris risus, dictum ac egestas eget, tincidunt sed tortor. Vivamus rhoncus mi eu nisi maximus varius. Proin at hendrerit purus, posuere faucibus mi. Cras elementum hendrerit est eu rutrum. Sed est magna, volutpat at dui tempor, venenatis vestibulum nisi. 211 | 212 | Etiam ex sem, ornare vitae nibh vel, fermentum dictum diam. Donec ultrices felis eget nisl tristique posuere. Duis varius, nibh non convallis pretium, velit lacus maximus magna, at semper dolor eros eu massa. Nulla vulputate mi tristique, aliquet justo vitae, gravida nulla. Fusce vel nibh nec enim faucibus faucibus. Sed fermentum magna eu sem hendrerit efficitur. Nulla consectetur lorem ex, non vehicula quam cursus at. Vivamus id sapien in tellus ultrices iaculis in nec dui. 213 | 214 | Donec in vehicula risus. Maecenas dui nisi, blandit at scelerisque ut, congue eget justo. Praesent eu vehicula dui. Nulla aliquam finibus efficitur. Maecenas facilisis sed dui sed dapibus. Ut feugiat quam tristique ultrices ullamcorper. Ut dictum, turpis nec blandit imperdiet, neque eros fringilla mi, vitae tempus felis tellus eu orci. Nunc consequat turpis ut venenatis bibendum. Vestibulum non ante ut nulla venenatis condimentum quis eget sapien. Vestibulum faucibus sed nulla ac dignissim. Maecenas nec placerat nibh. Fusce vel enim massa. Mauris ultrices lorem eget tristique tincidunt. In nec diam mollis, scelerisque ante et, molestie nulla. 215 | 216 | Maecenas id suscipit orci. Fusce malesuada semper turpis, ac consectetur nulla maximus ornare. Curabitur tempor ligula et ipsum feugiat, id euismod nibh molestie. Morbi convallis ullamcorper dui a malesuada. Fusce pretium est eu neque volutpat luctus a at est. Proin euismod eros lobortis sapien venenatis, vel consectetur justo suscipit. Duis magna ex, aliquet ullamcorper eros molestie, malesuada feugiat mauris. Vestibulum sed dolor finibus ipsum sagittis egestas. Vestibulum quis euismod quam, sed fermentum ante. Mauris consectetur id quam ac porttitor. Maecenas sed sodales justo. Donec semper efficitur nibh, id suscipit risus congue ut. Phasellus suscipit ante enim. 217 | 218 | In condimentum, enim sit amet porta placerat, orci lacus aliquam nulla, ut convallis ante urna eget turpis. Donec pharetra est rutrum nisi consectetur aliquam. Phasellus elementum mi ut pulvinar pulvinar. Quisque id bibendum ligula, ut aliquet tortor. Aenean condimentum egestas urna id tempus. In commodo, lacus eget vestibulum consequat, libero libero fringilla turpis, sit amet auctor libero sem sed sem. Aliquam mollis ipsum vel augue tristique, nec lacinia tellus mattis. Maecenas blandit, augue id vestibulum tempus, ante nisl sodales mauris, vel maximus lectus nisl non neque. Nunc id tortor sed augue mollis tempus. Cras auctor velit eu lacus ullamcorper, id congue urna luctus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur condimentum quam at tristique volutpat. Vestibulum pharetra quam arcu, ac iaculis felis hendrerit nec. Pellentesque ut blandit metus. 219 | 220 | Nam cursus ipsum nibh. Duis ligula turpis, efficitur a eleifend pellentesque, pellentesque non quam. In luctus congue ligula at dictum. Donec eget pretium risus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nam posuere est facilisis dictum pharetra. Aliquam eget odio nulla. Morbi lacinia maximus leo imperdiet tincidunt. Pellentesque ornare vulputate eros, vitae rutrum ipsum viverra a. Aenean neque mi, accumsan a lectus non, aliquam venenatis odio. Sed consectetur urna sapien, a vestibulum neque fringilla at. Pellentesque placerat neque a metus fringilla vehicula. Nullam ex tellus, sollicitudin id sollicitudin at, bibendum et dolor. Duis ac condimentum ipsum. 221 | 222 | Donec a tellus erat. Praesent at elit quis lorem cursus condimentum vitae id arcu. In imperdiet, magna ac hendrerit aliquet, nisl risus venenatis nunc, nec semper ex urna ut lorem. Suspendisse commodo leo eros, vel iaculis magna laoreet quis. Nunc iaculis euismod elit et ornare. Pellentesque fermentum mauris aliquam, maximus neque eget, imperdiet risus. Quisque lectus sapien, volutpat at elit in, pretium vestibulum lorem. Sed ut nisi id erat porttitor fermentum. In dapibus orci urna, nec sollicitudin metus viverra vitae. Curabitur neque eros, accumsan non dapibus quis, tristique at magna. Ut egestas dolor sapien, quis mollis sapien feugiat a. Cras tincidunt lectus sit amet hendrerit commodo. Proin sagittis eros at ipsum porta, eu iaculis leo facilisis. Vivamus commodo scelerisque ipsum, et elementum risus maximus non. 223 | 224 | Maecenas sed dui arcu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nulla quam ex, condimentum et vehicula in, mattis."; 225 | } 226 | } --------------------------------------------------------------------------------