From 9250ccb18afb5d538b6e300a05c606da2d38d3a6 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 8 Oct 2022 13:35:13 -0400 Subject: [PATCH 01/57] Add missing base constructor call --- .../ColorSpectrum/ColorSpectrum.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index bd44161a42..200a652d3b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives /// /// Initializes a new instance of the class. /// - public ColorSpectrum() + public ColorSpectrum() : base() { _shapeFromLastBitmapCreation = Shape; _componentsFromLastBitmapCreation = Components; From 5c3a56a9533eab9060a216831df2a9a8c5e08e12 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 8 Oct 2022 14:25:02 -0400 Subject: [PATCH 02/57] Fix the color spectrum selection ellipse position Specifically, when updated and not part of the visual tree --- .../ColorSpectrum/ColorSpectrum.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 200a652d3b..9261e5892d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -171,6 +171,18 @@ namespace Avalonia.Controls.Primitives { base.OnAttachedToVisualTree(e); + // If the color was updated while this ColorSpectrum was not part of the visual tree, + // the selection ellipse may be in an incorrect position. This is because the spectrum + // renders based on layout scaling to avoid color banding; however, layout scale is only + // available when the control is attached to the visual tree. The ColorSpectrum's color + // may be updated from code-behind or from binding with another control when it's not + // part of the visual tree. + // + // See discussion: https://github.com/AvaloniaUI/Avalonia/discussions/9077 + // + // To work-around this issue the selection ellipse is refreshed here. + UpdateEllipse(); + // OnAttachedToVisualTree is called after OnApplyTemplate so events cannot be connected here } @@ -832,6 +844,8 @@ namespace Avalonia.Controls.Primitives } // Remember the bitmap size follows physical device pixels + // Warning: LayoutHelper.GetLayoutScale() doesn't work unless the control is visible + // This will not be true in all cases if the color is updated from another control or code-behind var scale = LayoutHelper.GetLayoutScale(this); Canvas.SetLeft(_selectionEllipsePanel, (xPosition / scale) - (_selectionEllipsePanel.Width / 2)); Canvas.SetTop(_selectionEllipsePanel, (yPosition / scale) - (_selectionEllipsePanel.Height / 2)); From 55e27213f958304679d449ef4aeb25ca9e8743ea Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 8 Oct 2022 14:25:26 -0400 Subject: [PATCH 03/57] Comment additional ColorSpectrum methods --- .../ColorSpectrum/ColorSpectrum.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 9261e5892d..f272ce850e 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -600,6 +600,10 @@ namespace Avalonia.Controls.Primitives RaiseColorChanged(); } + /// + /// Updates the selected and based on a point within the color spectrum. + /// + /// The point on the spectrum representing the color. private void UpdateColorFromPoint(PointerPoint point) { // If we haven't initialized our HSV value array yet, then we should just ignore any user input - @@ -676,6 +680,9 @@ namespace Avalonia.Controls.Primitives UpdateColor(hsvAtPoint); } + /// + /// Updates the position of the selection ellipse on the spectrum which indicates the selected color. + /// private void UpdateEllipse() { if (_selectionEllipsePanel == null) From 8b070919398878acda991efc0140f7a02cd2a8b7 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 9 Oct 2022 12:21:08 -0400 Subject: [PATCH 04/57] Fix formatting --- .../Themes/Fluent/ColorView.xaml | 30 +++++++++---------- .../Themes/Simple/ColorView.xaml | 30 +++++++++---------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 16bc2acdd1..cf836aff4a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -241,23 +241,21 @@ - + - + - + - + Date: Sun, 2 Oct 2022 11:51:25 -0400 Subject: [PATCH 05/57] Implement WriteableBitmap caching/reuse and disposal in ColorSlider --- .../ColorSlider/ColorSlider.cs | 42 ++++++++++++++++++- .../Helpers/ColorPickerHelpers.cs | 19 ++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index b662d20223..4e907f610d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -2,6 +2,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Media.Imaging; using Avalonia.Utilities; namespace Avalonia.Controls.Primitives @@ -31,6 +32,8 @@ namespace Avalonia.Controls.Primitives protected bool ignorePropertyChanged = false; + private WriteableBitmap? _backgroundBitmap; + /// /// Initializes a new instance of the class. /// @@ -38,6 +41,26 @@ namespace Avalonia.Controls.Primitives { } + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + // Bitmaps were released when detached from the visual tree so they must be re-built + UpdateBackground(); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + // Clean-up all bitmaps + // https://github.com/AvaloniaUI/Avalonia/issues/9051 + _backgroundBitmap?.Dispose(); + _backgroundBitmap = null; + } + /// /// Updates the visual state of the control by applying latest PseudoClasses. /// @@ -110,7 +133,19 @@ namespace Avalonia.Controls.Primitives if (bitmap != null) { - Background = new ImageBrush(ColorPickerHelpers.CreateBitmapFromPixelData(bitmap, pixelWidth, pixelHeight)); + if (_backgroundBitmap != null) + { + // Re-use the existing WriteableBitmap + // This assumes the height, width and byte counts are the same and must be set to null + // elsewhere if that assumption is ever not true. + ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bitmap); + } + else + { + _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bitmap, pixelWidth, pixelHeight); + } + + Background = new ImageBrush(_backgroundBitmap); } } } @@ -399,6 +434,11 @@ namespace Avalonia.Controls.Primitives } else if (change.Property == BoundsProperty) { + // If the control's overall dimensions have changed the background bitmap size also needs to change. + // This means the existing bitmap must be released to be recreated correctly in UpdateBackground(). + _backgroundBitmap?.Dispose(); + _backgroundBitmap = null; + UpdateBackground(); } else if (change.Property == ValueProperty || diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs index c1904a3c30..f2683e4677 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -617,7 +617,6 @@ namespace Avalonia.Controls.Primitives PixelFormat.Bgra8888, AlphaFormat.Premul); - // Warning: This is highly questionable using (var frameBuffer = bitmap.Lock()) { Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); @@ -625,5 +624,23 @@ namespace Avalonia.Controls.Primitives return bitmap; } + + /// + /// Updates the given with new, raw BGRA pre-multiplied alpha pixel data. + /// WARNING: The bitmap's width, height and byte count MUST not have changed and MUST be enforced externally. + /// + /// The existing to update. + /// The bitmap (in raw BGRA pre-multiplied alpha pixels). + public static void UpdateBitmapFromPixelData( + WriteableBitmap bitmap, + IList bgraPixelData) + { + using (var frameBuffer = bitmap.Lock()) + { + Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); + } + + return; + } } } From ca7543f1d613f9d7b9f2cfed978f8e23379d6684 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 2 Oct 2022 14:24:10 -0400 Subject: [PATCH 06/57] Add new ArrayList to avoid an extra copy when creating spectrum bitmaps --- .../ColorSlider/ColorSlider.cs | 8 +-- .../ColorSpectrum/ColorSpectrum.cs | 61 +++++++++------- .../Helpers/ArrayList.cs | 71 +++++++++++++++++++ .../Helpers/ColorPickerHelpers.cs | 18 ++--- 4 files changed, 118 insertions(+), 40 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Helpers/ArrayList.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 4e907f610d..8bcdef5558 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -121,7 +121,7 @@ namespace Avalonia.Controls.Primitives if (pixelWidth != 0 && pixelHeight != 0) { - var bitmap = await ColorPickerHelpers.CreateComponentBitmapAsync( + ArrayList bgraPixelData = await ColorPickerHelpers.CreateComponentBitmapAsync( pixelWidth, pixelHeight, Orientation, @@ -131,18 +131,18 @@ namespace Avalonia.Controls.Primitives IsAlphaMaxForced, IsSaturationValueMaxForced); - if (bitmap != null) + if (bgraPixelData != null) { if (_backgroundBitmap != null) { // Re-use the existing WriteableBitmap // This assumes the height, width and byte counts are the same and must be set to null // elsewhere if that assumption is ever not true. - ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bitmap); + ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bgraPixelData); } else { - _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bitmap, pixelWidth, pixelHeight); + _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraPixelData, pixelWidth, pixelHeight); } Background = new ImageBrush(_backgroundBitmap); diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index f272ce850e..3604894833 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -994,13 +994,13 @@ namespace Avalonia.Controls.Primitives // The middle 4 are only needed and used in the case of hue as the third dimension. // Saturation and luminosity need only a min and max. - List bgraMinPixelData = new List(); - List bgraMiddle1PixelData = new List(); - List bgraMiddle2PixelData = new List(); - List bgraMiddle3PixelData = new List(); - List bgraMiddle4PixelData = new List(); - List bgraMaxPixelData = new List(); - List newHsvValues = new List(); + ArrayList bgraMinPixelData; + ArrayList bgraMiddle1PixelData; + ArrayList bgraMiddle2PixelData; + ArrayList bgraMiddle3PixelData; + ArrayList bgraMiddle4PixelData; + ArrayList bgraMaxPixelData; + List newHsvValues; // In Avalonia, Bounds returns the actual device-independent pixel size of a control. // However, this is not necessarily the size of the control rendered on a display. @@ -1011,20 +1011,27 @@ namespace Avalonia.Controls.Primitives int pixelDimension = (int)Math.Round(minDimension * scale); var pixelCount = pixelDimension * pixelDimension; var pixelDataSize = pixelCount * 4; - bgraMinPixelData.Capacity = pixelDataSize; + + bgraMinPixelData = new ArrayList(pixelDataSize); + bgraMaxPixelData = new ArrayList(pixelDataSize); + newHsvValues = new List(pixelCount); // We'll only save pixel data for the middle bitmaps if our third dimension is hue. if (components == ColorSpectrumComponents.ValueSaturation || components == ColorSpectrumComponents.SaturationValue) { - bgraMiddle1PixelData.Capacity = pixelDataSize; - bgraMiddle2PixelData.Capacity = pixelDataSize; - bgraMiddle3PixelData.Capacity = pixelDataSize; - bgraMiddle4PixelData.Capacity = pixelDataSize; + bgraMiddle1PixelData = new ArrayList(pixelDataSize); + bgraMiddle2PixelData = new ArrayList(pixelDataSize); + bgraMiddle3PixelData = new ArrayList(pixelDataSize); + bgraMiddle4PixelData = new ArrayList(pixelDataSize); + } + else + { + bgraMiddle1PixelData = new ArrayList(0); + bgraMiddle2PixelData = new ArrayList(0); + bgraMiddle3PixelData = new ArrayList(0); + bgraMiddle4PixelData = new ArrayList(0); } - - bgraMaxPixelData.Capacity = pixelDataSize; - newHsvValues.Capacity = pixelCount; await Task.Run(() => { @@ -1132,12 +1139,12 @@ namespace Avalonia.Controls.Primitives double maxSaturation, double minValue, double maxValue, - List bgraMinPixelData, - List bgraMiddle1PixelData, - List bgraMiddle2PixelData, - List bgraMiddle3PixelData, - List bgraMiddle4PixelData, - List bgraMaxPixelData, + ArrayList bgraMinPixelData, + ArrayList bgraMiddle1PixelData, + ArrayList bgraMiddle2PixelData, + ArrayList bgraMiddle3PixelData, + ArrayList bgraMiddle4PixelData, + ArrayList bgraMaxPixelData, List newHsvValues) { double hMin = minHue; @@ -1292,12 +1299,12 @@ namespace Avalonia.Controls.Primitives double maxSaturation, double minValue, double maxValue, - List bgraMinPixelData, - List bgraMiddle1PixelData, - List bgraMiddle2PixelData, - List bgraMiddle3PixelData, - List bgraMiddle4PixelData, - List bgraMaxPixelData, + ArrayList bgraMinPixelData, + ArrayList bgraMiddle1PixelData, + ArrayList bgraMiddle2PixelData, + ArrayList bgraMiddle3PixelData, + ArrayList bgraMiddle4PixelData, + ArrayList bgraMaxPixelData, List newHsvValues) { double hMin = minHue; diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ArrayList.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ArrayList.cs new file mode 100644 index 0000000000..0b4c2f8579 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ArrayList.cs @@ -0,0 +1,71 @@ +namespace Avalonia.Controls.Primitives +{ + /// + /// A thin wrapper over an that allows some additional list-like functionality. + /// + /// + /// This is only for internal ColorPicker-related functionality and should not be used elsewhere. + /// It is added for performance to enjoy the simplicity of the IList.Add() method without requiring + /// an additional copy to turn a list into an array for bitmaps. + /// + /// The type of items in the array. + internal class ArrayList + { + private int _nextIndex = 0; + + /// + /// Initializes a new instance of the class. + /// + public ArrayList(int capacity) + { + Capacity = capacity; + Array = new T[capacity]; + } + + /// + /// Provides access to the underlying array by index. + /// This exists for simplification and the property + /// may also be used. + /// + /// The index of the item to get or set. + /// The item at the given index. + public T this[int i] + { + get => Array[i]; + set => Array[i] = value; + } + + /// + /// Gets the underlying array. + /// + public T[] Array { get; private set; } + + /// + /// Gets the fixed capacity/size of the array. + /// This must be set during construction. + /// + public int Capacity { get; private set; } + + /// + /// Adds the given item to the array at the next available index. + /// WARNING: This must be used carefully and only once, in sequence. + /// + /// The item to add. + public void Add(T item) + { + if (_nextIndex >= 0 && + _nextIndex < Capacity) + { + Array[_nextIndex] = item; + _nextIndex++; + } + else + { + // If necessary an exception could be thrown here + // throw new IndexOutOfRangeException(); + } + + return; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs index f2683e4677..2378813f52 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls.Primitives /// during calculation with the HSVA color model. /// This will ensure colors are always discernible regardless of saturation/value. /// A new bitmap representing a gradient of color component values. - public static async Task CreateComponentBitmapAsync( + public static async Task> CreateComponentBitmapAsync( int width, int height, Orientation orientation, @@ -49,14 +49,14 @@ namespace Avalonia.Controls.Primitives { if (width == 0 || height == 0) { - return new byte[0]; + return new ArrayList(0); } - var bitmap = await Task.Run(() => + var bitmap = await Task.Run>(() => { int pixelDataIndex = 0; double componentStep; - byte[] bgraPixelData; + ArrayList bgraPixelData; Color baseRgbColor = Colors.White; Color rgbColor; int bgraPixelDataHeight; @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives // Allocate the buffer // BGRA formatted color components 1 byte each (4 bytes in a pixel) - bgraPixelData = new byte[width * height * 4]; + bgraPixelData = new ArrayList(width * height * 4); bgraPixelDataHeight = height * 4; bgraPixelDataWidth = width * 4; @@ -604,7 +604,7 @@ namespace Avalonia.Controls.Primitives /// The pixel height of the bitmap. /// A new . public static WriteableBitmap CreateBitmapFromPixelData( - IList bgraPixelData, + ArrayList bgraPixelData, int pixelWidth, int pixelHeight) { @@ -619,7 +619,7 @@ namespace Avalonia.Controls.Primitives using (var frameBuffer = bitmap.Lock()) { - Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); + Marshal.Copy(bgraPixelData.Array, 0, frameBuffer.Address, bgraPixelData.Array.Length); } return bitmap; @@ -633,11 +633,11 @@ namespace Avalonia.Controls.Primitives /// The bitmap (in raw BGRA pre-multiplied alpha pixels). public static void UpdateBitmapFromPixelData( WriteableBitmap bitmap, - IList bgraPixelData) + ArrayList bgraPixelData) { using (var frameBuffer = bitmap.Lock()) { - Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); + Marshal.Copy(bgraPixelData.Array, 0, frameBuffer.Address, bgraPixelData.Array.Length); } return; From 98a58217ce22d55f9343216fd4fbc727618c7306 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 2 Oct 2022 14:25:24 -0400 Subject: [PATCH 07/57] Fix ColorSlider update/refresh for all property changes --- .../ColorSlider/ColorSlider.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 8bcdef5558..6f20238b70 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -385,11 +385,11 @@ namespace Avalonia.Controls.Primitives return; } - // Always keep the two color properties in sync if (change.Property == ColorProperty) { ignorePropertyChanged = true; + // Always keep the two color properties in sync HsvColor = Color.ToHsv(); SetColorToSliderValues(); @@ -402,7 +402,10 @@ namespace Avalonia.Controls.Primitives ignorePropertyChanged = false; } - else if (change.Property == ColorModelProperty) + else if (change.Property == ColorComponentProperty || + change.Property == ColorModelProperty || + change.Property == IsAlphaMaxForcedProperty || + change.Property == IsSaturationValueMaxForcedProperty) { ignorePropertyChanged = true; @@ -416,6 +419,7 @@ namespace Avalonia.Controls.Primitives { ignorePropertyChanged = true; + // Always keep the two color properties in sync Color = HsvColor.ToRgb(); SetColorToSliderValues(); @@ -440,6 +444,7 @@ namespace Avalonia.Controls.Primitives _backgroundBitmap = null; UpdateBackground(); + UpdatePseudoClasses(); } else if (change.Property == ValueProperty || change.Property == MinimumProperty || From 4f1d315b4b11cf84ca7c3b4ffc65c41d35e24436 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 2 Oct 2022 14:26:28 -0400 Subject: [PATCH 08/57] Make all ColorSpectrum bitmaps globally accessible by the control for future disposal --- .../ColorSpectrum/ColorSpectrum.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 3604894833..6b06e1eac1 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -73,6 +73,8 @@ namespace Avalonia.Controls.Primitives private WriteableBitmap? _saturationMaximumBitmap; private WriteableBitmap? _valueBitmap; + private WriteableBitmap? _minBitmap; + private WriteableBitmap? _maxBitmap; // Fields used by UpdateEllipse() to ensure that it's using the data // associated with the last call to CreateBitmapsAndColorMap(), @@ -1084,28 +1086,28 @@ namespace Avalonia.Controls.Primitives ColorSpectrumComponents components2 = Components; - WriteableBitmap minBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMinPixelData, pixelWidth, pixelHeight); - WriteableBitmap maxBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMaxPixelData, pixelWidth, pixelHeight); + _minBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMinPixelData, pixelWidth, pixelHeight); + _maxBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMaxPixelData, pixelWidth, pixelHeight); switch (components2) { case ColorSpectrumComponents.HueValue: case ColorSpectrumComponents.ValueHue: - _saturationMinimumBitmap = minBitmap; - _saturationMaximumBitmap = maxBitmap; + _saturationMinimumBitmap = _minBitmap; + _saturationMaximumBitmap = _maxBitmap; break; case ColorSpectrumComponents.HueSaturation: case ColorSpectrumComponents.SaturationHue: - _valueBitmap = maxBitmap; + _valueBitmap = _maxBitmap; break; case ColorSpectrumComponents.ValueSaturation: case ColorSpectrumComponents.SaturationValue: - _hueRedBitmap = minBitmap; + _hueRedBitmap = _minBitmap; _hueYellowBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle1PixelData, pixelWidth, pixelHeight); _hueGreenBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle2PixelData, pixelWidth, pixelHeight); _hueCyanBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle3PixelData, pixelWidth, pixelHeight); _hueBlueBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle4PixelData, pixelWidth, pixelHeight); - _huePurpleBitmap = maxBitmap; + _huePurpleBitmap = _maxBitmap; break; } From 667b02d2c0d6e713980f0ad05c843d96e0c5a6f4 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 2 Oct 2022 14:33:24 -0400 Subject: [PATCH 09/57] Disable WriteableBitmap re-use in ColorSlider due to crashes --- .../ColorSlider/ColorSlider.cs | 8 +++++++- .../Helpers/ColorPickerHelpers.cs | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 6f20238b70..8b624958dd 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -135,10 +135,16 @@ namespace Avalonia.Controls.Primitives { if (_backgroundBitmap != null) { + // CURRENTLY DISABLED DUE TO INTERMITTENT CRASHES IN SKIA/RENDERER + // // Re-use the existing WriteableBitmap // This assumes the height, width and byte counts are the same and must be set to null // elsewhere if that assumption is ever not true. - ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bgraPixelData); + // ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bgraPixelData); + + // ALSO DISABLED DISPOSE DUE TO INTERMITTENT CRASHES + //_backgroundBitmap?.Dispose(); + _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraPixelData, pixelWidth, pixelHeight); } else { diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs index 2378813f52..01e5a0868f 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -627,6 +627,7 @@ namespace Avalonia.Controls.Primitives /// /// Updates the given with new, raw BGRA pre-multiplied alpha pixel data. + /// WARNING: THIS METHOD IS CURRENTLY PROVIDED AS REFERENCE BUT CAUSES INTERMITTENT CRASHES IF USED. /// WARNING: The bitmap's width, height and byte count MUST not have changed and MUST be enforced externally. /// /// The existing to update. From bc927d03129fa201248d2bcab78143f7a1c8f93f Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 9 Oct 2022 21:07:58 -0400 Subject: [PATCH 10/57] Remove ColorSlider background disposal on attach/detach from visual tree --- .../ColorSlider/ColorSlider.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 8b624958dd..0b9f7f9146 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -45,20 +45,12 @@ namespace Avalonia.Controls.Primitives protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - - // Bitmaps were released when detached from the visual tree so they must be re-built - UpdateBackground(); } /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - - // Clean-up all bitmaps - // https://github.com/AvaloniaUI/Avalonia/issues/9051 - _backgroundBitmap?.Dispose(); - _backgroundBitmap = null; } /// From 75b3abbdd86720e2e688687283b49ef95e2ca63b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 10 Oct 2022 18:25:44 +0200 Subject: [PATCH 11/57] Properly arrange Embedded controls during ArrangeOverride --- .../Documents/IInlineHost.cs | 2 - src/Avalonia.Controls/Documents/InlineRun.cs | 42 +++++++++ .../Documents/InlineUIContainer.cs | 48 +--------- src/Avalonia.Controls/RichTextBlock.cs | 87 ++++++++++++------- .../RichTextBlockTests.cs | 36 ++++++++ 5 files changed, 137 insertions(+), 78 deletions(-) create mode 100644 src/Avalonia.Controls/Documents/InlineRun.cs diff --git a/src/Avalonia.Controls/Documents/IInlineHost.cs b/src/Avalonia.Controls/Documents/IInlineHost.cs index da72c207be..5d142952ab 100644 --- a/src/Avalonia.Controls/Documents/IInlineHost.cs +++ b/src/Avalonia.Controls/Documents/IInlineHost.cs @@ -4,8 +4,6 @@ namespace Avalonia.Controls.Documents { internal interface IInlineHost : ILogical { - void AddVisualChild(IControl child); - void Invalidate(); } } diff --git a/src/Avalonia.Controls/Documents/InlineRun.cs b/src/Avalonia.Controls/Documents/InlineRun.cs new file mode 100644 index 0000000000..68c61ca3fa --- /dev/null +++ b/src/Avalonia.Controls/Documents/InlineRun.cs @@ -0,0 +1,42 @@ +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Documents +{ + internal class EmbeddedControlRun : DrawableTextRun + { + public EmbeddedControlRun(IControl control, TextRunProperties properties) + { + Control = control; + Properties = properties; + } + + public IControl Control { get; } + + public override TextRunProperties? Properties { get; } + + public override Size Size => Control.DesiredSize; + + public override double Baseline + { + get + { + double baseline = Size.Height; + double baselineOffsetValue = Control.GetValue(TextBlock.BaselineOffsetProperty); + + if (!MathUtilities.IsZero(baselineOffsetValue)) + { + baseline = baselineOffsetValue; + } + + return -baseline; + } + } + + public override void Draw(DrawingContext drawingContext, Point origin) + { + //noop + } + } +} diff --git a/src/Avalonia.Controls/Documents/InlineUIContainer.cs b/src/Avalonia.Controls/Documents/InlineUIContainer.cs index d632e5fea7..7107fb7fed 100644 --- a/src/Avalonia.Controls/Documents/InlineUIContainer.cs +++ b/src/Avalonia.Controls/Documents/InlineUIContainer.cs @@ -3,7 +3,6 @@ using System.Text; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -using Avalonia.Utilities; namespace Avalonia.Controls.Documents { @@ -59,56 +58,11 @@ namespace Avalonia.Controls.Documents internal override void BuildTextRun(IList textRuns) { - if(InlineHost == null) - { - return; - } - - ((ISetLogicalParent)Child).SetParent(InlineHost); - - InlineHost.AddVisualChild(Child); - - textRuns.Add(new InlineRun(Child, CreateTextRunProperties())); + textRuns.Add(new EmbeddedControlRun(Child, CreateTextRunProperties())); } internal override void AppendText(StringBuilder stringBuilder) { } - - private class InlineRun : DrawableTextRun - { - public InlineRun(IControl control, TextRunProperties properties) - { - Control = control; - Properties = properties; - } - - public IControl Control { get; } - - public override TextRunProperties? Properties { get; } - - public override Size Size => Control.DesiredSize; - - public override double Baseline - { - get - { - double baseline = Size.Height; - double baselineOffsetValue = Control.GetValue(TextBlock.BaselineOffsetProperty); - - if (!MathUtilities.IsZero(baselineOffsetValue)) - { - baseline = baselineOffsetValue; - } - - return -baseline; - } - } - - public override void Draw(DrawingContext drawingContext, Point origin) - { - //noop - } - } } } diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 906a038ec3..a7eee504ec 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -61,6 +61,7 @@ namespace Avalonia.Controls private int _selectionStart; private int _selectionEnd; private int _wordSelectionStart = -1; + private IReadOnlyList? _textRuns; static RichTextBlock() { @@ -277,8 +278,8 @@ namespace Avalonia.Controls protected override void SetText(string? text) { var oldValue = GetText(); - - AddText(text); + + AddText(text); RaisePropertyChanged(TextProperty, oldValue, text); } @@ -301,18 +302,9 @@ namespace Avalonia.Controls ITextSource textSource; - if (HasComplexContent) + if (_textRuns != null) { - var inlines = Inlines!; - - var textRuns = new List(); - - foreach (var inline in inlines) - { - inline.BuildTextRun(textRuns); - } - - textSource = new InlinesTextSource(textRuns); + textSource = new InlinesTextSource(_textRuns); } else { @@ -546,27 +538,72 @@ namespace Avalonia.Controls protected override Size MeasureOverride(Size availableSize) { - foreach (var child in VisualChildren) + LogicalChildren.Clear(); + + VisualChildren.Clear(); + + if (Inlines != null && Inlines.Count > 0) { - if (child is Control control) + var inlines = Inlines; + + var textRuns = new List(); + + foreach (var inline in inlines) { - control.Measure(Size.Infinity); + inline.BuildTextRun(textRuns); + } + + foreach (var textRun in textRuns) + { + if (textRun is EmbeddedControlRun controlRun && + controlRun.Control is Control control) + { + ((ISetLogicalParent)control).SetParent(this); + + VisualChildren.Add(control); + + control.Measure(Size.Infinity); + } } + + _textRuns = textRuns; + } + else + { + _textRuns = null; } - + return base.MeasureOverride(availableSize); } protected override Size ArrangeOverride(Size finalSize) { - foreach (var child in VisualChildren) + if (HasComplexContent) { - if (child is Control control) + var currentY = 0.0; + + foreach (var textLine in TextLayout.TextLines) { - control.Arrange(new Rect(control.DesiredSize)); + var currentX = textLine.Start; + + foreach (var run in textLine.TextRuns) + { + if (run is DrawableTextRun drawable) + { + if (drawable is EmbeddedControlRun controlRun + && controlRun.Control is Control control) + { + control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize)); + } + + currentX += drawable.Size.Width; + } + } + + currentY += textLine.Height; } } - + return base.ArrangeOverride(finalSize); } @@ -618,14 +655,6 @@ namespace Avalonia.Controls } } - void IInlineHost.AddVisualChild(IControl child) - { - if (child.VisualParent == null) - { - VisualChildren.Add(child); - } - } - void IInlineHost.Invalidate() { InvalidateTextLayout(); diff --git a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs index c74f13b808..05007e4f2e 100644 --- a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs @@ -1,4 +1,6 @@ using Avalonia.Controls.Documents; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -92,5 +94,39 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(target, run.Parent); } } + + [Fact] + public void InlineUIContainer_Child_Schould_Be_Arranged() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new RichTextBlock(); + + var button = new Button { Content = "12345678" }; + + button.Template = new FuncControlTemplate