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..1d21b3f
--- /dev/null
+++ b/Source/Examples/Avalonia/ExampleBrowser/Category.cs
@@ -0,0 +1,27 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) 2014 OxyPlot contributors
+//
+//
+// Represents a category of examples.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+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..8015850 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,50 @@ 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 PlotModel SkiaSharpPictureRecorderModel => this.SelectedRenderer == Renderer.SkiaSharpRecorder ? 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));
+ this.OnPropertyChanged(nameof(this.SkiaSharpPictureRecorderModel));
+ }
+
+ 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));
+ this.OnPropertyChanged(nameof(this.SkiaSharpPictureRecorderModel));
+ }
+
+ 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..7538d32 100644
--- a/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml
+++ b/Source/Examples/Avalonia/ExampleBrowser/MainWindow.axaml
@@ -2,7 +2,10 @@
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"
+ 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"
@@ -11,7 +14,7 @@
-
+
@@ -42,6 +45,15 @@
-
+
+
+
+
+
+
+
+
+
+
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..e6f8ba7
--- /dev/null
+++ b/Source/Examples/Avalonia/ExampleBrowser/Renderer.cs
@@ -0,0 +1,10 @@
+namespace ExampleBrowser
+{
+ public enum Renderer
+ {
+ Canvas,
+ SkiaSharp,
+ SkiaSharpDoubleBuffered,
+ SkiaSharpRecorder,
+ }
+}
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..5f90a93
--- /dev/null
+++ b/Source/OxyPlot.Avalonia.Shared/PlotBase.cs
@@ -0,0 +1,258 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// 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 data).
+ ///
+ 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)
+ {
+ 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);
+ }
+ }
+
+ ///
+ /// 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 UpdatePlotIfRequired()
+ {
+ 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
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..af4e73b 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,138 @@
// --------------------------------------------------------------------------------------------------------------------
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
+ base.InvalidatePlot(updateData);
+ // do plot update on the UI Thread
+ Dispatcher.UIThread.InvokeAsync(() =>
{
- return GetValue(ModelProperty);
- }
+ this.UpdatePlotIfRequired();
- set
- {
- SetValue(ModelProperty, value);
- }
+ // 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);
}
- ///
- /// 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);
+ if (Interlocked.Exchange(ref this.isRenderRequired, 0) == 1)
+ {
+ 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);
- }
+ // Clear the canvas
+ this.canvas.Children.Clear();
- ///
- /// 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();
- }
-
- ///
- /// 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..83616bf
--- /dev/null
+++ b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotRenderer.cs
@@ -0,0 +1,223 @@
+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.Runtime.CompilerServices;
+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 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 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 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)
+ {
+ 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);
+
+ // 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.backBuffer?.Dispose();
+ this.frontBuffer?.Dispose();
+ this.frontBuffer = this.backBuffer = null;
+ 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();
+
+ // 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..1f5e3d6
--- /dev/null
+++ b/Source/OxyPlot.SkiaSharp.Avalonia/DoubleBuffered/PlotView.cs
@@ -0,0 +1,29 @@
+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);
+ // 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/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..2c98b28
--- /dev/null
+++ b/Source/OxyPlot.SkiaSharp.Avalonia/OxyPlotModule.cs
@@ -0,0 +1,15 @@
+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")]
+[assembly: XmlnsDefinition("http://oxyplot.org/skiasharp/avalonia/picturerecorder", "OxyPlot.SkiaSharp.Avalonia.PictureRecorder")]
+namespace OxyPlot.SkiaSharp.Avalonia
+{
+ public static class OxyPlotModule
+ {
+ public static void EnsureLoaded()
+ {
+ }
+ }
+}
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/PlotRenderer.cs b/Source/OxyPlot.SkiaSharp.Avalonia/PlotRenderer.cs
new file mode 100644
index 0000000..b15ad53
--- /dev/null
+++ b/Source/OxyPlot.SkiaSharp.Avalonia/PlotRenderer.cs
@@ -0,0 +1,58 @@
+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());
+ }
+
+ this.Parent.PlotView.isRenderRequired = 0;
+ ((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..2a4025e
--- /dev/null
+++ b/Source/OxyPlot.SkiaSharp.Avalonia/PlotView.cs
@@ -0,0 +1,41 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Threading;
+using OxyPlot.Avalonia;
+using System.Threading;
+
+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
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ 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);
+ }
+
+ 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..9d11f6b
--- /dev/null
+++ b/Source/OxyPlot.SkiaSharp.Avalonia/Themes/Default.axaml
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file