From 268b765e0d4526ac73598e3fef6c3356ad58bcb2 Mon Sep 17 00:00:00 2001 From: Jonathan Arweck Date: Tue, 6 Aug 2024 19:48:46 +0200 Subject: [PATCH 01/10] [wip] implement SkiaSharp PlotViews --- .../Avalonia/ExampleBrowser/App.axaml | 1 + .../Avalonia/ExampleBrowser/Category.cs | 27 + .../ExampleBrowser/ExampleBrowser.csproj | 1 + .../Avalonia/ExampleBrowser/MainViewModel.cs | 93 +-- .../Avalonia/ExampleBrowser/MainWindow.axaml | 22 +- .../ExampleBrowser/NotNullBooleanConverter.cs | 20 + .../Avalonia/ExampleBrowser/Program.cs | 8 +- .../Avalonia/ExampleBrowser/Renderer.cs | 9 + .../Converters/OxyColorConverter.cs | 0 .../Converters/ThicknessConverter.cs | 0 .../Extensions/ConverterExtensions.cs | 16 + .../Extensions/DataPointExtension.cs | 0 .../MoreColors.cs | 0 .../OxyPlot.Avalonia.Shared.projitems | 27 + .../OxyPlot.Avalonia.Shared.shproj | 13 + .../PlotBase.Events.cs | 54 +- .../OxyPlot.Avalonia.Shared/PlotBase.Model.cs | 127 ++++ .../PlotBase.Properties.cs | 81 +-- Source/OxyPlot.Avalonia.Shared/PlotBase.cs | 250 ++++++++ .../Tracker/TrackerControl.cs | 0 .../Tracker/TrackerDefinition.cs | 0 .../Utilities/ConverterExtensions.cs | 0 Source/OxyPlot.Avalonia.sln | 27 +- .../OxyPlot.Avalonia/OxyPlot.Avalonia.csproj | 2 + Source/OxyPlot.Avalonia/Plot.cs | 186 +++--- Source/OxyPlot.Avalonia/PlotBase.cs | 565 ------------------ ...{PlotBase.Export.cs => PlotView.Export.cs} | 16 +- Source/OxyPlot.Avalonia/PlotView.cs | 193 +++--- Source/OxyPlot.Avalonia/Themes/Default.axaml | 1 + .../DoubleBuffered/PlotRenderer.cs | 225 +++++++ .../DoubleBuffered/PlotView.cs | 28 + .../OxyPlot.SkiaSharp.Avalonia.csproj | 44 ++ .../OxyPlotModule.cs | 14 + .../PlotRenderer.cs | 57 ++ Source/OxyPlot.SkiaSharp.Avalonia/PlotView.cs | 34 ++ .../SkiaDrawOperation.cs | 57 ++ .../Themes/Default.axaml | 108 ++++ 37 files changed, 1369 insertions(+), 937 deletions(-) create mode 100644 Source/Examples/Avalonia/ExampleBrowser/Category.cs create mode 100644 Source/Examples/Avalonia/ExampleBrowser/NotNullBooleanConverter.cs create mode 100644 Source/Examples/Avalonia/ExampleBrowser/Renderer.cs rename Source/{OxyPlot.Avalonia => OxyPlot.Avalonia.Shared}/Converters/OxyColorConverter.cs (100%) rename Source/{OxyPlot.Avalonia => OxyPlot.Avalonia.Shared}/Converters/ThicknessConverter.cs (100%) create mode 100644 Source/OxyPlot.Avalonia.Shared/Extensions/ConverterExtensions.cs rename Source/{OxyPlot.Avalonia => OxyPlot.Avalonia.Shared}/Extensions/DataPointExtension.cs (100%) rename Source/{OxyPlot.Avalonia => OxyPlot.Avalonia.Shared}/MoreColors.cs (100%) create mode 100644 Source/OxyPlot.Avalonia.Shared/OxyPlot.Avalonia.Shared.projitems create mode 100644 Source/OxyPlot.Avalonia.Shared/OxyPlot.Avalonia.Shared.shproj rename Source/{OxyPlot.Avalonia => OxyPlot.Avalonia.Shared}/PlotBase.Events.cs (78%) create mode 100644 Source/OxyPlot.Avalonia.Shared/PlotBase.Model.cs rename Source/{OxyPlot.Avalonia => OxyPlot.Avalonia.Shared}/PlotBase.Properties.cs (73%) create mode 100644 Source/OxyPlot.Avalonia.Shared/PlotBase.cs rename Source/{OxyPlot.Avalonia => OxyPlot.Avalonia.Shared}/Tracker/TrackerControl.cs (100%) rename Source/{OxyPlot.Avalonia => OxyPlot.Avalonia.Shared}/Tracker/TrackerDefinition.cs (100%) rename Source/{OxyPlot.Avalonia => OxyPlot.Avalonia.Shared}/Utilities/ConverterExtensions.cs (100%) delete mode 100644 Source/OxyPlot.Avalonia/PlotBase.cs rename Source/OxyPlot.Avalonia/{PlotBase.Export.cs => PlotView.Export.cs} (73%) create mode 100644 Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs create mode 100644 Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotView.cs create mode 100644 Source/OxyPlot.SkiaSharp.Avalonia/OxyPlot.SkiaSharp.Avalonia.csproj create mode 100644 Source/OxyPlot.SkiaSharp.Avalonia/OxyPlotModule.cs create mode 100644 Source/OxyPlot.SkiaSharp.Avalonia/PlotRenderer.cs create mode 100644 Source/OxyPlot.SkiaSharp.Avalonia/PlotView.cs create mode 100644 Source/OxyPlot.SkiaSharp.Avalonia/SkiaDrawOperation.cs create mode 100644 Source/OxyPlot.SkiaSharp.Avalonia/Themes/Default.axaml diff --git a/Source/Examples/Avalonia/ExampleBrowser/App.axaml b/Source/Examples/Avalonia/ExampleBrowser/App.axaml index 8d04dee..7ae94a3 100644 --- a/Source/Examples/Avalonia/ExampleBrowser/App.axaml +++ b/Source/Examples/Avalonia/ExampleBrowser/App.axaml @@ -4,5 +4,6 @@ + diff --git a/Source/Examples/Avalonia/ExampleBrowser/Category.cs b/Source/Examples/Avalonia/ExampleBrowser/Category.cs new file mode 100644 index 0000000..ccf879b --- /dev/null +++ b/Source/Examples/Avalonia/ExampleBrowser/Category.cs @@ -0,0 +1,27 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2014 OxyPlot contributors +// +// +// Represents the view-model for the main window. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ExampleBrowser +{ + using ExampleLibrary; + using System; + using System.Collections.Generic; + + public class Category + { + public Category(string key, List examples) + { + this.Key = key; + this.Examples = examples ?? throw new ArgumentNullException(nameof(examples)); + } + + public string Key { get; } + public List Examples { get; } + } +} \ No newline at end of file diff --git a/Source/Examples/Avalonia/ExampleBrowser/ExampleBrowser.csproj b/Source/Examples/Avalonia/ExampleBrowser/ExampleBrowser.csproj index 90ea5f9..237d605 100644 --- a/Source/Examples/Avalonia/ExampleBrowser/ExampleBrowser.csproj +++ b/Source/Examples/Avalonia/ExampleBrowser/ExampleBrowser.csproj @@ -12,5 +12,6 @@ + diff --git a/Source/Examples/Avalonia/ExampleBrowser/MainViewModel.cs b/Source/Examples/Avalonia/ExampleBrowser/MainViewModel.cs index 585f62f..fb13315 100644 --- a/Source/Examples/Avalonia/ExampleBrowser/MainViewModel.cs +++ b/Source/Examples/Avalonia/ExampleBrowser/MainViewModel.cs @@ -11,51 +11,57 @@ namespace ExampleBrowser { using ExampleLibrary; using OxyPlot; - using OxyPlot.Series; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; - public class Category - { - public Category(string key, List examples) - { - Key = key; - Examples = examples ?? throw new ArgumentNullException(nameof(examples)); - } - - public string Key { get; } - public List Examples { get; } - } - /// /// Represents the view-model for the main window. /// public class MainViewModel : INotifyPropertyChanged { + private ExampleInfo example; + private Renderer selectedRenderer; + private PlotModel model; + /// /// Initializes a new instance of the class. /// public MainViewModel() { - Model = new PlotModel() { Title = "Example Browser", Subtitle = "Select an example from the list" }; - Categories = Examples.GetList() + this.model = new PlotModel() { Title = "Example Browser", Subtitle = "Select an example from the list" }; + this.Categories = Examples.GetList() .GroupBy(e => e.Category) .Select(g => new Category(g.Key, g.ToList())) .OrderBy(c => c.Key) .ToList(); } + public IReadOnlyList Renderers { get; } = Enum.GetValues(); + + public Renderer SelectedRenderer + { + get => this.selectedRenderer; + set + { + if (this.selectedRenderer != value) + { + this.selectedRenderer = value; + this.CoerceSelectedRenderer(); + this.OnPropertyChanged(nameof(this.SelectedRenderer)); + } + } + } + public List Categories { get; } - private ExampleInfo example; /// /// Gets the plot model. /// public ExampleInfo Example { - get => example; + get => this.example; set { if (value == null) @@ -64,38 +70,47 @@ public ExampleInfo Example return; } - if (example != value) + if (this.example != value) { - example = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Example))); - Model = example?.PlotModel; - Model?.InvalidatePlot(true); + this.example = value; + this.OnPropertyChanged(nameof(this.Example)); + this.CoerceExample(); } } } - private PlotModel model; - /// - /// Gets the plot model. - /// - public PlotModel Model - { - get => model; - set - { - if (model != value) - { - model = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Model))); - } - } - } + public PlotModel CanvasModel => this.SelectedRenderer == Renderer.Canvas ? this.model : null; + + public PlotModel SkiaSharpModel => this.SelectedRenderer == Renderer.SkiaSharp ? this.model : null; + public PlotModel SkiaSharpDoubleBufferedModel => this.SelectedRenderer == Renderer.SkiaSharpDoubleBuffered ? this.model : null; public void ChangeExample(ExampleInfo example) { - Example = example; + this.Example = example; } public event PropertyChangedEventHandler PropertyChanged; + + private void CoerceSelectedRenderer() + { + ((IPlotModel)this.model)?.AttachPlotView(null); + this.OnPropertyChanged(nameof(this.CanvasModel)); + this.OnPropertyChanged(nameof(this.SkiaSharpModel)); + this.OnPropertyChanged(nameof(this.SkiaSharpDoubleBufferedModel)); + } + + private void CoerceExample() + { + this.model = this.example?.PlotModel; + this.model?.InvalidatePlot(true); + this.OnPropertyChanged(nameof(this.CanvasModel)); + this.OnPropertyChanged(nameof(this.SkiaSharpModel)); + this.OnPropertyChanged(nameof(this.SkiaSharpDoubleBufferedModel)); + } + + private void OnPropertyChanged(string propertyName) + { + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } } } \ No newline at end of file diff --git a/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml b/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml index a73203a..f8b3745 100644 --- a/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml +++ b/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml @@ -2,7 +2,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:oxy="clr-namespace:OxyPlot.Avalonia;assembly=OxyPlot.Avalonia" + xmlns:oxy="http://oxyplot.org/avalonia" + xmlns:oxySkia="http://oxyplot.org/skiasharp/avalonia" + xmlns:oxySkiaDb="http://oxyplot.org/skiasharp/avalonia/doublebuffered" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" xmlns:exampleBrowser="clr-namespace:ExampleBrowser;assembly=ExampleBrowser" xmlns:exampleLibrary="clr-namespace:ExampleLibrary;assembly=ExampleLibrary" @@ -11,6 +13,10 @@ + + + + @@ -42,6 +48,18 @@ - + + + + + + + + + + + + + diff --git a/Source/Examples/Avalonia/ExampleBrowser/NotNullBooleanConverter.cs b/Source/Examples/Avalonia/ExampleBrowser/NotNullBooleanConverter.cs new file mode 100644 index 0000000..1c33742 --- /dev/null +++ b/Source/Examples/Avalonia/ExampleBrowser/NotNullBooleanConverter.cs @@ -0,0 +1,20 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2014 OxyPlot contributors +// +// +// Represents the view-model for the main window. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ExampleBrowser +{ + using Avalonia.Data.Converters; + + public class NotNullBooleanConverter : FuncValueConverter + { + public NotNullBooleanConverter() : base(obj => obj is not null) + { + } + } +} \ No newline at end of file diff --git a/Source/Examples/Avalonia/ExampleBrowser/Program.cs b/Source/Examples/Avalonia/ExampleBrowser/Program.cs index 5f71d4b..e8a417a 100644 --- a/Source/Examples/Avalonia/ExampleBrowser/Program.cs +++ b/Source/Examples/Avalonia/ExampleBrowser/Program.cs @@ -7,8 +7,14 @@ class Program // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. - public static void Main(string[] args) => BuildAvaloniaApp() + public static void Main(string[] args) + { + OxyPlot.Avalonia.OxyPlotModule.EnsureLoaded(); + OxyPlot.SkiaSharp.Avalonia.OxyPlotModule.EnsureLoaded(); + + BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); + } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() diff --git a/Source/Examples/Avalonia/ExampleBrowser/Renderer.cs b/Source/Examples/Avalonia/ExampleBrowser/Renderer.cs new file mode 100644 index 0000000..8dca72b --- /dev/null +++ b/Source/Examples/Avalonia/ExampleBrowser/Renderer.cs @@ -0,0 +1,9 @@ +namespace ExampleBrowser +{ + public enum Renderer + { + Canvas, + SkiaSharp, + SkiaSharpDoubleBuffered, + } +} diff --git a/Source/OxyPlot.Avalonia/Converters/OxyColorConverter.cs b/Source/OxyPlot.Avalonia.Shared/Converters/OxyColorConverter.cs similarity index 100% rename from Source/OxyPlot.Avalonia/Converters/OxyColorConverter.cs rename to Source/OxyPlot.Avalonia.Shared/Converters/OxyColorConverter.cs diff --git a/Source/OxyPlot.Avalonia/Converters/ThicknessConverter.cs b/Source/OxyPlot.Avalonia.Shared/Converters/ThicknessConverter.cs similarity index 100% rename from Source/OxyPlot.Avalonia/Converters/ThicknessConverter.cs rename to Source/OxyPlot.Avalonia.Shared/Converters/ThicknessConverter.cs diff --git a/Source/OxyPlot.Avalonia.Shared/Extensions/ConverterExtensions.cs b/Source/OxyPlot.Avalonia.Shared/Extensions/ConverterExtensions.cs new file mode 100644 index 0000000..e38670c --- /dev/null +++ b/Source/OxyPlot.Avalonia.Shared/Extensions/ConverterExtensions.cs @@ -0,0 +1,16 @@ +using Avalonia; + +namespace OxyPlot.Avalonia.Extensions +{ + public static class ConverterExtensions + { + public static OxyRect ToOxyRect(this Rect rect) + { + return new OxyRect(rect.Left, rect.Top, rect.Width, rect.Height); + } + public static Rect ToRect(this OxyRect rect) + { + return new Rect(rect.Left, rect.Top, rect.Width, rect.Height); + } + } +} diff --git a/Source/OxyPlot.Avalonia/Extensions/DataPointExtension.cs b/Source/OxyPlot.Avalonia.Shared/Extensions/DataPointExtension.cs similarity index 100% rename from Source/OxyPlot.Avalonia/Extensions/DataPointExtension.cs rename to Source/OxyPlot.Avalonia.Shared/Extensions/DataPointExtension.cs diff --git a/Source/OxyPlot.Avalonia/MoreColors.cs b/Source/OxyPlot.Avalonia.Shared/MoreColors.cs similarity index 100% rename from Source/OxyPlot.Avalonia/MoreColors.cs rename to Source/OxyPlot.Avalonia.Shared/MoreColors.cs diff --git a/Source/OxyPlot.Avalonia.Shared/OxyPlot.Avalonia.Shared.projitems b/Source/OxyPlot.Avalonia.Shared/OxyPlot.Avalonia.Shared.projitems new file mode 100644 index 0000000..3b92152 --- /dev/null +++ b/Source/OxyPlot.Avalonia.Shared/OxyPlot.Avalonia.Shared.projitems @@ -0,0 +1,27 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 8f6dadff-7073-4862-86c7-4299ae17cd68 + + + OxyPlot.Avalonia.Shared + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/OxyPlot.Avalonia.Shared/OxyPlot.Avalonia.Shared.shproj b/Source/OxyPlot.Avalonia.Shared/OxyPlot.Avalonia.Shared.shproj new file mode 100644 index 0000000..da492ad --- /dev/null +++ b/Source/OxyPlot.Avalonia.Shared/OxyPlot.Avalonia.Shared.shproj @@ -0,0 +1,13 @@ + + + + 8f6dadff-7073-4862-86c7-4299ae17cd68 + 14.0 + + + + + + + + diff --git a/Source/OxyPlot.Avalonia/PlotBase.Events.cs b/Source/OxyPlot.Avalonia.Shared/PlotBase.Events.cs similarity index 78% rename from Source/OxyPlot.Avalonia/PlotBase.Events.cs rename to Source/OxyPlot.Avalonia.Shared/PlotBase.Events.cs index 3443a61..3baaeb0 100644 --- a/Source/OxyPlot.Avalonia/PlotBase.Events.cs +++ b/Source/OxyPlot.Avalonia.Shared/PlotBase.Events.cs @@ -7,8 +7,6 @@ // // -------------------------------------------------------------------------------------------------------------------- -using Avalonia.Controls; - namespace OxyPlot.Avalonia { using global::Avalonia.Input; @@ -34,7 +32,7 @@ protected override void OnKeyDown(KeyEventArgs e) } var args = new OxyKeyEventArgs { ModifierKeys = e.KeyModifiers.ToModifierKeys(), Key = e.Key.Convert() }; - e.Handled = ActualController.HandleKeyDown(this, args); + e.Handled = this.ActualController.HandleKeyDown(this, args); } /// @@ -44,18 +42,18 @@ protected override void OnKeyDown(KeyEventArgs e) protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { base.OnPointerWheelChanged(e); - if (e.Handled || !IsMouseWheelEnabled) + if (e.Handled || !this.IsMouseWheelEnabled) { return; } - e.Handled = ActualController.HandleMouseWheel(this, e.ToMouseWheelEventArgs(this)); + e.Handled = this.ActualController.HandleMouseWheel(this, e.ToMouseWheelEventArgs(this)); } /// /// Gets the dictionary of locations of touch pointers. /// - private SortedDictionary TouchPositions { get; } = new SortedDictionary(); + private SortedDictionary TouchPositions { get; } = []; /// /// Invoked when an unhandled MouseDown attached event reaches an element in its route that is derived from this class. Implement this method to add class handling for this event. @@ -69,7 +67,7 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) return; } - Focus(); + this.Focus(); e.Pointer.Capture(this); if (e.Pointer.Type == PointerType.Touch) @@ -84,19 +82,19 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) DeltaScale = new ScreenVector(1, 1), }; - TouchPositions[e.Pointer.Id] = position; + this.TouchPositions[e.Pointer.Id] = position; - if (TouchPositions.Count == 1) + if (this.TouchPositions.Count == 1) { - e.Handled = ActualController.HandleTouchStarted(this, touchEventArgs); + e.Handled = this.ActualController.HandleTouchStarted(this, touchEventArgs); } } else { // store the mouse down point, check it when mouse button is released to determine if the context menu should be shown - mouseDownPoint = e.GetPosition(this).ToScreenPoint(); + this.mouseDownPoint = e.GetPosition(this).ToScreenPoint(); - e.Handled = ActualController.HandleMouseDown(this, e.ToMouseDownEventArgs(this)); + e.Handled = this.ActualController.HandleMouseDown(this, e.ToMouseDownEventArgs(this)); } } @@ -115,17 +113,17 @@ protected override void OnPointerMoved(PointerEventArgs e) if (e.Pointer.Type == PointerType.Touch) { var point = e.GetPosition(this).ToScreenPoint(); - var oldTouchPoints = TouchPositions.Values.ToArray(); - TouchPositions[e.Pointer.Id] = point; - var newTouchPoints = TouchPositions.Values.ToArray(); + var oldTouchPoints = this.TouchPositions.Values.ToArray(); + this.TouchPositions[e.Pointer.Id] = point; + var newTouchPoints = this.TouchPositions.Values.ToArray(); var touchEventArgs = new OxyTouchEventArgs(newTouchPoints, oldTouchPoints); - e.Handled = ActualController.HandleTouchDelta(this, touchEventArgs); + e.Handled = this.ActualController.HandleTouchDelta(this, touchEventArgs); } else { - e.Handled = ActualController.HandleMouseMove(this, e.ToMouseEventArgs(this)); + e.Handled = this.ActualController.HandleMouseMove(this, e.ToMouseEventArgs(this)); } } @@ -155,31 +153,31 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e) DeltaScale = new ScreenVector(1, 1), }; - TouchPositions.Remove(e.Pointer.Id); + this.TouchPositions.Remove(e.Pointer.Id); - if (TouchPositions.Count == 0) + if (this.TouchPositions.Count == 0) { - e.Handled = ActualController.HandleTouchCompleted(this, touchEventArgs); + e.Handled = this.ActualController.HandleTouchCompleted(this, touchEventArgs); } } else { - e.Handled = ActualController.HandleMouseUp(this, e.ToMouseReleasedEventArgs(this)); + e.Handled = this.ActualController.HandleMouseUp(this, e.ToMouseReleasedEventArgs(this)); // Open the context menu var p = e.GetPosition(this).ToScreenPoint(); - var d = p.DistanceTo(mouseDownPoint); + var d = p.DistanceTo(this.mouseDownPoint); - if (ContextMenu != null) + if (this.ContextMenu != null) { if (Math.Abs(d) < 1e-8 && e.InitialPressMouseButton == MouseButton.Right) { - ContextMenu.DataContext = DataContext; - ContextMenu.IsVisible = true; + this.ContextMenu.DataContext = this.DataContext; + this.ContextMenu.IsVisible = true; } else { - ContextMenu.IsVisible = false; + this.ContextMenu.IsVisible = false; } } } @@ -197,7 +195,7 @@ protected override void OnPointerEntered(PointerEventArgs e) return; } - e.Handled = ActualController.HandleMouseEnter(this, e.ToMouseEventArgs(this)); + e.Handled = this.ActualController.HandleMouseEnter(this, e.ToMouseEventArgs(this)); } /// @@ -212,7 +210,7 @@ protected override void OnPointerExited(PointerEventArgs e) return; } - e.Handled = ActualController.HandleMouseLeave(this, e.ToMouseEventArgs(this)); + e.Handled = this.ActualController.HandleMouseLeave(this, e.ToMouseEventArgs(this)); } } } \ No newline at end of file diff --git a/Source/OxyPlot.Avalonia.Shared/PlotBase.Model.cs b/Source/OxyPlot.Avalonia.Shared/PlotBase.Model.cs new file mode 100644 index 0000000..9611420 --- /dev/null +++ b/Source/OxyPlot.Avalonia.Shared/PlotBase.Model.cs @@ -0,0 +1,127 @@ +using Avalonia; + +namespace OxyPlot.Avalonia +{ + public partial class PlotBase + { + /// + /// Identifies the dependency property. + /// + public static readonly StyledProperty ControllerProperty = AvaloniaProperty.Register(nameof(Controller)); + + /// + /// Identifies the dependency property. + /// + public static readonly StyledProperty ModelProperty = AvaloniaProperty.Register(nameof(Model), null); + + /// + /// The model lock. + /// + private readonly object modelLock = new(); + + /// + /// The current model (synchronized with the property, but can be accessed from all threads. + /// + private PlotModel currentModel; + + /// + /// The default plot controller. + /// + private IPlotController defaultController; + + static PlotBase() + { + ModelProperty.Changed.AddClassHandler(ModelChanged); + PaddingProperty.OverrideMetadata(typeof(PlotBase), new StyledPropertyMetadata(new Thickness(8))); + PaddingProperty.Changed.AddClassHandler(AppearanceChanged); + } + + /// + /// Gets or sets the model. + /// + /// The model. + public PlotModel Model + { + get => this.GetValue(ModelProperty); + set => this.SetValue(ModelProperty, value); + } + + /// + /// Gets or sets the Plot controller. + /// + /// The Plot controller. + public IPlotController Controller + { + get => this.GetValue(ControllerProperty); + set => this.SetValue(ControllerProperty, value); + } + + Model IView.ActualModel => this.ActualModel; + + IController IView.ActualController => this.ActualController; + + /// + /// Gets the actual model. + /// + /// The actual model. + public virtual PlotModel ActualModel => this.currentModel; + + /// + /// Gets the actual PlotView controller. + /// + /// The actual PlotView controller. + public virtual IPlotController ActualController => this.Controller ?? (this.defaultController ??= new PlotController()); + + /// + /// Called when the model is changed. + /// + private void OnModelChanged() + { + lock (this.modelLock) + { + if (this.currentModel != null) + { + ((IPlotModel)this.currentModel).AttachPlotView(null); + this.currentModel = null; + } + + if (this.Model != null) + { + ((IPlotModel)this.Model).AttachPlotView(null); // detach so we can re-attach + ((IPlotModel)this.Model).AttachPlotView(this); + this.currentModel = this.Model; + } + } + + this.InvalidatePlot(); + } + + /// + /// Called when the model is changed. + /// + /// The sender. + /// The instance containing the event data. + private static void ModelChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + ((PlotBase)d).OnModelChanged(); + } + + /// + /// Called when the visual appearance is changed. + /// + protected void OnAppearanceChanged() + { + this.InvalidatePlot(false); + } + + /// + /// Called when the visual appearance is changed. + /// + /// The d. + /// The instance containing the event data. + protected static void AppearanceChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + ((PlotBase)d).OnAppearanceChanged(); + } + } +} diff --git a/Source/OxyPlot.Avalonia/PlotBase.Properties.cs b/Source/OxyPlot.Avalonia.Shared/PlotBase.Properties.cs similarity index 73% rename from Source/OxyPlot.Avalonia/PlotBase.Properties.cs rename to Source/OxyPlot.Avalonia.Shared/PlotBase.Properties.cs index 92dd5a8..6b8aa19 100644 --- a/Source/OxyPlot.Avalonia/PlotBase.Properties.cs +++ b/Source/OxyPlot.Avalonia.Shared/PlotBase.Properties.cs @@ -53,24 +53,13 @@ public partial class PlotBase /// public static readonly StyledProperty ZoomVerticalCursorProperty = AvaloniaProperty.Register(nameof(ZoomVerticalCursor), new Cursor(StandardCursorType.SizeNorthSouth)); - static PlotBase() - { - } - /// /// Gets or sets the default tracker template. /// public ControlTemplate DefaultTrackerTemplate { - get - { - return GetValue(DefaultTrackerTemplateProperty); - } - - set - { - SetValue(DefaultTrackerTemplateProperty, value); - } + get => this.GetValue(DefaultTrackerTemplateProperty); + set => this.SetValue(DefaultTrackerTemplateProperty, value); } /// @@ -78,15 +67,8 @@ public ControlTemplate DefaultTrackerTemplate /// public bool IsMouseWheelEnabled { - get - { - return GetValue(IsMouseWheelEnabledProperty); - } - - set - { - SetValue(IsMouseWheelEnabledProperty, value); - } + get => this.GetValue(IsMouseWheelEnabledProperty); + set => this.SetValue(IsMouseWheelEnabledProperty, value); } /// @@ -95,15 +77,8 @@ public bool IsMouseWheelEnabled /// The pan cursor. public Cursor PanCursor { - get - { - return GetValue(PanCursorProperty); - } - - set - { - SetValue(PanCursorProperty, value); - } + get => this.GetValue(PanCursorProperty); + set => this.SetValue(PanCursorProperty, value); } /// @@ -112,15 +87,8 @@ public Cursor PanCursor /// The zoom horizontal cursor. public Cursor ZoomHorizontalCursor { - get - { - return GetValue(ZoomHorizontalCursorProperty); - } - - set - { - SetValue(ZoomHorizontalCursorProperty, value); - } + get => this.GetValue(ZoomHorizontalCursorProperty); + set => this.SetValue(ZoomHorizontalCursorProperty, value); } /// @@ -129,15 +97,8 @@ public Cursor ZoomHorizontalCursor /// The zoom rectangle cursor. public Cursor ZoomRectangleCursor { - get - { - return GetValue(ZoomRectangleCursorProperty); - } - - set - { - SetValue(ZoomRectangleCursorProperty, value); - } + get => this.GetValue(ZoomRectangleCursorProperty); + set => this.SetValue(ZoomRectangleCursorProperty, value); } /// @@ -146,15 +107,8 @@ public Cursor ZoomRectangleCursor /// The zoom rectangle template. public ControlTemplate ZoomRectangleTemplate { - get - { - return GetValue(ZoomRectangleTemplateProperty); - } - - set - { - SetValue(ZoomRectangleTemplateProperty, value); - } + get => this.GetValue(ZoomRectangleTemplateProperty); + set => this.SetValue(ZoomRectangleTemplateProperty, value); } /// @@ -163,15 +117,8 @@ public ControlTemplate ZoomRectangleTemplate /// The zoom vertical cursor. public Cursor ZoomVerticalCursor { - get - { - return GetValue(ZoomVerticalCursorProperty); - } - - set - { - SetValue(ZoomVerticalCursorProperty, value); - } + get => this.GetValue(ZoomVerticalCursorProperty); + set => this.SetValue(ZoomVerticalCursorProperty, value); } } } diff --git a/Source/OxyPlot.Avalonia.Shared/PlotBase.cs b/Source/OxyPlot.Avalonia.Shared/PlotBase.cs new file mode 100644 index 0000000..0740f7f --- /dev/null +++ b/Source/OxyPlot.Avalonia.Shared/PlotBase.cs @@ -0,0 +1,250 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2014 OxyPlot contributors +// +// +// Represents a control that displays a . +// +// -------------------------------------------------------------------------------------------------------------------- + +using Avalonia.Reactive; + +namespace OxyPlot.Avalonia +{ + using global::Avalonia; + using global::Avalonia.Controls; + using global::Avalonia.Controls.Primitives; + using global::Avalonia.Input; + using System.Collections.ObjectModel; + using System.Linq; + using System.Threading; + + /// + /// Represents a control that displays a . + /// + public abstract partial class PlotBase : TemplatedControl, IPlotView + { + private Control currentTracker; + protected Panel panel; + private Canvas overlays; + private ContentControl zoomControl; + private ScreenPoint mouseDownPoint; + + protected const string PartPanel = "PART_Panel"; + + /// + /// Invalidation flag (0: no update, 1: update, 2: update date). + /// + protected internal int isUpdateRequired; + + /// + /// Invalidation flag (0: no update, 1: update visual elements). + /// + protected internal int isRenderRequired; + + /// + /// Initializes a new instance of the class. + /// + protected PlotBase() + { + this.TrackerDefinitions = []; + this.GetObservable(BoundsProperty).Subscribe(new AnonymousObserver(this.OnSizeChanged)); + } + + /// + /// Gets the coordinates of the client area of the view. + /// + public OxyRect ClientArea => new(0, 0, this.Bounds.Width, this.Bounds.Height); + + /// + /// Gets the tracker definitions. + /// + /// The tracker definitions. + public ObservableCollection TrackerDefinitions { get; } + + /// + /// Hides the tracker. + /// + public void HideTracker() + { + if (this.currentTracker != null) + { + this.overlays.Children.Remove(this.currentTracker); + this.currentTracker = null; + } + } + + /// + /// Hides the zoom rectangle. + /// + public void HideZoomRectangle() + { + this.zoomControl.IsVisible = false; + } + + /// + /// Pans all axes. + /// + /// The delta. + public void PanAllAxes(Vector delta) + { + this.ActualModel?.PanAllAxes(delta.X, delta.Y); + this.InvalidatePlot(false); + } + + /// + /// Zooms all axes. + /// + /// The zoom factor. + public void ZoomAllAxes(double factor) + { + this.ActualModel?.ZoomAllAxes(factor); + this.InvalidatePlot(false); + } + + /// + /// Resets all axes. + /// + public void ResetAllAxes() + { + this.ActualModel?.ResetAllAxes(); + this.InvalidatePlot(false); + } + + /// + /// Invalidate the PlotView (not blocking the UI thread) + /// + /// The update Data. + public virtual void InvalidatePlot(bool updateData = true) + { + this.isUpdateRequired = updateData ? 2 : 1; + } + + /// + /// Sets the cursor type. + /// + /// The cursor type. + public void SetCursorType(CursorType cursorType) + { + this.Cursor = cursorType switch + { + CursorType.Pan => this.PanCursor, + CursorType.ZoomRectangle => this.ZoomRectangleCursor, + CursorType.ZoomHorizontal => this.ZoomHorizontalCursor, + CursorType.ZoomVertical => this.ZoomVerticalCursor, + _ => Cursor.Default, + }; + } + + /// + /// Shows the tracker. + /// + /// The tracker data. + public void ShowTracker(TrackerHitResult trackerHitResult) + { + if (trackerHitResult == null) + { + this.HideTracker(); + return; + } + + var trackerTemplate = this.DefaultTrackerTemplate; + if (trackerHitResult.Series != null && !string.IsNullOrEmpty(trackerHitResult.Series.TrackerKey)) + { + var match = this.TrackerDefinitions.FirstOrDefault(t => t.TrackerKey == trackerHitResult.Series.TrackerKey); + if (match != null) + { + trackerTemplate = match.TrackerTemplate; + } + } + + if (trackerTemplate == null) + { + this.HideTracker(); + return; + } + + var tracker = trackerTemplate.Build(new ContentControl()); + + if (tracker.Result != this.currentTracker) + { + this.HideTracker(); + this.overlays.Children.Add(tracker.Result); + this.currentTracker = tracker.Result; + } + + if (this.currentTracker != null) + { + this.currentTracker.DataContext = trackerHitResult; + } + } + + /// + /// Shows the zoom rectangle. + /// + /// The rectangle. + public void ShowZoomRectangle(OxyRect r) + { + this.zoomControl.Width = r.Width; + this.zoomControl.Height = r.Height; + Canvas.SetLeft(this.zoomControl, r.Left); + Canvas.SetTop(this.zoomControl, r.Top); + this.zoomControl.Template = this.ZoomRectangleTemplate; + this.zoomControl.IsVisible = true; + } + + /// + /// Stores text on the clipboard. + /// + /// The text. + public async void SetClipboardText(string text) + { + if (TopLevel.GetTopLevel(this) is { Clipboard: { } clipboard }) + { + await clipboard.SetTextAsync(text).ConfigureAwait(true); + } + } + + protected void UpdatePlot() + { + if (this.ActualModel is PlotModel plotModel) + { + var updateRequired = Interlocked.Exchange(ref this.isUpdateRequired, 0); + if (updateRequired == 0) + { + return; + } + + lock (plotModel.SyncRoot) + { + ((IPlotModel)plotModel).Update(updateRequired > 1); + } + } + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + this.panel = e.NameScope.Find(PartPanel) as Panel; + if (this.panel == null) + { + return; + } + + this.overlays = new Canvas { Name = "Overlays" }; + this.panel.Children.Add(this.overlays); + + this.zoomControl = new ContentControl(); + this.overlays.Children.Add(this.zoomControl); + } + + private void OnSizeChanged(Rect size) + { + if (size.Height > 0 && size.Width > 0) + { + this.InvalidatePlot(false); + } + } + } +} diff --git a/Source/OxyPlot.Avalonia/Tracker/TrackerControl.cs b/Source/OxyPlot.Avalonia.Shared/Tracker/TrackerControl.cs similarity index 100% rename from Source/OxyPlot.Avalonia/Tracker/TrackerControl.cs rename to Source/OxyPlot.Avalonia.Shared/Tracker/TrackerControl.cs diff --git a/Source/OxyPlot.Avalonia/Tracker/TrackerDefinition.cs b/Source/OxyPlot.Avalonia.Shared/Tracker/TrackerDefinition.cs similarity index 100% rename from Source/OxyPlot.Avalonia/Tracker/TrackerDefinition.cs rename to Source/OxyPlot.Avalonia.Shared/Tracker/TrackerDefinition.cs diff --git a/Source/OxyPlot.Avalonia/Utilities/ConverterExtensions.cs b/Source/OxyPlot.Avalonia.Shared/Utilities/ConverterExtensions.cs similarity index 100% rename from Source/OxyPlot.Avalonia/Utilities/ConverterExtensions.cs rename to Source/OxyPlot.Avalonia.Shared/Utilities/ConverterExtensions.cs diff --git a/Source/OxyPlot.Avalonia.sln b/Source/OxyPlot.Avalonia.sln index 84b5a41..209bb91 100644 --- a/Source/OxyPlot.Avalonia.sln +++ b/Source/OxyPlot.Avalonia.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29409.12 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34330.188 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OxyPlot.Avalonia", "OxyPlot.Avalonia\OxyPlot.Avalonia.csproj", "{33F816D5-7FAA-46EC-947E-9B51CB366D9D}" EndProject @@ -26,7 +26,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MemoryTest", "Examples\Aval EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvaloniaExamples", "Examples\Avalonia\AvaloniaExamples\AvaloniaExamples.csproj", "{B86739B8-5DE9-406F-9418-0751BF7C166F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleBrowser", "Examples\Avalonia\ExampleBrowser\ExampleBrowser.csproj", "{E4993D0B-C4C6-47CE-A55E-19EFB28EC034}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleBrowser", "Examples\Avalonia\ExampleBrowser\ExampleBrowser.csproj", "{E4993D0B-C4C6-47CE-A55E-19EFB28EC034}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OxyPlot.SkiaSharp.Avalonia", "OxyPlot.SkiaSharp.Avalonia\OxyPlot.SkiaSharp.Avalonia.csproj", "{01E98C01-3F55-460E-8F28-4334C51B6A70}" +EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "OxyPlot.Avalonia.Shared", "OxyPlot.Avalonia.Shared\OxyPlot.Avalonia.Shared.shproj", "{8F6DADFF-7073-4862-86C7-4299AE17CD68}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -98,6 +102,18 @@ Global {E4993D0B-C4C6-47CE-A55E-19EFB28EC034}.Release|x64.Build.0 = Release|Any CPU {E4993D0B-C4C6-47CE-A55E-19EFB28EC034}.Release|x86.ActiveCfg = Release|Any CPU {E4993D0B-C4C6-47CE-A55E-19EFB28EC034}.Release|x86.Build.0 = Release|Any CPU + {01E98C01-3F55-460E-8F28-4334C51B6A70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01E98C01-3F55-460E-8F28-4334C51B6A70}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01E98C01-3F55-460E-8F28-4334C51B6A70}.Debug|x64.ActiveCfg = Debug|Any CPU + {01E98C01-3F55-460E-8F28-4334C51B6A70}.Debug|x64.Build.0 = Debug|Any CPU + {01E98C01-3F55-460E-8F28-4334C51B6A70}.Debug|x86.ActiveCfg = Debug|Any CPU + {01E98C01-3F55-460E-8F28-4334C51B6A70}.Debug|x86.Build.0 = Debug|Any CPU + {01E98C01-3F55-460E-8F28-4334C51B6A70}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01E98C01-3F55-460E-8F28-4334C51B6A70}.Release|Any CPU.Build.0 = Release|Any CPU + {01E98C01-3F55-460E-8F28-4334C51B6A70}.Release|x64.ActiveCfg = Release|Any CPU + {01E98C01-3F55-460E-8F28-4334C51B6A70}.Release|x64.Build.0 = Release|Any CPU + {01E98C01-3F55-460E-8F28-4334C51B6A70}.Release|x86.ActiveCfg = Release|Any CPU + {01E98C01-3F55-460E-8F28-4334C51B6A70}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -111,4 +127,9 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {468E0F0B-9D70-4C0F-93F7-60DA5581D466} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + OxyPlot.Avalonia.Shared\OxyPlot.Avalonia.Shared.projitems*{01e98c01-3f55-460e-8f28-4334c51b6a70}*SharedItemsImports = 5 + OxyPlot.Avalonia.Shared\OxyPlot.Avalonia.Shared.projitems*{33f816d5-7faa-46ec-947e-9b51cb366d9d}*SharedItemsImports = 5 + OxyPlot.Avalonia.Shared\OxyPlot.Avalonia.Shared.projitems*{8f6dadff-7073-4862-86c7-4299ae17cd68}*SharedItemsImports = 13 + EndGlobalSection EndGlobal diff --git a/Source/OxyPlot.Avalonia/OxyPlot.Avalonia.csproj b/Source/OxyPlot.Avalonia/OxyPlot.Avalonia.csproj index 7874ce4..a4849e2 100644 --- a/Source/OxyPlot.Avalonia/OxyPlot.Avalonia.csproj +++ b/Source/OxyPlot.Avalonia/OxyPlot.Avalonia.csproj @@ -35,4 +35,6 @@ + + diff --git a/Source/OxyPlot.Avalonia/Plot.cs b/Source/OxyPlot.Avalonia/Plot.cs index abab5a3..abcfc49 100644 --- a/Source/OxyPlot.Avalonia/Plot.cs +++ b/Source/OxyPlot.Avalonia/Plot.cs @@ -13,7 +13,6 @@ namespace OxyPlot.Avalonia { using global::Avalonia.Controls; using global::Avalonia.LogicalTree; - using global::Avalonia.VisualTree; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; @@ -21,7 +20,7 @@ namespace OxyPlot.Avalonia /// /// Represents a control that displays a . /// - public partial class Plot : PlotBase, IPlot + public partial class Plot : PlotView, IPlot { /// /// The internal model. @@ -38,107 +37,65 @@ public partial class Plot : PlotBase, IPlot /// public Plot() { - series = new ObservableCollection(); - axes = new ObservableCollection(); - annotations = new ObservableCollection(); - legends = new ObservableCollection(); - - series.CollectionChanged += OnSeriesChanged; - axes.CollectionChanged += OnAxesChanged; - annotations.CollectionChanged += OnAnnotationsChanged; - legends.CollectionChanged += this.OnAnnotationsChanged; - - defaultController = new PlotController(); - internalModel = new PlotModel(); - ((IPlotModel)internalModel).AttachPlotView(this); + this.series = []; + this.axes = []; + this.annotations = []; + this.legends = []; + + this.series.CollectionChanged += this.OnSeriesChanged; + this.axes.CollectionChanged += this.OnAxesChanged; + this.annotations.CollectionChanged += this.OnAnnotationsChanged; + this.legends.CollectionChanged += this.OnAnnotationsChanged; + + this.defaultController = new PlotController(); + this.internalModel = new PlotModel(); + ((IPlotModel)this.internalModel).AttachPlotView(this); } /// /// Gets the annotations. /// /// The annotations. - public ObservableCollection Annotations - { - get - { - return annotations; - } - } + public ObservableCollection Annotations => this.annotations; /// /// Gets the actual model. /// /// The actual model. - public override PlotModel ActualModel - { - get - { - return internalModel; - } - } + public override PlotModel ActualModel => this.internalModel; /// /// Gets the actual Plot controller. /// /// The actual Plot controller. - public override IPlotController ActualController - { - get - { - return defaultController; - } - } + public override IPlotController ActualController => this.defaultController; /// /// Updates the model. If Model==null, an internal model will be created. The ActualModel.Update will be called (updates all series data). /// /// if set to true , all data collections will be updated. - protected new void UpdateModel(bool updateData = true) + public override void InvalidatePlot(bool updateData = true) { - SynchronizeProperties(); - SynchronizeSeries(); - SynchronizeAxes(); - SynchronizeAnnotations(); - SynchronizeLegends(); - - // TODO: does this achieve anything? we always call InvalidatePlot after UpdateModel - base.UpdateModel(updateData); - } - - /// - /// Called when the visual appearance is changed. - /// - protected void OnAppearanceChanged() - { - UpdateModel(false); - InvalidatePlot(false); + base.InvalidatePlot(updateData); + this.SynchronizeProperties(); + this.SynchronizeSeries(); + this.SynchronizeAxes(); + this.SynchronizeAnnotations(); + this.SynchronizeLegends(); } void IPlot.ElementAppearanceChanged(object element) { // TODO: determine type of element to perform a more fine-grained update - this.UpdateModel(false); base.InvalidatePlot(false); } void IPlot.ElementDataChanged(object element) { // TODO: determine type of element to perform a more fine-grained update - this.UpdateModel(true); base.InvalidatePlot(true); } - - /// - /// Called when the visual appearance is changed. - /// - /// The d. - /// The instance containing the event data. - private static void AppearanceChanged(global::Avalonia.AvaloniaObject d, global::Avalonia.AvaloniaPropertyChangedEventArgs e) - { - ((Plot)d).OnAppearanceChanged(); - } - /// /// Called when annotations is changed. /// @@ -146,7 +103,7 @@ private static void AppearanceChanged(global::Avalonia.AvaloniaObject d, global: /// The instance containing the event data. private void OnAnnotationsChanged(object sender, NotifyCollectionChangedEventArgs e) { - SyncLogicalTree(e); + this.SyncLogicalTree(e); } /// @@ -156,7 +113,7 @@ private void OnAnnotationsChanged(object sender, NotifyCollectionChangedEventArg /// The instance containing the event data. private void OnAxesChanged(object sender, NotifyCollectionChangedEventArgs e) { - SyncLogicalTree(e); + this.SyncLogicalTree(e); } /// @@ -166,7 +123,7 @@ private void OnAxesChanged(object sender, NotifyCollectionChangedEventArgs e) /// The instance containing the event data. private void OnSeriesChanged(object sender, NotifyCollectionChangedEventArgs e) { - SyncLogicalTree(e); + this.SyncLogicalTree(e); } /// @@ -183,8 +140,9 @@ private void SyncLogicalTree(NotifyCollectionChangedEventArgs e) { item.SetParent(this); } - LogicalChildren.AddRange(e.NewItems.OfType()); - VisualChildren.AddRange(e.NewItems.OfType()); + + this.LogicalChildren.AddRange(e.NewItems.OfType()); + this.VisualChildren.AddRange(e.NewItems.OfType()); } if (e.OldItems != null) @@ -196,8 +154,8 @@ private void SyncLogicalTree(NotifyCollectionChangedEventArgs e) foreach (var item in e.OldItems) { - LogicalChildren.Remove((ILogical)item); - VisualChildren.Remove((Visual)item); + this.LogicalChildren.Remove((ILogical)item); + this.VisualChildren.Remove((Visual)item); } } @@ -209,45 +167,45 @@ private void SyncLogicalTree(NotifyCollectionChangedEventArgs e) /// private void SynchronizeProperties() { - var m = internalModel; + var m = this.internalModel; - m.PlotType = PlotType; + m.PlotType = this.PlotType; - m.PlotMargins = PlotMargins.ToOxyThickness(); - m.Padding = Padding.ToOxyThickness(); - m.TitlePadding = TitlePadding; + m.PlotMargins = this.PlotMargins.ToOxyThickness(); + m.Padding = this.Padding.ToOxyThickness(); + m.TitlePadding = this.TitlePadding; - m.Culture = Culture; + m.Culture = this.Culture; - m.DefaultColors = DefaultColors.Select(c => c.ToOxyColor()).ToArray(); - m.DefaultFont = DefaultFont; - m.DefaultFontSize = DefaultFontSize; + m.DefaultColors = this.DefaultColors.Select(c => c.ToOxyColor()).ToArray(); + m.DefaultFont = this.DefaultFont; + m.DefaultFontSize = this.DefaultFontSize; - m.Title = Title; - m.TitleColor = TitleColor.ToOxyColor(); - m.TitleFont = TitleFont; - m.TitleFontSize = TitleFontSize; - m.TitleFontWeight = (int)TitleFontWeight; - m.TitleToolTip = TitleToolTip; + m.Title = this.Title; + m.TitleColor = this.TitleColor.ToOxyColor(); + m.TitleFont = this.TitleFont; + m.TitleFontSize = this.TitleFontSize; + m.TitleFontWeight = (int)this.TitleFontWeight; + m.TitleToolTip = this.TitleToolTip; - m.Subtitle = Subtitle; - m.SubtitleColor = SubtitleColor.ToOxyColor(); - m.SubtitleFont = SubtitleFont; - m.SubtitleFontSize = SubtitleFontSize; - m.SubtitleFontWeight = (int)SubtitleFontWeight; + m.Subtitle = this.Subtitle; + m.SubtitleColor = this.SubtitleColor.ToOxyColor(); + m.SubtitleFont = this.SubtitleFont; + m.SubtitleFontSize = this.SubtitleFontSize; + m.SubtitleFontWeight = (int)this.SubtitleFontWeight; - m.TextColor = TextColor.ToOxyColor(); - m.SelectionColor = SelectionColor.ToOxyColor(); + m.TextColor = this.TextColor.ToOxyColor(); + m.SelectionColor = this.SelectionColor.ToOxyColor(); - m.RenderingDecorator = RenderingDecorator; + m.RenderingDecorator = this.RenderingDecorator; - m.AxisTierDistance = AxisTierDistance; + m.AxisTierDistance = this.AxisTierDistance; - m.IsLegendVisible = IsLegendVisible; + m.IsLegendVisible = this.IsLegendVisible; - m.PlotAreaBackground = PlotAreaBackground.ToOxyColor(); - m.PlotAreaBorderColor = PlotAreaBorderColor.ToOxyColor(); - m.PlotAreaBorderThickness = PlotAreaBorderThickness.ToOxyThickness(); + m.PlotAreaBackground = this.PlotAreaBackground.ToOxyColor(); + m.PlotAreaBorderColor = this.PlotAreaBorderColor.ToOxyColor(); + m.PlotAreaBorderThickness = this.PlotAreaBorderThickness.ToOxyThickness(); } /// @@ -255,10 +213,10 @@ private void SynchronizeProperties() /// private void SynchronizeAnnotations() { - internalModel.Annotations.Clear(); - foreach (var a in Annotations) + this.internalModel.Annotations.Clear(); + foreach (var a in this.Annotations) { - internalModel.Annotations.Add(a.CreateModel()); + this.internalModel.Annotations.Add(a.CreateModel()); } } @@ -267,10 +225,10 @@ private void SynchronizeAnnotations() /// private void SynchronizeAxes() { - internalModel.Axes.Clear(); - foreach (var a in Axes) + this.internalModel.Axes.Clear(); + foreach (var a in this.Axes) { - internalModel.Axes.Add(a.CreateModel()); + this.internalModel.Axes.Add(a.CreateModel()); } } @@ -279,10 +237,10 @@ private void SynchronizeAxes() /// private void SynchronizeSeries() { - internalModel.Series.Clear(); - foreach (var s in Series) + this.internalModel.Series.Clear(); + foreach (var s in this.Series) { - internalModel.Series.Add(s.CreateModel()); + this.internalModel.Series.Add(s.CreateModel()); } } @@ -291,10 +249,10 @@ private void SynchronizeSeries() /// private void SynchronizeLegends() { - internalModel.Legends.Clear(); - foreach (var l in Legends) + this.internalModel.Legends.Clear(); + foreach (var l in this.Legends) { - internalModel.Legends.Add(l.CreateModel()); + this.internalModel.Legends.Add(l.CreateModel()); } } } diff --git a/Source/OxyPlot.Avalonia/PlotBase.cs b/Source/OxyPlot.Avalonia/PlotBase.cs deleted file mode 100644 index 1d43d90..0000000 --- a/Source/OxyPlot.Avalonia/PlotBase.cs +++ /dev/null @@ -1,565 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright (c) 2014 OxyPlot contributors -// -// -// Represents a control that displays a . -// -// -------------------------------------------------------------------------------------------------------------------- - -using Avalonia.Reactive; - -namespace OxyPlot.Avalonia -{ - using global::Avalonia; - using global::Avalonia.Controls; - using global::Avalonia.Controls.Presenters; - using global::Avalonia.Controls.Primitives; - using global::Avalonia.Input; - using global::Avalonia.Threading; - using global::Avalonia.VisualTree; - using System; - using System.Collections.ObjectModel; - using System.Linq; - using System.Threading; - - /// - /// Represents a control that displays a . - /// - public abstract partial class PlotBase : TemplatedControl, IPlotView - { - /// - /// The Grid PART constant. - /// - protected const string PartPanel = "PART_Panel"; - - /// - /// The tracker definitions. - /// - private readonly ObservableCollection trackerDefinitions; - - /// - /// The render context - /// - private CanvasRenderContext renderContext; - - /// - /// The canvas. - /// - private Canvas canvas; - - /// - /// The current tracker. - /// - private Control currentTracker; - - /// - /// The grid. - /// - private Panel panel; - - /// - /// Invalidation flag (0: no update, 1: update, 2: update date). - /// - private int isUpdateRequired; - - /// - /// Invalidation flag (0: no update, 1: update visual elements). - /// - private int isPlotInvalidated; - - /// - /// The mouse down point. - /// - private ScreenPoint mouseDownPoint; - - /// - /// The overlays. - /// - private Canvas overlays; - - /// - /// The zoom control. - /// - private ContentControl zoomControl; - - /// - /// The is visible to user cache. - /// - private bool isVisibleToUserCache; - - /// - /// The cached parent. - /// - private Control containerCache; - - /// - /// Initializes a new instance of the class. - /// - protected PlotBase() - { - DisconnectCanvasWhileUpdating = true; - trackerDefinitions = new ObservableCollection(); - - this.GetObservable(BoundsProperty).Subscribe(new AnonymousObserver(OnSizeChanged)); - } - - /// - /// Gets or sets a value indicating whether to disconnect the canvas while updating. - /// - /// true if canvas should be disconnected while updating; otherwise, false. - public bool DisconnectCanvasWhileUpdating { get; set; } - - /// - /// Gets the actual model in the view. - /// - /// - /// The actual model. - /// - Model IView.ActualModel - { - get - { - return ActualModel; - } - } - - /// - /// Gets the actual model. - /// - /// The actual model. - public abstract PlotModel ActualModel { get; } - - /// - /// Gets the actual controller. - /// - /// - /// The actual . - /// - IController IView.ActualController - { - get - { - return ActualController; - } - } - - /// - /// Gets the actual PlotView controller. - /// - /// The actual PlotView controller. - public abstract IPlotController ActualController { get; } - - /// - /// Gets the coordinates of the client area of the view. - /// - public OxyRect ClientArea - { - get - { - return new OxyRect(0, 0, Bounds.Width, Bounds.Height); - } - } - - /// - /// Gets the tracker definitions. - /// - /// The tracker definitions. - public ObservableCollection TrackerDefinitions - { - get - { - return trackerDefinitions; - } - } - - /// - /// Hides the tracker. - /// - public void HideTracker() - { - if (currentTracker != null) - { - overlays.Children.Remove(currentTracker); - currentTracker = null; - } - } - - /// - /// Hides the zoom rectangle. - /// - public void HideZoomRectangle() - { - zoomControl.IsVisible = false; - } - - /// - /// Pans all axes. - /// - /// The delta. - public void PanAllAxes(Vector delta) - { - ActualModel?.PanAllAxes(delta.X, delta.Y); - - InvalidatePlot(false); - } - - /// - /// Zooms all axes. - /// - /// The zoom factor. - public void ZoomAllAxes(double factor) - { - ActualModel?.ZoomAllAxes(factor); - - InvalidatePlot(false); - } - - /// - /// Resets all axes. - /// - public void ResetAllAxes() - { - ActualModel?.ResetAllAxes(); - - InvalidatePlot(false); - } - - /// - /// Invalidate the PlotView (not blocking the UI thread) - /// - /// The update Data. - public void InvalidatePlot(bool updateData = true) - { - // perform update on UI thread - var updateState = updateData ? 2 : 1; - int currentState = isUpdateRequired; - - while (currentState < updateState) - { - if (Interlocked.CompareExchange(ref isUpdateRequired, updateState, currentState) == currentState) - { - BeginInvoke(() => UpdateModel(updateData)); - break; - } - else - { - currentState = isUpdateRequired; - } - } - } - - /// - /// When overridden in a derived class, is invoked whenever application code or internal processes (such as a rebuilding layout pass) - /// call . In simplest terms, this means the method is called - /// just before a UI element displays in an application. For more information, see Remarks. - /// - /// Event data for applying the template. - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) - { - base.OnApplyTemplate(e); - panel = e.NameScope.Find(PartPanel) as Panel; - if (panel == null) - { - return; - } - - canvas = new Canvas(); - panel.Children.Add(canvas); - renderContext = new CanvasRenderContext(canvas); - - overlays = new Canvas { Name = "Overlays" }; - panel.Children.Add(overlays); - - zoomControl = new ContentControl(); - overlays.Children.Add(zoomControl); - } - - /// - /// Sets the cursor type. - /// - /// The cursor type. - public void SetCursorType(CursorType cursorType) - { - switch (cursorType) - { - case CursorType.Pan: - Cursor = PanCursor; - break; - case CursorType.ZoomRectangle: - Cursor = ZoomRectangleCursor; - break; - case CursorType.ZoomHorizontal: - Cursor = ZoomHorizontalCursor; - break; - case CursorType.ZoomVertical: - Cursor = ZoomVerticalCursor; - break; - default: - Cursor = Cursor.Default; - break; - } - } - - /// - /// Shows the tracker. - /// - /// The tracker data. - public void ShowTracker(TrackerHitResult trackerHitResult) - { - if (trackerHitResult == null) - { - HideTracker(); - return; - } - - var trackerTemplate = DefaultTrackerTemplate; - if (trackerHitResult.Series != null && !string.IsNullOrEmpty(trackerHitResult.Series.TrackerKey)) - { - var match = TrackerDefinitions.FirstOrDefault(t => t.TrackerKey == trackerHitResult.Series.TrackerKey); - if (match != null) - { - trackerTemplate = match.TrackerTemplate; - } - } - - if (trackerTemplate == null) - { - HideTracker(); - return; - } - - var tracker = trackerTemplate.Build(new ContentControl()); - - // ReSharper disable once RedundantNameQualifier - if (!object.ReferenceEquals(tracker, currentTracker)) - { - HideTracker(); - overlays.Children.Add(tracker.Result); - currentTracker = tracker.Result; - } - - if (currentTracker != null) - { - currentTracker.DataContext = trackerHitResult; - } - } - - /// - /// Shows the zoom rectangle. - /// - /// The rectangle. - public void ShowZoomRectangle(OxyRect r) - { - zoomControl.Width = r.Width; - zoomControl.Height = r.Height; - Canvas.SetLeft(zoomControl, r.Left); - Canvas.SetTop(zoomControl, r.Top); - zoomControl.Template = ZoomRectangleTemplate; - zoomControl.IsVisible = true; - } - - /// - /// Stores text on the clipboard. - /// - /// The text. - public async void SetClipboardText(string text) - { - if (TopLevel.GetTopLevel(this) is { Clipboard: { } clipboard }) - { - await clipboard.SetTextAsync(text).ConfigureAwait(true); - } - } - - /// - /// Provides the behavior for the Arrange pass of Silverlight layout. Classes can override this method to define their own Arrange pass behavior. - /// - /// The final area within the parent that this object should use to arrange itself and its children. - /// The actual size that is used after the element is arranged in layout. - protected override Size ArrangeOverride(Size finalSize) - { - var actualSize = base.ArrangeOverride(finalSize); - if (actualSize.Width > 0 && actualSize.Height > 0) - { - if (Interlocked.CompareExchange(ref isPlotInvalidated, 0, 1) == 1) - { - UpdateVisuals(); - } - } - - return actualSize; - } - - /// - /// Updates the model. - /// - /// The update Data. - protected void UpdateModel(bool updateData = true) - { - if (Width <= 0 || Height <= 0 || ActualModel == null) - { - isUpdateRequired = 0; - return; - } - - lock (this.ActualModel.SyncRoot) - { - var updateState = (Interlocked.Exchange(ref isUpdateRequired, 0)); - - if (updateState > 0) - { - ((IPlotModel)ActualModel).Update(updateState == 2 || updateData); - } - } - - if (Interlocked.CompareExchange(ref isPlotInvalidated, 1, 0) == 0) - { - // Invalidate the arrange state for the element. - // After the invalidation, the element will have its layout updated, - // which will occur asynchronously unless subsequently forced by UpdateLayout. - BeginInvoke(InvalidateArrange); - } - - } - - /// - /// Determines whether the plot is currently visible to the user. - /// - /// true if the plot is currently visible to the user; otherwise, false. - protected bool IsVisibleToUser() - { - return IsUserVisible(this); - } - - /// - /// Determines whether the specified element is currently visible to the user. - /// - /// The element. - /// true if the specified element is currently visible to the user; otherwise, false. - private static bool IsUserVisible(Control element) - { - return element.IsEffectivelyVisible; - } - - /// - /// Called when the size of the control is changed. - /// - /// The sender. - /// The new size - private void OnSizeChanged(Rect size) - { - if (size.Height > 0 && size.Width > 0) - { - InvalidatePlot(false); - } - } - - /// - /// Gets the relevant parent. - /// - /// Type of the relevant parent - /// The object. - /// The relevant parent. - private Control GetRelevantParent(Visual obj) - where T : Control - { - var container = obj.GetVisualParent(); - - if (container is ContentPresenter contentPresenter) - { - container = GetRelevantParent(contentPresenter); - } - - if (container is Panel panel) - { - container = GetRelevantParent(panel); - } - - if (!(container is T) && (container != null)) - { - container = GetRelevantParent(container); - } - - return (Control)container; - } - - /// - /// Updates the visuals. - /// - private void UpdateVisuals() - { - if (canvas == null || renderContext == null) - { - return; - } - - if (!IsVisibleToUser()) - { - return; - } - - // Clear the canvas - canvas.Children.Clear(); - - if (ActualModel?.Background.IsVisible() == true) - { - canvas.Background = ActualModel.Background.ToBrush(); - } - else - { - canvas.Background = null; - } - - if (ActualModel != null) - { - lock (this.ActualModel.SyncRoot) - { - var updateState = (Interlocked.Exchange(ref isUpdateRequired, 0)); - - if (updateState > 0) - { - ((IPlotModel)ActualModel).Update(updateState == 2); - } - - if (DisconnectCanvasWhileUpdating) - { - // TODO: profile... not sure if this makes any difference - var idx = panel.Children.IndexOf(canvas); - if (idx != -1) - { - panel.Children.RemoveAt(idx); - } - - ((IPlotModel)ActualModel).Render(renderContext, new OxyRect(0, 0, canvas.Bounds.Width, canvas.Bounds.Height)); - - // reinsert the canvas again - if (idx != -1) - { - panel.Children.Insert(idx, canvas); - } - } - else - { - ((IPlotModel)ActualModel).Render(renderContext, new OxyRect(0, 0, canvas.Bounds.Width, canvas.Bounds.Height)); - } - } - } - } - - /// - /// Invokes the specified action on the dispatcher, if necessary. - /// - /// The action. - private static void BeginInvoke(Action action) - { - if (Dispatcher.UIThread.CheckAccess()) - { - action?.Invoke(); - } - else - { - Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Loaded); - } - } - } -} diff --git a/Source/OxyPlot.Avalonia/PlotBase.Export.cs b/Source/OxyPlot.Avalonia/PlotView.Export.cs similarity index 73% rename from Source/OxyPlot.Avalonia/PlotBase.Export.cs rename to Source/OxyPlot.Avalonia/PlotView.Export.cs index a41f100..aac167e 100644 --- a/Source/OxyPlot.Avalonia/PlotBase.Export.cs +++ b/Source/OxyPlot.Avalonia/PlotView.Export.cs @@ -15,7 +15,7 @@ namespace OxyPlot.Avalonia /// /// Represents a control that displays a . /// - public partial class PlotBase + public partial class PlotView { /// /// Saves the PlotView as a bitmap. @@ -23,7 +23,7 @@ public partial class PlotBase /// Stream to which to write the bitmap. public void SaveBitmap(Stream stream) { - SaveBitmap(stream, -1, -1, ActualModel.Background); + this.SaveBitmap(stream, -1, -1, this.ActualModel.Background); } /// @@ -37,20 +37,20 @@ public void SaveBitmap(Stream stream, int width, int height, OxyColor background { if (width <= 0) { - width = (int)Bounds.Width; + width = (int)this.Bounds.Width; } if (height <= 0) { - height = (int)Bounds.Height; + height = (int)this.Bounds.Height; } if (!background.IsVisible()) { - background = Background.ToOxyColor(); + background = this.Background.ToOxyColor(); } - PngExporter.Export(ActualModel, stream, width, height, background); + PngExporter.Export(this.ActualModel, stream, width, height, background); } /// @@ -59,8 +59,8 @@ public void SaveBitmap(Stream stream, int width, int height, OxyColor background /// A bitmap. public Bitmap ToBitmap() { - var background = ActualModel.Background.IsVisible() ? ActualModel.Background : Background.ToOxyColor(); - return PngExporter.ExportToBitmap(ActualModel, (int)Bounds.Width, (int)Bounds.Height, background); + var background = this.ActualModel.Background.IsVisible() ? this.ActualModel.Background : this.Background.ToOxyColor(); + return PngExporter.ExportToBitmap(this.ActualModel, (int)this.Bounds.Width, (int)this.Bounds.Height, background); } } } diff --git a/Source/OxyPlot.Avalonia/PlotView.cs b/Source/OxyPlot.Avalonia/PlotView.cs index 67ec9f2..8fe040e 100644 --- a/Source/OxyPlot.Avalonia/PlotView.cs +++ b/Source/OxyPlot.Avalonia/PlotView.cs @@ -1,5 +1,5 @@ // -------------------------------------------------------------------------------------------------------------------- -// +// // Copyright (c) 2014 OxyPlot contributors // // @@ -8,157 +8,130 @@ // -------------------------------------------------------------------------------------------------------------------- using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Threading; +using System.Threading; namespace OxyPlot.Avalonia { - /// - /// Represents a control that displays a . - /// - public class PlotView : PlotBase + public partial class PlotView : PlotBase { /// - /// Identifies the dependency property. + /// The render context /// - public static readonly StyledProperty ControllerProperty = AvaloniaProperty.Register(nameof(Controller)); + private CanvasRenderContext renderContext; /// - /// Identifies the dependency property. + /// The canvas. /// - public static readonly StyledProperty ModelProperty = AvaloniaProperty.Register(nameof(Model), null); + private Canvas canvas; /// - /// The model lock. + /// Initializes a new instance of the class. /// - private readonly object modelLock = new object(); - - /// - /// The current model (synchronized with the property, but can be accessed from all threads. - /// - private PlotModel currentModel; + public PlotView() + { + this.DisconnectCanvasWhileUpdating = true; + } /// - /// The default plot controller. + /// Gets or sets a value indicating whether to disconnect the canvas while updating. /// - private IPlotController defaultController; + /// true if canvas should be disconnected while updating; otherwise, false. + public bool DisconnectCanvasWhileUpdating { get; set; } - /// - /// Initializes static members of the class. - /// - static PlotView() + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - PaddingProperty.OverrideMetadata(typeof(PlotView), new StyledPropertyMetadata(new Thickness(8))); - ModelProperty.Changed.AddClassHandler(ModelChanged); - PaddingProperty.Changed.AddClassHandler(AppearanceChanged); + base.OnApplyTemplate(e); + this.canvas = new Canvas(); + this.panel.Children.Insert(0, this.canvas); + this.renderContext = new CanvasRenderContext(this.canvas); } - /// - /// Gets or sets the model. - /// - /// The model. - public PlotModel Model + /// + public override void InvalidatePlot(bool updateData = true) { - get - { - return GetValue(ModelProperty); - } - - set + base.InvalidatePlot(updateData); + // do plot update on the UI Thread, but with 'Background' priority, so it doesn't block UI + Dispatcher.UIThread.InvokeAsync(() => { - SetValue(ModelProperty, value); - } + this.UpdatePlot(); + this.InvalidateArrange(); + }, DispatcherPriority.Background); } - /// - /// Gets or sets the Plot controller. - /// - /// The Plot controller. - public IPlotController Controller + /// + protected override Size ArrangeOverride(Size finalSize) { - get + var actualSize = base.ArrangeOverride(finalSize); + if (actualSize.Width > 0 && actualSize.Height > 0) { - return GetValue(ControllerProperty); + this.UpdateVisuals(); } - set - { - SetValue(ControllerProperty, value); - } + return actualSize; } - /// - /// Gets the actual model. - /// - /// The actual model. - public override PlotModel ActualModel + private void UpdateVisuals() { - get + if (this.canvas == null || this.renderContext == null) { - return currentModel; + return; } - } - /// - /// Gets the actual PlotView controller. - /// - /// The actual PlotView controller. - public override IPlotController ActualController - { - get + if (!this.IsEffectivelyVisible) { - return Controller ?? (defaultController ?? (defaultController = new PlotController())); + return; } - } - /// - /// Called when the visual appearance is changed. - /// - protected void OnAppearanceChanged() - { - InvalidatePlot(false); - } - - /// - /// Called when the visual appearance is changed. - /// - /// The d. - /// The instance containing the event data. - private static void AppearanceChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) - { - ((PlotView)d).OnAppearanceChanged(); - } + // Clear the canvas + this.canvas.Children.Clear(); - /// - /// Called when the model is changed. - /// - /// The sender. - /// The instance containing the event data. - private static void ModelChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) - { - ((PlotView)d).OnModelChanged(); - } + if (this.ActualModel?.Background.IsVisible() == true) + { + this.canvas.Background = this.ActualModel.Background.ToBrush(); + } + else + { + this.canvas.Background = null; + } - /// - /// Called when the model is changed. - /// - private void OnModelChanged() - { - lock (modelLock) + if (this.ActualModel is PlotModel plotModel) { - if (currentModel != null) + lock (plotModel.SyncRoot) { - ((IPlotModel)currentModel).AttachPlotView(null); - currentModel = null; - } - - if (Model != null) - { - ((IPlotModel)Model).AttachPlotView(null); // detach so we can re-attach - ((IPlotModel)Model).AttachPlotView(this); - currentModel = Model; + var updateState = Interlocked.Exchange(ref this.isUpdateRequired, 0); + + if (updateState > 0) + { + ((IPlotModel)plotModel).Update(updateState > 1); + } + + if (this.DisconnectCanvasWhileUpdating) + { + // TODO: profile... not sure if this makes any difference + var idx = this.panel.Children.IndexOf(this.canvas); + if (idx != -1) + { + this.panel.Children.RemoveAt(idx); + } + + ((IPlotModel)plotModel).Render(this.renderContext, new OxyRect(0, 0, this.canvas.Bounds.Width, this.canvas.Bounds.Height)); + + // reinsert the canvas again + if (idx != -1) + { + this.panel.Children.Insert(idx, this.canvas); + } + } + else + { + ((IPlotModel)plotModel).Render(this.renderContext, new OxyRect(0, 0, this.canvas.Bounds.Width, this.canvas.Bounds.Height)); + } } } - - InvalidatePlot(); } } } diff --git a/Source/OxyPlot.Avalonia/Themes/Default.axaml b/Source/OxyPlot.Avalonia/Themes/Default.axaml index 9395e97..e894207 100644 --- a/Source/OxyPlot.Avalonia/Themes/Default.axaml +++ b/Source/OxyPlot.Avalonia/Themes/Default.axaml @@ -16,6 +16,7 @@ + diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs new file mode 100644 index 0000000..735997e --- /dev/null +++ b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs @@ -0,0 +1,225 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Skia; +using Avalonia.Threading; +using OxyPlot.Avalonia; +using SkiaSharp; +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; + +namespace OxyPlot.SkiaSharp.Avalonia.DoubleBuffered +{ + public sealed class PlotRenderer(PlotView parent) : Control, IDisposable + { + private readonly SkiaRenderContext renderContext = new(); + private Exception renderException; + private readonly object frontBufferLock = new(); + private SKBitmap frontBuffer; + private SKBitmap backBuffer; + private CancellationTokenSource renderCancellationTokenSource; + private readonly AutoResetEvent renderRequiredEvent = new(false); + private readonly Mutex renderLoopMutex = new(); + + public PlotView PlotView { get; } = parent; + + /// + /// Notifies the that a re-render is required. + /// + public void RequestRender() + { + this.renderRequiredEvent.Set(); + } + + /// + public override void Render(DrawingContext context) + { + if (this.renderException is not null) + { + var exceptionText = new FormattedText( + this.renderException.ToString(), + CultureInfo.CurrentCulture, + CultureInfo.CurrentCulture.TextInfo.IsRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight, + Typeface.Default, + 10, + Brushes.Black); + + context.DrawText(exceptionText, new Point(20, 20)); + return; + } + + using var drawOperation = new DoubleBufferDrawOperation(new Rect(0, 0, this.Bounds.Width, this.Bounds.Height), this); + context.Custom(drawOperation); + } + + private void BufferSwitch() + { + lock (this.frontBufferLock) + { + (this.frontBuffer, this.backBuffer) = (this.backBuffer, this.frontBuffer); + } + } + + /// + /// Makes sure that the backbuffer is initialized with the required size. + /// + /// The size (in device-independent pixels). + /// The DPI scaling factor. + private double EnsureBackBuffer(Size size) + { + var dpiScale = TopLevel.GetTopLevel(this)?.RenderScaling ?? 1; + var width = (int)(size.Width * dpiScale); + var height = (int)(size.Height * dpiScale); + if (this.backBuffer is null || this.backBuffer.Width != width || this.backBuffer.Height != height) + { + this.backBuffer?.Dispose(); + this.backBuffer = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); + } + + return dpiScale; + } + + /// + /// This loop runs until canceled and updates and renders the plot if required. + /// + private void RenderLoop(CancellationToken cancellationToken) + { + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + this.renderRequiredEvent.WaitOne(); + cancellationToken.ThrowIfCancellationRequested(); + var size = this.Bounds.Size; + + if (size.Width > 0 && size.Height > 0 && this.PlotView.ActualModel is PlotModel plotModel) + { + var isUpdateRequired = Interlocked.Exchange(ref this.PlotView.isUpdateRequired, 0); + var iPlotModel = (IPlotModel)plotModel; + if (isUpdateRequired > 0) + { + lock (plotModel.SyncRoot) + { + iPlotModel.Update(isUpdateRequired > 1); + cancellationToken.ThrowIfCancellationRequested(); + this.Render(iPlotModel, size); + } + } + else if (Interlocked.Exchange(ref this.PlotView.isRenderRequired, 0) == 1) + { + lock (plotModel.SyncRoot) + { + this.Render(iPlotModel, size); + } + } + } + } + } + + private void Render(IPlotModel model, Size size) + { + var scale = this.EnsureBackBuffer(size); + + using (var canvas = new SKCanvas(this.backBuffer)) + { + canvas.Scale((float)scale); + this.renderContext.SkCanvas = canvas; + canvas.Clear(model.Background.ToSKColor()); + model.Render(this.renderContext, new OxyRect(0, 0, this.backBuffer.Width / scale, this.backBuffer.Height / scale)); + } + + this.BufferSwitch(); + Dispatcher.UIThread.InvokeAsync(this.InvalidateVisual); + } + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + this.StartRenderLoop(); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + this.StopRenderLoop(); + } + + private void StartRenderLoop() + { + new Task(() => + { + this.renderLoopMutex.WaitOne(); + this.renderException = null; + this.renderCancellationTokenSource = new CancellationTokenSource(); + + try + { + this.RenderLoop(this.renderCancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + } + catch (Exception e) + { + this.renderException = e; + } + finally + { + this.backBuffer?.Dispose(); + this.frontBuffer?.Dispose(); + this.frontBuffer = this.backBuffer = null; + this.renderLoopMutex.ReleaseMutex(); + } + }, TaskCreationOptions.LongRunning).Start(); + } + + private void StopRenderLoop() + { + this.renderCancellationTokenSource?.Cancel(); + this.renderCancellationTokenSource?.Dispose(); + this.renderCancellationTokenSource = null; + } + + /// + public void Dispose() + { + // First stop the render loop + this.StopRenderLoop(); + + // release renderRequiredEvent once more in case the render loop currently is waiting for the mutex; otherwise it might wait forever and will not be canceled + this.RequestRender(); + + // wait until renderLoopMutex is free -> this means the render loop has terminated + this.renderLoopMutex.WaitOne(); + + // the buffers should be disposed at this point, but let's make sure + this.backBuffer?.Dispose(); + this.frontBuffer?.Dispose(); + + this.renderContext.Dispose(); + this.renderLoopMutex.Dispose(); + this.renderRequiredEvent.Dispose(); + + GC.SuppressFinalize(this); + } + + private class DoubleBufferDrawOperation(Rect bounds, PlotRenderer parent) : SkiaDrawOperation(bounds) + { + public PlotRenderer Parent { get; } = parent; + + protected override void Render(SKCanvas canvas) + { + lock (this.Parent.frontBufferLock) + { + if (this.Parent.frontBuffer is not null) + { + canvas.DrawBitmap(this.Parent.frontBuffer, this.Bounds.ToSKRect()); + } + } + } + } + } +} diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotView.cs b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotView.cs new file mode 100644 index 0000000..4ea84b3 --- /dev/null +++ b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotView.cs @@ -0,0 +1,28 @@ +using Avalonia.Controls.Primitives; +using OxyPlot.Avalonia; + +namespace OxyPlot.SkiaSharp.Avalonia.DoubleBuffered +{ + public class PlotView : PlotBase + { + private readonly PlotRenderer plotRenderer; + + public PlotView() + { + this.plotRenderer = new PlotRenderer(this); + } + + public override void InvalidatePlot(bool updateData = true) + { + base.InvalidatePlot(updateData); + // Update is done on the render thread, so it doesn't block the UI Thread + this.plotRenderer.RequestRender(); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + this.panel.Children.Insert(0, plotRenderer); + } + } +} diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/OxyPlot.SkiaSharp.Avalonia.csproj b/Source/OxyPlot.SkiaSharp.Avalonia/OxyPlot.SkiaSharp.Avalonia.csproj new file mode 100644 index 0000000..76115ae --- /dev/null +++ b/Source/OxyPlot.SkiaSharp.Avalonia/OxyPlot.SkiaSharp.Avalonia.csproj @@ -0,0 +1,44 @@ + + + + netstandard2.0;net6;net8 + Oxyplot.SkiaSharp.Avalonia + True + OxyPlot is a plotting library for .NET. This is a support library for OxyPlot to work with AvaloniaUI using the SkiaSharp renderer. + MIT + OxyPlot contributors + http://oxyplot.org/ + OxyPlot_128.png + README.md + plotting plot charting chart + git + https://github.com/oxyplot/oxyplot.git + 2.1.2 + latest + + + + + + + + + + %(Filename) + + + Designer + + + + + + + + + + + + + + diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/OxyPlotModule.cs b/Source/OxyPlot.SkiaSharp.Avalonia/OxyPlotModule.cs new file mode 100644 index 0000000..d45aa31 --- /dev/null +++ b/Source/OxyPlot.SkiaSharp.Avalonia/OxyPlotModule.cs @@ -0,0 +1,14 @@ +using Avalonia.Metadata; + +[assembly: XmlnsDefinition("http://oxyplot.org/avalonia", "OxyPlot.Avalonia")] +[assembly: XmlnsDefinition("http://oxyplot.org/skiasharp/avalonia", "OxyPlot.SkiaSharp.Avalonia")] +[assembly: XmlnsDefinition("http://oxyplot.org/skiasharp/avalonia/doublebuffered", "OxyPlot.SkiaSharp.Avalonia.DoubleBuffered")] +namespace OxyPlot.SkiaSharp.Avalonia +{ + public static class OxyPlotModule + { + public static void EnsureLoaded() + { + } + } +} diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/PlotRenderer.cs b/Source/OxyPlot.SkiaSharp.Avalonia/PlotRenderer.cs new file mode 100644 index 0000000..17680ee --- /dev/null +++ b/Source/OxyPlot.SkiaSharp.Avalonia/PlotRenderer.cs @@ -0,0 +1,57 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using OxyPlot.Avalonia; +using OxyPlot.Avalonia.Extensions; +using SkiaSharp; +using System; + +namespace OxyPlot.SkiaSharp.Avalonia +{ + public class PlotRenderer(PlotView plotView) : Control, IDisposable + { + private readonly SkiaRenderContext renderContext = new(); + + public PlotView PlotView { get; } = plotView; + + public override void Render(DrawingContext context) + { + if (this.PlotView.ActualModel is not PlotModel plotModel || plotModel.Background.IsInvisible()) + { + context.FillRectangle(this.PlotView.Background, this.Bounds); + } + + using var drawOperation = new SkiaPlotDrawOperation(new Rect(0, 0, this.Bounds.Width, this.Bounds.Height), this); + context.Custom(drawOperation); + } + + private class SkiaPlotDrawOperation(Rect bounds, PlotRenderer parent) : SkiaDrawOperation(bounds) + { + public PlotRenderer Parent { get; } = parent; + + protected override void Render(SKCanvas canvas) + { + if (this.Parent.PlotView.ActualModel is PlotModel plotModel) + { + this.Parent.renderContext.SkCanvas = canvas; + + lock (plotModel.SyncRoot) + { + if (plotModel.Background.IsVisible()) + { + canvas.Clear(plotModel.Background.ToSKColor()); + } + + ((IPlotModel)plotModel).Render(this.Parent.renderContext, this.Bounds.ToOxyRect()); + } + } + } + } + + public void Dispose() + { + this.renderContext.Dispose(); + GC.SuppressFinalize(this); + } + } +} diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/PlotView.cs b/Source/OxyPlot.SkiaSharp.Avalonia/PlotView.cs new file mode 100644 index 0000000..1b3f6ef --- /dev/null +++ b/Source/OxyPlot.SkiaSharp.Avalonia/PlotView.cs @@ -0,0 +1,34 @@ +using Avalonia.Controls.Primitives; +using Avalonia.Threading; +using OxyPlot.Avalonia; + +namespace OxyPlot.SkiaSharp.Avalonia +{ + public class PlotView : PlotBase + { + private readonly PlotRenderer plotRenderer; + + public PlotView() + { + this.plotRenderer = new PlotRenderer(this); + } + + public override void InvalidatePlot(bool updateData = true) + { + base.InvalidatePlot(updateData); + + // do plot update on the UI Thread, but with 'Background' priority, so it doesn't block UI + Dispatcher.UIThread.InvokeAsync(() => + { + this.UpdatePlot(); + this.plotRenderer.InvalidateVisual(); + }, DispatcherPriority.Background); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + this.panel.Children.Insert(0, plotRenderer); + } + } +} diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/SkiaDrawOperation.cs b/Source/OxyPlot.SkiaSharp.Avalonia/SkiaDrawOperation.cs new file mode 100644 index 0000000..f80ef3d --- /dev/null +++ b/Source/OxyPlot.SkiaSharp.Avalonia/SkiaDrawOperation.cs @@ -0,0 +1,57 @@ +using Avalonia; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using SkiaSharp; +using System; +using System.Linq; + +namespace OxyPlot.Avalonia +{ + public abstract class SkiaDrawOperation(Rect bounds) : ICustomDrawOperation + { + public Rect Bounds { get; } = bounds; + + private IImmutableGlyphRunReference noSkiaGlyphRun; + private IImmutableGlyphRunReference NoSkiaGlyphRun => this.noSkiaGlyphRun ??= GetNoSkiaGlyphRun(); + protected abstract void Render(SKCanvas canvas); + + private static IImmutableGlyphRunReference GetNoSkiaGlyphRun() + { + var text = "SkiaDrawOperation can only be used if Skia rendering API is used."; + var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.GetGlyph(ch)).ToArray(); + return new GlyphRun(Typeface.Default.GlyphTypeface, 12, text.AsMemory(), glyphs).TryCreateImmutableGlyphRunReference(); + } + + void ICustomDrawOperation.Render(ImmediateDrawingContext context) + { + var skiaSharpLeaseFeature = context.TryGetFeature(); + if (skiaSharpLeaseFeature is null) + { + context.DrawGlyphRun(Brushes.Black, this.NoSkiaGlyphRun); + } + else + { + using var lease = skiaSharpLeaseFeature.Lease(); + this.Render(lease.SkCanvas); + } + } + + void IDisposable.Dispose() + { + // we don't hold any unmanaged resources + GC.SuppressFinalize(this); + } + + bool ICustomDrawOperation.HitTest(Point p) + { + return false; + } + + bool IEquatable.Equals(ICustomDrawOperation other) + { + return false; + } + } +} diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/Themes/Default.axaml b/Source/OxyPlot.SkiaSharp.Avalonia/Themes/Default.axaml new file mode 100644 index 0000000..a6f028b --- /dev/null +++ b/Source/OxyPlot.SkiaSharp.Avalonia/Themes/Default.axaml @@ -0,0 +1,108 @@ + + + + + + + + + \ No newline at end of file From 0c3cc766ef88f43a7f1049c57aa121bde752e543 Mon Sep 17 00:00:00 2001 From: Jonathan Arweck Date: Sun, 11 Aug 2024 16:25:00 +0200 Subject: [PATCH 02/10] make sure isUpdateRequired flag is not overwritten --- Source/OxyPlot.Avalonia.Shared/PlotBase.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/OxyPlot.Avalonia.Shared/PlotBase.cs b/Source/OxyPlot.Avalonia.Shared/PlotBase.cs index 0740f7f..19e4bd7 100644 --- a/Source/OxyPlot.Avalonia.Shared/PlotBase.cs +++ b/Source/OxyPlot.Avalonia.Shared/PlotBase.cs @@ -117,7 +117,15 @@ public void ResetAllAxes() /// The update Data. public virtual void InvalidatePlot(bool updateData = true) { - this.isUpdateRequired = updateData ? 2 : 1; + if (updateData) + { + this.isUpdateRequired = 2; + } + else + { + // only write 1 if current value is 0, so we don't overwrite a 2 + Interlocked.CompareExchange(ref this.isUpdateRequired, 1, 0); + } } /// From feaf9e4b8ce002df9bd0d993b9768bc4e5d1cce4 Mon Sep 17 00:00:00 2001 From: Jonathan Arweck Date: Sun, 11 Aug 2024 16:46:02 +0200 Subject: [PATCH 03/10] fix Category.cs header --- Source/Examples/Avalonia/ExampleBrowser/Category.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Examples/Avalonia/ExampleBrowser/Category.cs b/Source/Examples/Avalonia/ExampleBrowser/Category.cs index ccf879b..1d21b3f 100644 --- a/Source/Examples/Avalonia/ExampleBrowser/Category.cs +++ b/Source/Examples/Avalonia/ExampleBrowser/Category.cs @@ -1,9 +1,9 @@ // -------------------------------------------------------------------------------------------------------------------- -// +// // Copyright (c) 2014 OxyPlot contributors // // -// Represents the view-model for the main window. +// Represents a category of examples. // // -------------------------------------------------------------------------------------------------------------------- From f7a2a8676a4ce782be3bb311cd9823523d3ea4af Mon Sep 17 00:00:00 2001 From: Jonathan Arweck Date: Sun, 11 Aug 2024 16:47:40 +0200 Subject: [PATCH 04/10] remove NotNullBooleanConverter --- .../Avalonia/ExampleBrowser/MainWindow.axaml | 10 +++------- .../ExampleBrowser/NotNullBooleanConverter.cs | 20 ------------------- 2 files changed, 3 insertions(+), 27 deletions(-) delete mode 100644 Source/Examples/Avalonia/ExampleBrowser/NotNullBooleanConverter.cs diff --git a/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml b/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml index f8b3745..e6477ca 100644 --- a/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml +++ b/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml @@ -14,10 +14,6 @@ - - - - @@ -53,9 +49,9 @@ - - - + + + diff --git a/Source/Examples/Avalonia/ExampleBrowser/NotNullBooleanConverter.cs b/Source/Examples/Avalonia/ExampleBrowser/NotNullBooleanConverter.cs deleted file mode 100644 index 1c33742..0000000 --- a/Source/Examples/Avalonia/ExampleBrowser/NotNullBooleanConverter.cs +++ /dev/null @@ -1,20 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright (c) 2014 OxyPlot contributors -// -// -// Represents the view-model for the main window. -// -// -------------------------------------------------------------------------------------------------------------------- - -namespace ExampleBrowser -{ - using Avalonia.Data.Converters; - - public class NotNullBooleanConverter : FuncValueConverter - { - public NotNullBooleanConverter() : base(obj => obj is not null) - { - } - } -} \ No newline at end of file From af951aa62507228eec0178704a0b6e445ce90641 Mon Sep 17 00:00:00 2001 From: Jonathan Arweck Date: Sun, 11 Aug 2024 16:48:20 +0200 Subject: [PATCH 05/10] simplify RowDefinitions --- Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml b/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml index e6477ca..1c9c1f8 100644 --- a/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml +++ b/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml @@ -44,11 +44,7 @@ - - - - - + From ad7e186edd0d11445cb36f28340ca7523c342c22 Mon Sep 17 00:00:00 2001 From: Jonathan Arweck Date: Sun, 11 Aug 2024 16:54:05 +0200 Subject: [PATCH 06/10] fix typo --- Source/OxyPlot.Avalonia.Shared/PlotBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/OxyPlot.Avalonia.Shared/PlotBase.cs b/Source/OxyPlot.Avalonia.Shared/PlotBase.cs index 19e4bd7..fc05c36 100644 --- a/Source/OxyPlot.Avalonia.Shared/PlotBase.cs +++ b/Source/OxyPlot.Avalonia.Shared/PlotBase.cs @@ -33,7 +33,7 @@ public abstract partial class PlotBase : TemplatedControl, IPlotView protected const string PartPanel = "PART_Panel"; /// - /// Invalidation flag (0: no update, 1: update, 2: update date). + /// Invalidation flag (0: no update, 1: update, 2: update data). /// protected internal int isUpdateRequired; From ceecb6e105f77bf1e87f42ee5369a477d5ece912 Mon Sep 17 00:00:00 2001 From: Jonathan Arweck Date: Mon, 23 Sep 2024 17:38:39 +0200 Subject: [PATCH 07/10] replace AutoResetEvent and Mutex by SemaphoreSlim for async waiting --- .../DoubleBuffered/PlotRenderer.cs | 110 ++++++++++-------- 1 file changed, 62 insertions(+), 48 deletions(-) diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs index 735997e..e3cc214 100644 --- a/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs +++ b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs @@ -20,8 +20,8 @@ public sealed class PlotRenderer(PlotView parent) : Control, IDisposable private SKBitmap frontBuffer; private SKBitmap backBuffer; private CancellationTokenSource renderCancellationTokenSource; - private readonly AutoResetEvent renderRequiredEvent = new(false); - private readonly Mutex renderLoopMutex = new(); + private readonly SemaphoreSlim renderRequiredEvent = new(0); + private readonly SemaphoreSlim renderLoopMutex = new(1, 1); public PlotView PlotView { get; } = parent; @@ -30,7 +30,7 @@ public sealed class PlotRenderer(PlotView parent) : Control, IDisposable /// public void RequestRender() { - this.renderRequiredEvent.Set(); + this.renderRequiredEvent.Release(); } /// @@ -84,35 +84,43 @@ private double EnsureBackBuffer(Size size) /// /// This loop runs until canceled and updates and renders the plot if required. /// - private void RenderLoop(CancellationToken cancellationToken) + private async Task RenderLoopAsync(CancellationToken cancellationToken) { while (true) { - cancellationToken.ThrowIfCancellationRequested(); - this.renderRequiredEvent.WaitOne(); - cancellationToken.ThrowIfCancellationRequested(); + await this.renderRequiredEvent.WaitAsync(cancellationToken); + while (this.renderRequiredEvent.CurrentCount > 0) + { + await this.renderRequiredEvent.WaitAsync(cancellationToken); + } + var size = this.Bounds.Size; if (size.Width > 0 && size.Height > 0 && this.PlotView.ActualModel is PlotModel plotModel) { var isUpdateRequired = Interlocked.Exchange(ref this.PlotView.isUpdateRequired, 0); var iPlotModel = (IPlotModel)plotModel; - if (isUpdateRequired > 0) + + // plot update and render might be CPU-intensive, so run it on a background thread + await Task.Run(() => { - lock (plotModel.SyncRoot) + if (isUpdateRequired > 0) { - iPlotModel.Update(isUpdateRequired > 1); - cancellationToken.ThrowIfCancellationRequested(); - this.Render(iPlotModel, size); + lock (plotModel.SyncRoot) + { + iPlotModel.Update(isUpdateRequired > 1); + cancellationToken.ThrowIfCancellationRequested(); + this.Render(iPlotModel, size); + } } - } - else if (Interlocked.Exchange(ref this.PlotView.isRenderRequired, 0) == 1) - { - lock (plotModel.SyncRoot) + else if (Interlocked.Exchange(ref this.PlotView.isRenderRequired, 0) == 1) { - this.Render(iPlotModel, size); + lock (plotModel.SyncRoot) + { + this.Render(iPlotModel, size); + } } - } + }, cancellationToken); } } } @@ -137,7 +145,19 @@ private void Render(IPlotModel model, Size size) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - this.StartRenderLoop(); + WaitAndRethrow(this.StartRenderLoopAsync()); + } + + private static async void WaitAndRethrow(Task task) + { + try + { + await task; + } + catch (Exception) + { + throw; + } } /// @@ -147,33 +167,30 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e this.StopRenderLoop(); } - private void StartRenderLoop() + private async Task StartRenderLoopAsync() { - new Task(() => - { - this.renderLoopMutex.WaitOne(); - this.renderException = null; - this.renderCancellationTokenSource = new CancellationTokenSource(); + await this.renderLoopMutex.WaitAsync(); + this.renderException = null; + this.renderCancellationTokenSource = new CancellationTokenSource(); - try - { - this.RenderLoop(this.renderCancellationTokenSource.Token); - } - catch (OperationCanceledException) - { - } - catch (Exception e) - { - this.renderException = e; - } - finally - { - this.backBuffer?.Dispose(); - this.frontBuffer?.Dispose(); - this.frontBuffer = this.backBuffer = null; - this.renderLoopMutex.ReleaseMutex(); - } - }, TaskCreationOptions.LongRunning).Start(); + try + { + await this.RenderLoopAsync(this.renderCancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + } + catch (Exception e) + { + this.renderException = e; + } + finally + { + this.backBuffer?.Dispose(); + this.frontBuffer?.Dispose(); + this.frontBuffer = this.backBuffer = null; + this.renderLoopMutex.Release(); + } } private void StopRenderLoop() @@ -189,11 +206,8 @@ public void Dispose() // First stop the render loop this.StopRenderLoop(); - // release renderRequiredEvent once more in case the render loop currently is waiting for the mutex; otherwise it might wait forever and will not be canceled - this.RequestRender(); - // wait until renderLoopMutex is free -> this means the render loop has terminated - this.renderLoopMutex.WaitOne(); + this.renderLoopMutex.Wait(); // the buffers should be disposed at this point, but let's make sure this.backBuffer?.Dispose(); From 0f0c14981ec29a061f9f940d0c54e1729fd3eeed Mon Sep 17 00:00:00 2001 From: Jonathan Arweck Date: Sun, 29 Sep 2024 07:12:11 +0200 Subject: [PATCH 08/10] Review usages of isRenderRequired and isUpdateRequired flags --- Source/OxyPlot.Avalonia.Shared/PlotBase.cs | 2 +- Source/OxyPlot.Avalonia/PlotView.cs | 16 +++++++++---- .../DoubleBuffered/PlotRenderer.cs | 24 +++++++------------ .../DoubleBuffered/PlotView.cs | 3 ++- .../PlotRenderer.cs | 1 + Source/OxyPlot.SkiaSharp.Avalonia/PlotView.cs | 13 +++++++--- 6 files changed, 34 insertions(+), 25 deletions(-) diff --git a/Source/OxyPlot.Avalonia.Shared/PlotBase.cs b/Source/OxyPlot.Avalonia.Shared/PlotBase.cs index fc05c36..5f90a93 100644 --- a/Source/OxyPlot.Avalonia.Shared/PlotBase.cs +++ b/Source/OxyPlot.Avalonia.Shared/PlotBase.cs @@ -213,7 +213,7 @@ public async void SetClipboardText(string text) } } - protected void UpdatePlot() + protected void UpdatePlotIfRequired() { if (this.ActualModel is PlotModel plotModel) { diff --git a/Source/OxyPlot.Avalonia/PlotView.cs b/Source/OxyPlot.Avalonia/PlotView.cs index 8fe040e..af4e73b 100644 --- a/Source/OxyPlot.Avalonia/PlotView.cs +++ b/Source/OxyPlot.Avalonia/PlotView.cs @@ -54,11 +54,16 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) public override void InvalidatePlot(bool updateData = true) { base.InvalidatePlot(updateData); - // do plot update on the UI Thread, but with 'Background' priority, so it doesn't block UI + // do plot update on the UI Thread Dispatcher.UIThread.InvokeAsync(() => { - this.UpdatePlot(); - this.InvalidateArrange(); + this.UpdatePlotIfRequired(); + + // this check prevents us from calling InvalidateArrange multiple times when the plot is invalidated in quick succession + if (Interlocked.Exchange(ref this.isRenderRequired, 1) == 0) + { + this.InvalidateArrange(); + } }, DispatcherPriority.Background); } @@ -68,7 +73,10 @@ protected override Size ArrangeOverride(Size finalSize) var actualSize = base.ArrangeOverride(finalSize); if (actualSize.Width > 0 && actualSize.Height > 0) { - this.UpdateVisuals(); + if (Interlocked.Exchange(ref this.isRenderRequired, 0) == 1) + { + this.UpdateVisuals(); + } } return actualSize; diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs index e3cc214..6fc9f83 100644 --- a/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs +++ b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs @@ -96,31 +96,23 @@ private async Task RenderLoopAsync(CancellationToken cancellationToken) var size = this.Bounds.Size; - if (size.Width > 0 && size.Height > 0 && this.PlotView.ActualModel is PlotModel plotModel) - { - var isUpdateRequired = Interlocked.Exchange(ref this.PlotView.isUpdateRequired, 0); - var iPlotModel = (IPlotModel)plotModel; + if (size.Width > 0 && size.Height > 0 && this.PlotView.ActualModel is PlotModel plotModel) { - // plot update and render might be CPU-intensive, so run it on a background thread - await Task.Run(() => + var isUpdateRequired = Interlocked.Exchange(ref this.PlotView.isUpdateRequired, 0); + if (isUpdateRequired > 0) { - if (isUpdateRequired > 0) + // plot update and render might be CPU-intensive, so run it on a background thread + await Task.Run(() => { lock (plotModel.SyncRoot) { + var iPlotModel = (IPlotModel)plotModel; iPlotModel.Update(isUpdateRequired > 1); cancellationToken.ThrowIfCancellationRequested(); this.Render(iPlotModel, size); } - } - else if (Interlocked.Exchange(ref this.PlotView.isRenderRequired, 0) == 1) - { - lock (plotModel.SyncRoot) - { - this.Render(iPlotModel, size); - } - } - }, cancellationToken); + }, cancellationToken); + } } } } diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotView.cs b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotView.cs index 4ea84b3..1f5e3d6 100644 --- a/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotView.cs +++ b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotView.cs @@ -15,7 +15,8 @@ public PlotView() public override void InvalidatePlot(bool updateData = true) { base.InvalidatePlot(updateData); - // Update is done on the render thread, so it doesn't block the UI Thread + // At this point we only notify PlotRenderer that an update needs to be done. + // Plot update and/or rendering will be done by PlotRenderer on a background thread as necessary. this.plotRenderer.RequestRender(); } diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/PlotRenderer.cs b/Source/OxyPlot.SkiaSharp.Avalonia/PlotRenderer.cs index 17680ee..b15ad53 100644 --- a/Source/OxyPlot.SkiaSharp.Avalonia/PlotRenderer.cs +++ b/Source/OxyPlot.SkiaSharp.Avalonia/PlotRenderer.cs @@ -42,6 +42,7 @@ protected override void Render(SKCanvas canvas) canvas.Clear(plotModel.Background.ToSKColor()); } + this.Parent.PlotView.isRenderRequired = 0; ((IPlotModel)plotModel).Render(this.Parent.renderContext, this.Bounds.ToOxyRect()); } } diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/PlotView.cs b/Source/OxyPlot.SkiaSharp.Avalonia/PlotView.cs index 1b3f6ef..2a4025e 100644 --- a/Source/OxyPlot.SkiaSharp.Avalonia/PlotView.cs +++ b/Source/OxyPlot.SkiaSharp.Avalonia/PlotView.cs @@ -1,6 +1,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Threading; using OxyPlot.Avalonia; +using System.Threading; namespace OxyPlot.SkiaSharp.Avalonia { @@ -17,11 +18,17 @@ public override void InvalidatePlot(bool updateData = true) { base.InvalidatePlot(updateData); - // do plot update on the UI Thread, but with 'Background' priority, so it doesn't block UI + // do plot update on the UI Thread Dispatcher.UIThread.InvokeAsync(() => { - this.UpdatePlot(); - this.plotRenderer.InvalidateVisual(); + this.UpdatePlotIfRequired(); + // this check prevents us from calling InvalidateVisual multiple times when the plot is invalidated in quick succession + // InvalidateVisual will eventually cause PlotRenderer.Render to be executed + if (Interlocked.Exchange(ref this.isRenderRequired, 1) == 0) + { + this.plotRenderer.InvalidateVisual(); + } + }, DispatcherPriority.Background); } From 44c3fa9d9c22ea50e2808a5864764c11acbd2459 Mon Sep 17 00:00:00 2001 From: Jonathan Arweck Date: Sun, 29 Sep 2024 07:36:34 +0200 Subject: [PATCH 09/10] remove misleading function --- .../DoubleBuffered/PlotRenderer.cs | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs index 6fc9f83..83616bf 100644 --- a/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs +++ b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs @@ -7,6 +7,7 @@ using SkiaSharp; using System; using System.Globalization; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -96,8 +97,8 @@ private async Task RenderLoopAsync(CancellationToken cancellationToken) var size = this.Bounds.Size; - if (size.Width > 0 && size.Height > 0 && this.PlotView.ActualModel is PlotModel plotModel) { - + if (size.Width > 0 && size.Height > 0 && this.PlotView.ActualModel is PlotModel plotModel) + { var isUpdateRequired = Interlocked.Exchange(ref this.PlotView.isUpdateRequired, 0); if (isUpdateRequired > 0) { @@ -137,19 +138,10 @@ private void Render(IPlotModel model, Size size) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - WaitAndRethrow(this.StartRenderLoopAsync()); - } - private static async void WaitAndRethrow(Task task) - { - try - { - await task; - } - catch (Exception) - { - throw; - } + // We deliberately just 'fire and forget' the render loop. It will run forever until canceled via renderCancellationTokenSource. + // Potential exceptions are stored in renderException. + _ = this.StartRenderLoopAsync(); } /// From 1b044fe20c3379005590fc810ee3988d94825483 Mon Sep 17 00:00:00 2001 From: Jonathan Arweck Date: Mon, 11 Nov 2024 18:15:16 +0100 Subject: [PATCH 10/10] implement PictureRecorder-based PlotView --- .../Avalonia/ExampleBrowser/MainViewModel.cs | 3 + .../Avalonia/ExampleBrowser/MainWindow.axaml | 2 + .../Avalonia/ExampleBrowser/Renderer.cs | 1 + .../OxyPlotModule.cs | 1 + .../PictureRecorder/PlotRenderer.cs | 187 ++++++++++++++++++ .../PictureRecorder/PlotView.cs | 29 +++ .../Themes/Default.axaml | 33 ++++ 7 files changed, 256 insertions(+) create mode 100644 Source/OxyPlot.SkiaSharp.Avalonia/PictureRecorder/PlotRenderer.cs create mode 100644 Source/OxyPlot.SkiaSharp.Avalonia/PictureRecorder/PlotView.cs diff --git a/Source/Examples/Avalonia/ExampleBrowser/MainViewModel.cs b/Source/Examples/Avalonia/ExampleBrowser/MainViewModel.cs index fb13315..8015850 100644 --- a/Source/Examples/Avalonia/ExampleBrowser/MainViewModel.cs +++ b/Source/Examples/Avalonia/ExampleBrowser/MainViewModel.cs @@ -83,6 +83,7 @@ public ExampleInfo Example public PlotModel SkiaSharpModel => this.SelectedRenderer == Renderer.SkiaSharp ? this.model : null; public PlotModel SkiaSharpDoubleBufferedModel => this.SelectedRenderer == Renderer.SkiaSharpDoubleBuffered ? this.model : null; + public PlotModel SkiaSharpPictureRecorderModel => this.SelectedRenderer == Renderer.SkiaSharpRecorder ? this.model : null; public void ChangeExample(ExampleInfo example) { @@ -97,6 +98,7 @@ private void CoerceSelectedRenderer() this.OnPropertyChanged(nameof(this.CanvasModel)); this.OnPropertyChanged(nameof(this.SkiaSharpModel)); this.OnPropertyChanged(nameof(this.SkiaSharpDoubleBufferedModel)); + this.OnPropertyChanged(nameof(this.SkiaSharpPictureRecorderModel)); } private void CoerceExample() @@ -106,6 +108,7 @@ private void CoerceExample() this.OnPropertyChanged(nameof(this.CanvasModel)); this.OnPropertyChanged(nameof(this.SkiaSharpModel)); this.OnPropertyChanged(nameof(this.SkiaSharpDoubleBufferedModel)); + this.OnPropertyChanged(nameof(this.SkiaSharpPictureRecorderModel)); } private void OnPropertyChanged(string propertyName) diff --git a/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml b/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml index 1c9c1f8..7538d32 100644 --- a/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml +++ b/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml @@ -5,6 +5,7 @@ xmlns:oxy="http://oxyplot.org/avalonia" xmlns:oxySkia="http://oxyplot.org/skiasharp/avalonia" xmlns:oxySkiaDb="http://oxyplot.org/skiasharp/avalonia/doublebuffered" + xmlns:oxySkiaPr="http://oxyplot.org/skiasharp/avalonia/picturerecorder" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" xmlns:exampleBrowser="clr-namespace:ExampleBrowser;assembly=ExampleBrowser" xmlns:exampleLibrary="clr-namespace:ExampleLibrary;assembly=ExampleLibrary" @@ -48,6 +49,7 @@ + diff --git a/Source/Examples/Avalonia/ExampleBrowser/Renderer.cs b/Source/Examples/Avalonia/ExampleBrowser/Renderer.cs index 8dca72b..e6f8ba7 100644 --- a/Source/Examples/Avalonia/ExampleBrowser/Renderer.cs +++ b/Source/Examples/Avalonia/ExampleBrowser/Renderer.cs @@ -5,5 +5,6 @@ public enum Renderer Canvas, SkiaSharp, SkiaSharpDoubleBuffered, + SkiaSharpRecorder, } } diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/OxyPlotModule.cs b/Source/OxyPlot.SkiaSharp.Avalonia/OxyPlotModule.cs index d45aa31..2c98b28 100644 --- a/Source/OxyPlot.SkiaSharp.Avalonia/OxyPlotModule.cs +++ b/Source/OxyPlot.SkiaSharp.Avalonia/OxyPlotModule.cs @@ -3,6 +3,7 @@ [assembly: XmlnsDefinition("http://oxyplot.org/avalonia", "OxyPlot.Avalonia")] [assembly: XmlnsDefinition("http://oxyplot.org/skiasharp/avalonia", "OxyPlot.SkiaSharp.Avalonia")] [assembly: XmlnsDefinition("http://oxyplot.org/skiasharp/avalonia/doublebuffered", "OxyPlot.SkiaSharp.Avalonia.DoubleBuffered")] +[assembly: XmlnsDefinition("http://oxyplot.org/skiasharp/avalonia/picturerecorder", "OxyPlot.SkiaSharp.Avalonia.PictureRecorder")] namespace OxyPlot.SkiaSharp.Avalonia { public static class OxyPlotModule diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/PictureRecorder/PlotRenderer.cs b/Source/OxyPlot.SkiaSharp.Avalonia/PictureRecorder/PlotRenderer.cs new file mode 100644 index 0000000..b4d5547 --- /dev/null +++ b/Source/OxyPlot.SkiaSharp.Avalonia/PictureRecorder/PlotRenderer.cs @@ -0,0 +1,187 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Skia; +using Avalonia.Threading; +using OxyPlot.Avalonia; +using SkiaSharp; +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; + +namespace OxyPlot.SkiaSharp.Avalonia.PictureRecorder +{ + public sealed class PlotRenderer(PlotView parent) : Control, IDisposable + { + private readonly SkiaRenderContext renderContext = new(); + private Exception renderException; + private SKPicture currentPicture; + private CancellationTokenSource renderCancellationTokenSource; + private readonly SemaphoreSlim renderRequiredEvent = new(0); + private readonly SemaphoreSlim renderLoopMutex = new(1, 1); + + public PlotView PlotView { get; } = parent; + + /// + /// Notifies the that a re-render is required. + /// + public void RequestRender() + { + this.renderRequiredEvent.Release(); + } + + /// + public override void Render(DrawingContext context) + { + if (this.renderException is not null) + { + var exceptionText = new FormattedText( + this.renderException.ToString(), + CultureInfo.CurrentCulture, + CultureInfo.CurrentCulture.TextInfo.IsRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight, + Typeface.Default, + 10, + Brushes.Black); + + context.DrawText(exceptionText, new Point(20, 20)); + return; + } + + using var drawOperation = new SKPictureDrawOperation(new Rect(0, 0, this.Bounds.Width, this.Bounds.Height), this); + context.Custom(drawOperation); + } + + /// + /// This loop runs until canceled and updates and renders the plot if required. + /// + private async Task RenderLoopAsync(CancellationToken cancellationToken) + { + while (true) + { + await this.renderRequiredEvent.WaitAsync(cancellationToken); + while (this.renderRequiredEvent.CurrentCount > 0) + { + await this.renderRequiredEvent.WaitAsync(cancellationToken); + } + + var size = this.Bounds.Size; + + if (size.Width > 0 && size.Height > 0 && this.PlotView.ActualModel is PlotModel plotModel) + { + var isUpdateRequired = Interlocked.Exchange(ref this.PlotView.isUpdateRequired, 0); + if (isUpdateRequired > 0) + { + // plot update and render might be CPU-intensive, so run it on a background thread + await Task.Run(() => + { + lock (plotModel.SyncRoot) + { + var iPlotModel = (IPlotModel)plotModel; + iPlotModel.Update(isUpdateRequired > 1); + cancellationToken.ThrowIfCancellationRequested(); + this.Render(iPlotModel, size); + } + }, cancellationToken); + } + } + } + } + + private void Render(IPlotModel model, Size size) + { + using var recorder = new SKPictureRecorder(); + var rect = new Rect(size); + using var canvas = recorder.BeginRecording(rect.ToSKRect()); + + this.renderContext.SkCanvas = canvas; + + if (model.Background.IsVisible()) + { + canvas.Clear(model.Background.ToSKColor()); + } + + model.Render(this.renderContext, new OxyRect(0, 0, size.Width, size.Height)); + + // TODO Somehow dispose of old image + this.currentPicture = recorder.EndRecording(); + Dispatcher.UIThread.InvokeAsync(this.InvalidateVisual); + } + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + // We deliberately just 'fire and forget' the render loop. It will run forever until canceled via renderCancellationTokenSource. + // Potential exceptions are stored in renderException. + _ = this.StartRenderLoopAsync(); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + this.StopRenderLoop(); + } + + private async Task StartRenderLoopAsync() + { + await this.renderLoopMutex.WaitAsync(); + this.renderException = null; + this.renderCancellationTokenSource = new CancellationTokenSource(); + + try + { + await this.RenderLoopAsync(this.renderCancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + } + catch (Exception e) + { + this.renderException = e; + } + finally + { + this.renderLoopMutex.Release(); + } + } + + private void StopRenderLoop() + { + this.renderCancellationTokenSource?.Cancel(); + this.renderCancellationTokenSource?.Dispose(); + this.renderCancellationTokenSource = null; + } + + /// + public void Dispose() + { + // First stop the render loop + this.StopRenderLoop(); + + // wait until renderLoopMutex is free -> this means the render loop has terminated + this.renderLoopMutex.Wait(); + + this.renderContext.Dispose(); + this.renderLoopMutex.Dispose(); + this.renderRequiredEvent.Dispose(); + + GC.SuppressFinalize(this); + } + + private class SKPictureDrawOperation(Rect bounds, PlotRenderer parent) : SkiaDrawOperation(bounds) + { + public PlotRenderer Parent { get; } = parent; + + protected override void Render(SKCanvas canvas) + { + if (this.Parent.currentPicture is not null) + { + canvas.DrawPicture(this.Parent.currentPicture); + } + } + } + } +} diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/PictureRecorder/PlotView.cs b/Source/OxyPlot.SkiaSharp.Avalonia/PictureRecorder/PlotView.cs new file mode 100644 index 0000000..76891a5 --- /dev/null +++ b/Source/OxyPlot.SkiaSharp.Avalonia/PictureRecorder/PlotView.cs @@ -0,0 +1,29 @@ +using Avalonia.Controls.Primitives; +using OxyPlot.Avalonia; + +namespace OxyPlot.SkiaSharp.Avalonia.PictureRecorder +{ + public class PlotView : PlotBase + { + private readonly PlotRenderer plotRenderer; + + public PlotView() + { + this.plotRenderer = new PlotRenderer(this); + } + + public override void InvalidatePlot(bool updateData = true) + { + base.InvalidatePlot(updateData); + // At this point we only notify PlotRenderer that an update needs to be done. + // Plot update and/or rendering will be done by PlotRenderer on a background thread as necessary. + this.plotRenderer.RequestRender(); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + this.panel.Children.Insert(0, plotRenderer); + } + } +} diff --git a/Source/OxyPlot.SkiaSharp.Avalonia/Themes/Default.axaml b/Source/OxyPlot.SkiaSharp.Avalonia/Themes/Default.axaml index a6f028b..9d11f6b 100644 --- a/Source/OxyPlot.SkiaSharp.Avalonia/Themes/Default.axaml +++ b/Source/OxyPlot.SkiaSharp.Avalonia/Themes/Default.axaml @@ -1,5 +1,6 @@ + + + \ No newline at end of file