Browse Source

Merge branch 'master' into focus-manager-per-top-level

pull/11407/head
Max Katz 3 years ago
committed by GitHub
parent
commit
92ef524052
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/Avalonia/Avalonia.csproj
  2. 3
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  3. 18
      src/Avalonia.Base/Diagnostics/AppliedStyle.cs
  4. 4
      src/Avalonia.Base/Diagnostics/StyleDiagnostics.cs
  5. 32
      src/Avalonia.Base/Media/Color.cs
  6. 7
      src/Avalonia.Base/Media/GlyphRun.cs
  7. 78
      src/Avalonia.Base/Media/HslColor.cs
  8. 78
      src/Avalonia.Base/Media/HsvColor.cs
  9. 2
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  10. 210
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  11. 61
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  12. 52
      src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs
  13. 3
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  14. 4
      src/Avalonia.Base/StyledElement.cs
  15. 3
      src/Avalonia.Base/Styling/Activators/IStyleActivator.cs
  16. 3
      src/Avalonia.Base/Styling/Activators/IStyleActivatorSink.cs
  17. 12
      src/Avalonia.Base/Styling/ChildSelector.cs
  18. 12
      src/Avalonia.Base/Styling/DescendentSelector.cs
  19. 24
      src/Avalonia.Base/Styling/ISetter.cs
  20. 2
      src/Avalonia.Base/Styling/ISetterInstance.cs
  21. 4
      src/Avalonia.Base/Styling/ISetterValue.cs
  22. 3
      src/Avalonia.Base/Styling/IStyleInstance.cs
  23. 12
      src/Avalonia.Base/Styling/NestingSelector.cs
  24. 12
      src/Avalonia.Base/Styling/NotSelector.cs
  25. 14
      src/Avalonia.Base/Styling/NthChildSelector.cs
  26. 2
      src/Avalonia.Base/Styling/NthLastChildSelector.cs
  27. 12
      src/Avalonia.Base/Styling/OrSelector.cs
  28. 12
      src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
  29. 14
      src/Avalonia.Base/Styling/Selector.cs
  30. 4
      src/Avalonia.Base/Styling/SelectorMatch.cs
  31. 4
      src/Avalonia.Base/Styling/Setter.cs
  32. 12
      src/Avalonia.Base/Styling/SetterBase.cs
  33. 6
      src/Avalonia.Base/Styling/StyleBase.cs
  34. 12
      src/Avalonia.Base/Styling/TemplateSelector.cs
  35. 12
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  36. 72
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs
  37. 88
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
  38. 91
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  39. 6
      src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs
  40. 60
      src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs
  41. 2
      src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs
  42. 2
      src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs
  43. 25
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  44. 13
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml
  45. 25
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  46. 25
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
  47. 13
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml
  48. 25
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  49. 2
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  50. 2
      src/Avalonia.Controls/ContextMenu.cs
  51. 2
      src/Avalonia.Controls/Control.cs
  52. 8
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  53. 4
      src/Avalonia.Controls/SelectableTextBlock.cs
  54. 8
      src/Avalonia.Controls/TextBlock.cs
  55. 119
      src/Avalonia.Controls/TextBox.cs
  56. 2
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  57. 2
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  58. 4
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs
  59. 9
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  60. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs
  61. 4
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  62. 2
      src/Markup/Avalonia.Markup/Data/TemplateBinding.cs
  63. 36
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  64. 44
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  65. 11
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  66. 13
      src/Skia/Avalonia.Skia/SKFontCache.cs
  67. 2
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  68. 4
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  69. 2
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  70. 9
      src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs
  71. 29
      tests/Avalonia.Base.UnitTests/Media/ColorTests.cs
  72. 36
      tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs
  73. 34
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
  74. 2
      tests/Avalonia.RenderTests/Controls/TextBlockTests.cs
  75. 12
      tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
  76. 6
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  77. 22
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  78. 4
      tests/Avalonia.UnitTests/StyleHelpers.cs
  79. BIN
      tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png
  80. BIN
      tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Fill_NoTile.expected.png
  81. BIN
      tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png
  82. BIN
      tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png
  83. BIN
      tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_UniformToFill_NoTile.expected.png
  84. BIN
      tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Uniform_NoTile.expected.png
  85. BIN
      tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_Fill.expected.png
  86. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png
  87. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png
  88. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png
  89. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png
  90. BIN
      tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png
  91. BIN
      tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Fill_NoTile.expected.png
  92. BIN
      tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png
  93. BIN
      tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png
  94. BIN
      tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_UniformToFill_NoTile.expected.png
  95. BIN
      tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Uniform_NoTile.expected.png
  96. BIN
      tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_Fill.expected.png
  97. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png
  98. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png
  99. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png

2
packages/Avalonia/Avalonia.csproj

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.BuildServices" Version="0.0.12" />
<PackageReference Include="Avalonia.BuildServices" Version="0.0.15" />
<ProjectReference Include="../../src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj" />
<ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj">
<PrivateAssets>all</PrivateAssets>

3
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@ -103,7 +103,8 @@
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />-->
<ColorPreviewer Grid.Row="8"
IsAccentColorsVisible="False"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}"
Margin="0,2,0,0" />
</Grid>
</Grid>
</UserControl>

18
src/Avalonia.Base/Diagnostics/AppliedStyle.cs

@ -0,0 +1,18 @@
using Avalonia.Styling;
namespace Avalonia.Diagnostics
{
public class AppliedStyle
{
private readonly IStyleInstance _instance;
internal AppliedStyle(IStyleInstance instance)
{
_instance = instance;
}
public bool HasActivator => _instance.HasActivator;
public bool IsActive => _instance.IsActive;
public StyleBase Style => (StyleBase)_instance.Source;
}
}

4
src/Avalonia.Base/Diagnostics/StyleDiagnostics.cs

@ -11,9 +11,9 @@ namespace Avalonia.Diagnostics
/// <summary>
/// Currently applied styles.
/// </summary>
public IReadOnlyList<IStyleInstance> AppliedStyles { get; }
public IReadOnlyList<AppliedStyle> AppliedStyles { get; }
public StyleDiagnostics(IReadOnlyList<IStyleInstance> appliedStyles)
public StyleDiagnostics(IReadOnlyList<AppliedStyle> appliedStyles)
{
AppliedStyles = appliedStyles;
}

32
src/Avalonia.Base/Media/Color.cs

@ -478,7 +478,6 @@ namespace Avalonia.Media
/// <returns>The HSL equivalent color.</returns>
public HslColor ToHsl()
{
// Don't use the HslColor(Color) constructor to avoid an extra HslColor
return Color.ToHsl(R, G, B, A);
}
@ -488,7 +487,6 @@ namespace Avalonia.Media
/// <returns>The HSV equivalent color.</returns>
public HsvColor ToHsv()
{
// Don't use the HsvColor(Color) constructor to avoid an extra HsvColor
return Color.ToHsv(R, G, B, A);
}
@ -517,21 +515,6 @@ namespace Avalonia.Media
}
}
/// <summary>
/// Converts the given RGB color to its HSL color equivalent.
/// </summary>
/// <param name="color">The color in the RGB color model.</param>
/// <returns>A new <see cref="HslColor"/> equivalent to the given RGBA values.</returns>
public static HslColor ToHsl(Color color)
{
// Normalize RGBA components into the 0..1 range
return Color.ToHsl(
(byteToDouble * color.R),
(byteToDouble * color.G),
(byteToDouble * color.B),
(byteToDouble * color.A));
}
/// <summary>
/// Converts the given RGBA color component values to their HSL color equivalent.
/// </summary>
@ -606,21 +589,6 @@ namespace Avalonia.Media
return new HslColor(a, 60 * h1, saturation, lightness, clampValues: false);
}
/// <summary>
/// Converts the given RGB color to its HSV color equivalent.
/// </summary>
/// <param name="color">The color in the RGB color model.</param>
/// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
public static HsvColor ToHsv(Color color)
{
// Normalize RGBA components into the 0..1 range
return Color.ToHsv(
(byteToDouble * color.R),
(byteToDouble * color.G),
(byteToDouble * color.B),
(byteToDouble * color.A));
}
/// <summary>
/// Converts the given RGBA color component values to their HSV color equivalent.
/// </summary>

7
src/Avalonia.Base/Media/GlyphRun.cs

@ -155,6 +155,8 @@ namespace Avalonia.Media
/// </summary>
public Rect Bounds => new Rect(new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height));
public Rect InkBounds => PlatformImpl.Item.Bounds;
/// <summary>
///
/// </summary>
@ -728,7 +730,7 @@ namespace Avalonia.Media
clusterLength++;
i--;
if(characterIndex >= 0)
if (characterIndex >= 0)
{
codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
@ -827,8 +829,7 @@ namespace Avalonia.Media
GlyphTypeface,
FontRenderingEmSize,
GlyphInfos,
BaselineOrigin,
Bounds);
BaselineOrigin);
_platformImpl = RefCountable.Create(platformImpl);

78
src/Avalonia.Base/Media/HslColor.cs

@ -90,7 +90,7 @@ namespace Avalonia.Media
/// <param name="color">The RGB color to convert to HSL.</param>
public HslColor(Color color)
{
var hsl = Color.ToHsl(color);
var hsl = color.ToHsl();
A = hsl.A;
H = hsl.H;
@ -165,10 +165,18 @@ namespace Avalonia.Media
/// <returns>The RGB equivalent color.</returns>
public Color ToRgb()
{
// Use the by-component conversion method directly for performance
return HslColor.ToRgb(H, S, L, A);
}
/// <summary>
/// Returns the HSV color model equivalent of this HSL color.
/// </summary>
/// <returns>The HSV equivalent color.</returns>
public HsvColor ToHsv()
{
return HslColor.ToHsv(H, S, L, A);
}
/// <inheritdoc/>
public override string ToString()
{
@ -349,16 +357,6 @@ namespace Avalonia.Media
return new HslColor(1.0, h, s, l);
}
/// <summary>
/// Converts the given HSL color to its RGB color equivalent.
/// </summary>
/// <param name="hslColor">The color in the HSL color model.</param>
/// <returns>A new RGB <see cref="Color"/> equivalent to the given HSLA values.</returns>
public static Color ToRgb(HslColor hslColor)
{
return HslColor.ToRgb(hslColor.H, hslColor.S, hslColor.L, hslColor.A);
}
/// <summary>
/// Converts the given HSLA color component values to their RGB color equivalent.
/// </summary>
@ -442,13 +440,67 @@ namespace Avalonia.Media
b1 = x;
}
return Color.FromArgb(
return new Color(
(byte)Math.Round(255 * alpha),
(byte)Math.Round(255 * (r1 + m)),
(byte)Math.Round(255 * (g1 + m)),
(byte)Math.Round(255 * (b1 + m)));
}
/// <summary>
/// Converts the given HSLA color component values to their HSV color equivalent.
/// </summary>
/// <param name="hue">The Hue component in the HSL color model in the range from 0..360.</param>
/// <param name="saturation">The Saturation component in the HSL color model in the range from 0..1.</param>
/// <param name="lightness">The Lightness component in the HSL color model in the range from 0..1.</param>
/// <param name="alpha">The Alpha component in the range from 0..1.</param>
/// <returns>A new <see cref="HsvColor"/> equivalent to the given HSLA values.</returns>
public static HsvColor ToHsv(
double hue,
double saturation,
double lightness,
double alpha = 1.0)
{
// We want the hue to be between 0 and 359,
// so we first ensure that that's the case.
while (hue >= 360.0)
{
hue -= 360.0;
}
while (hue < 0.0)
{
hue += 360.0;
}
// We similarly clamp saturation, lightness and alpha between 0 and 1.
saturation = saturation < 0.0 ? 0.0 : saturation;
saturation = saturation > 1.0 ? 1.0 : saturation;
lightness = lightness < 0.0 ? 0.0 : lightness;
lightness = lightness > 1.0 ? 1.0 : lightness;
alpha = alpha < 0.0 ? 0.0 : alpha;
alpha = alpha > 1.0 ? 1.0 : alpha;
// The conversion algorithm is from the below link
// https://en.wikipedia.org/wiki/HSL_and_HSV#Interconversion
double s;
double v = lightness + (saturation * Math.Min(lightness, 1.0 - lightness));
if (v <= 0)
{
s = 0;
}
else
{
s = 2.0 * (1.0 - (lightness / v));
}
return new HsvColor(alpha, hue, s, v);
}
/// <summary>
/// Indicates whether the values of two specified <see cref="HslColor"/> objects are equal.
/// </summary>

78
src/Avalonia.Base/Media/HsvColor.cs

@ -90,7 +90,7 @@ namespace Avalonia.Media
/// <param name="color">The RGB color to convert to HSV.</param>
public HsvColor(Color color)
{
var hsv = Color.ToHsv(color);
var hsv = color.ToHsv();
A = hsv.A;
H = hsv.H;
@ -195,10 +195,18 @@ namespace Avalonia.Media
/// <returns>The RGB equivalent color.</returns>
public Color ToRgb()
{
// Use the by-component conversion method directly for performance
return HsvColor.ToRgb(H, S, V, A);
}
/// <summary>
/// Returns the HSL color model equivalent of this HSV color.
/// </summary>
/// <returns>The HSL equivalent color.</returns>
public HslColor ToHsl()
{
return HsvColor.ToHsl(H, S, V, A);
}
/// <inheritdoc/>
public override string ToString()
{
@ -379,16 +387,6 @@ namespace Avalonia.Media
return new HsvColor(1.0, h, s, v);
}
/// <summary>
/// Converts the given HSV color to its RGB color equivalent.
/// </summary>
/// <param name="hsvColor">The color in the HSV color model.</param>
/// <returns>A new RGB <see cref="Color"/> equivalent to the given HSVA values.</returns>
public static Color ToRgb(HsvColor hsvColor)
{
return HsvColor.ToRgb(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A);
}
/// <summary>
/// Converts the given HSVA color component values to their RGB color equivalent.
/// </summary>
@ -520,13 +518,67 @@ namespace Avalonia.Media
break;
}
return Color.FromArgb(
return new Color(
(byte)Math.Round(alpha * 255),
(byte)Math.Round(r * 255),
(byte)Math.Round(g * 255),
(byte)Math.Round(b * 255));
}
/// <summary>
/// Converts the given HSVA color component values to their HSL color equivalent.
/// </summary>
/// <param name="hue">The Hue component in the HSV color model in the range from 0..360.</param>
/// <param name="saturation">The Saturation component in the HSV color model in the range from 0..1.</param>
/// <param name="value">The Value component in the HSV color model in the range from 0..1.</param>
/// <param name="alpha">The Alpha component in the range from 0..1.</param>
/// <returns>A new <see cref="HslColor"/> equivalent to the given HSVA values.</returns>
public static HslColor ToHsl(
double hue,
double saturation,
double value,
double alpha = 1.0)
{
// We want the hue to be between 0 and 359,
// so we first ensure that that's the case.
while (hue >= 360.0)
{
hue -= 360.0;
}
while (hue < 0.0)
{
hue += 360.0;
}
// We similarly clamp saturation, value and alpha between 0 and 1.
saturation = saturation < 0.0 ? 0.0 : saturation;
saturation = saturation > 1.0 ? 1.0 : saturation;
value = value < 0.0 ? 0.0 : value;
value = value > 1.0 ? 1.0 : value;
alpha = alpha < 0.0 ? 0.0 : alpha;
alpha = alpha > 1.0 ? 1.0 : alpha;
// The conversion algorithm is from the below link
// https://en.wikipedia.org/wiki/HSL_and_HSV#Interconversion
double s;
double l = value * (1.0 - (saturation / 2.0));
if (l <= 0 || l >= 1)
{
s = 0.0;
}
else
{
s = (value - l) / Math.Min(l, 1.0 - l);
}
return new HslColor(alpha, hue, s, l);
}
/// <summary>
/// Indicates whether the values of two specified <see cref="HsvColor"/> objects are equal.
/// </summary>

2
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@ -64,7 +64,7 @@ namespace Avalonia.Media.TextFormatting
if (Properties.BackgroundBrush != null)
{
drawingContext.DrawRectangle(Properties.BackgroundBrush, null, new Rect(Size));
drawingContext.DrawRectangle(Properties.BackgroundBrush, null, GlyphRun.Bounds);
}
drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun);

210
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -13,6 +13,7 @@ namespace Avalonia.Media.TextFormatting
private readonly TextParagraphProperties _paragraphProperties;
private readonly TextTrimming _textTrimming;
private readonly TextLine[] _textLines;
private readonly CachedMetrics _metrics = new();
private int _textSourceLength;
@ -151,12 +152,95 @@ namespace Avalonia.Media.TextFormatting
=> _textLines;
/// <summary>
/// Gets the bounds of the layout.
/// The distance from the top of the first line to the bottom of the last line.
/// </summary>
/// <value>
/// The bounds.
/// </value>
public Rect Bounds { get; private set; }
public double Height
{
get
{
return _metrics.Height;
}
}
/// <summary>
/// The distance from the topmost black pixel of the first line
/// to the bottommost black pixel of the last line.
/// </summary>
public double Extent
{
get
{
return _metrics.Extent;
}
}
/// <summary>
/// The distance from the top of the first line to the baseline of the first line.
/// </summary>
public double Baseline
{
get
{
return _metrics.Baseline;
}
}
/// <summary>
/// The distance from the bottom of the last line to the extent bottom.
/// </summary>
public double OverhangAfter
{
get
{
return _metrics.OverhangAfter;
}
}
/// <summary>
/// The maximum distance from the leading black pixel to the leading alignment point of a line.
/// </summary>
public double OverhangLeading
{
get
{
return _metrics.OverhangLeading;
}
}
/// <summary>
/// The maximum distance from the trailing black pixel to the trailing alignment point of a line.
/// </summary>
public double OverhangTrailing
{
get
{
return _metrics.OverhangTrailing;
}
}
/// <summary>
/// The maximum advance width between the leading and trailing alignment points of a line,
/// excluding the width of whitespace characters at the end of the line.
/// </summary>
public double Width
{
get
{
return _metrics.Width;
}
}
/// <summary>
/// The maximum advance width between the leading and trailing alignment points of a line,
/// including the width of whitespace characters at the end of the line.
/// </summary>
public double WidthIncludingTrailingWhitespace
{
get
{
return _metrics.WidthIncludingTrailingWhitespace;
}
}
/// <summary>
/// Draws the text layout.
@ -382,7 +466,7 @@ namespace Avalonia.Media.TextFormatting
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
y > Bounds.Bottom;
y > Height;
if (textPosition == textLine.FirstTextSourceIndex + textLine.Length)
{
@ -422,41 +506,25 @@ namespace Avalonia.Media.TextFormatting
textRunStyle, textWrapping, lineHeight, 0, letterSpacing);
}
/// <summary>
/// Updates the current bounds.
/// </summary>
/// <param name="textLine">The text line.</param>
/// <param name="left">The current left.</param>
/// <param name="width">The current width.</param>
/// <param name="height">The current height.</param>
private static void UpdateBounds(TextLine textLine, ref double left, ref double width, ref double height)
private TextLine[] CreateTextLines()
{
var lineWidth = textLine.WidthIncludingTrailingWhitespace;
if (width < lineWidth)
{
width = lineWidth;
}
var objectPool = FormattingObjectPool.Instance;
var start = textLine.Start;
var lineStartOfLongestLine = double.MaxValue;
var origin = new Point();
var first = true;
if (left > start)
{
left = start;
}
height += textLine.Height;
}
double accBlackBoxLeft, accBlackBoxTop, accBlackBoxRight, accBlackBoxBottom;
private TextLine[] CreateTextLines()
{
var objectPool = FormattingObjectPool.Instance;
accBlackBoxLeft = accBlackBoxTop = double.MaxValue;
accBlackBoxRight = accBlackBoxBottom = double.MinValue;
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
Bounds = new Rect(0, 0, 0, textLine.Height);
UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
return new TextLine[] { textLine };
}
@ -465,8 +533,6 @@ namespace Avalonia.Media.TextFormatting
try
{
double left = double.PositiveInfinity, width = 0.0, height = 0.0;
_textSourceLength = 0;
TextLine? previousLine = null;
@ -487,7 +553,8 @@ namespace Avalonia.Media.TextFormatting
textLines.Add(emptyTextLine);
UpdateBounds(emptyTextLine, ref left, ref width, ref height);
UpdateMetrics(emptyTextLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
}
break;
@ -497,7 +564,7 @@ namespace Avalonia.Media.TextFormatting
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight)
&& height + textLine.Height > MaxHeight)
&& Height + textLine.Height > MaxHeight)
{
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{
@ -519,7 +586,8 @@ namespace Avalonia.Media.TextFormatting
textLines.Add(textLine);
UpdateBounds(textLine, ref left, ref width, ref height);
UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
previousLine = textLine;
@ -528,7 +596,7 @@ namespace Avalonia.Media.TextFormatting
{
if (textLine.TextLineBreak is { IsSplit: true })
{
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(WidthIncludingTrailingWhitespace));
}
break;
@ -546,18 +614,17 @@ namespace Avalonia.Media.TextFormatting
textLines.Add(textLine);
UpdateBounds(textLine, ref left, ref width, ref height);
UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
}
Bounds = new Rect(left, 0, width, height);
if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{
var justificationWidth = MaxWidth;
if (_paragraphProperties.TextWrapping != TextWrapping.NoWrap)
{
justificationWidth = width;
justificationWidth = WidthIncludingTrailingWhitespace;
}
if (justificationWidth > 0)
@ -582,6 +649,46 @@ namespace Avalonia.Media.TextFormatting
}
}
private void UpdateMetrics(
TextLine currentLine,
ref double lineStartOfLongestLine,
ref Point origin,
ref bool first,
ref double accBlackBoxLeft,
ref double accBlackBoxTop,
ref double accBlackBoxRight,
ref double accBlackBoxBottom)
{
var blackBoxLeft = origin.X + currentLine.Start + currentLine.OverhangLeading;
var blackBoxRight = origin.X + currentLine.Start + currentLine.Width - currentLine.OverhangTrailing;
var blackBoxBottom = origin.Y + currentLine.Height + currentLine.OverhangAfter;
var blackBoxTop = blackBoxBottom - currentLine.Extent;
accBlackBoxLeft = Math.Min(accBlackBoxLeft, blackBoxLeft);
accBlackBoxRight = Math.Max(accBlackBoxRight, blackBoxRight);
accBlackBoxBottom = Math.Max(accBlackBoxBottom, blackBoxBottom);
accBlackBoxTop = Math.Min(accBlackBoxTop, blackBoxTop);
_metrics.OverhangAfter = currentLine.OverhangAfter;
_metrics.Height += currentLine.Height;
_metrics.Width = Math.Max(_metrics.Width, currentLine.Width);
_metrics.WidthIncludingTrailingWhitespace = Math.Max(_metrics.WidthIncludingTrailingWhitespace, currentLine.WidthIncludingTrailingWhitespace);
lineStartOfLongestLine = Math.Min(lineStartOfLongestLine, currentLine.Start);
_metrics.Extent = accBlackBoxBottom - accBlackBoxTop;
_metrics.OverhangLeading = accBlackBoxLeft - lineStartOfLongestLine;
_metrics.OverhangTrailing = _metrics.Width - (accBlackBoxRight - lineStartOfLongestLine);
if (first)
{
_metrics.Baseline = currentLine.Baseline;
first = false;
}
origin = origin.WithY(origin.Y + currentLine.Height);
}
/// <summary>
/// Gets the <see cref="TextCollapsingProperties"/> for current text trimming mode.
/// </summary>
@ -605,5 +712,24 @@ namespace Avalonia.Media.TextFormatting
line.Dispose();
}
}
private class CachedMetrics
{
// vertical
public double Height;
public double Baseline;
// horizontal
public double Width;
public double WidthIncludingTrailingWhitespace;
// vertical bounding box metrics
public double Extent;
public double OverhangAfter;
// horizontal bounding box metrics
public double OverhangLeading;
public double OverhangTrailing;
}
}
}

61
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -51,7 +51,7 @@ namespace Avalonia.Media.TextFormatting
public override double Baseline => _textLineMetrics.TextBaseline;
/// <inheritdoc/>
public override double Extent => _textLineMetrics.Height;
public override double Extent => _textLineMetrics.Extent;
/// <inheritdoc/>
public override double Height => _textLineMetrics.Height;
@ -60,13 +60,13 @@ namespace Avalonia.Media.TextFormatting
public override int NewLineLength => _textLineMetrics.NewlineLength;
/// <inheritdoc/>
public override double OverhangAfter => 0;
public override double OverhangAfter => _textLineMetrics.OverhangAfter;
/// <inheritdoc/>
public override double OverhangLeading => 0;
public override double OverhangLeading => _textLineMetrics.OverhangLeading;
/// <inheritdoc/>
public override double OverhangTrailing => 0;
public override double OverhangTrailing => _textLineMetrics.OverhangTrailing;
/// <inheritdoc/>
public override int TrailingWhitespaceLength => _textLineMetrics.TrailingWhitespaceLength;
@ -87,13 +87,18 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in _textRuns)
{
if (textRun is DrawableTextRun drawable)
switch (textRun)
{
var offsetY = GetBaselineOffset(this, drawable);
case DrawableTextRun drawableTextRun:
{
var offsetY = GetBaselineOffset(this, drawableTextRun);
drawableTextRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
drawable.Draw(drawingContext, new Point(currentX, currentY + offsetY));
currentX += drawableTextRun.Size.Width;
currentX += drawable.Size.Width;
break;
}
}
}
}
@ -174,10 +179,12 @@ namespace Avalonia.Media.TextFormatting
distance -= Start;
var lastIndex = _textRuns.Length - 1;
var lineLength = Length;
if (_textRuns[lastIndex] is TextEndOfLine)
if (_textRuns[lastIndex] is TextEndOfLine textEndOfLine)
{
lastIndex--;
lineLength -= textEndOfLine.Length;
}
var currentPosition = FirstTextSourceIndex;
@ -205,7 +212,7 @@ namespace Avalonia.Media.TextFormatting
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{
currentPosition = Length - lastRun.Length;
currentPosition = lineLength - lastRun.Length;
}
return GetRunCharacterHit(lastRun, currentPosition, distance);
@ -703,7 +710,7 @@ namespace Avalonia.Media.TextFormatting
//In case a run only contains a linebreak we don't want to skip it.
if (currentRun is ShapedTextRun shaped)
{
if(currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0)
if (currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0)
{
continue;
}
@ -1431,9 +1438,10 @@ namespace Avalonia.Media.TextFormatting
var lineGap = fontMetrics.LineGap * scale;
var height = descent - ascent + lineGap;
var lineHeight = _paragraphProperties.LineHeight;
var bounds = new Rect();
for (var index = 0; index < _textRuns.Length; index++)
{
switch (_textRuns[index])
@ -1441,6 +1449,9 @@ namespace Avalonia.Media.TextFormatting
case ShapedTextRun textRun:
{
var textMetrics = textRun.TextMetrics;
var glyphRun = textRun.GlyphRun;
bounds = bounds.Union(glyphRun.InkBounds);
if (fontRenderingEmSize < textMetrics.FontRenderingEmSize)
{
@ -1486,18 +1497,22 @@ namespace Avalonia.Media.TextFormatting
ascent = -drawableTextRun.Baseline;
}
bounds = bounds.Union(new Rect(new Point(bounds.Right, 0), drawableTextRun.Size));
break;
}
}
}
var overhangAfter = Math.Max(0, bounds.Bottom - height);
var width = widthIncludingWhitespace;
for (var i = _textRuns.Length - 1; i >= 0; i--)
{
var currentRun = _textRuns[i];
if(currentRun is ShapedTextRun shapedText)
if (currentRun is ShapedTextRun shapedText)
{
var glyphRun = shapedText.GlyphRun;
var glyphRunMetrics = glyphRun.Metrics;
@ -1518,6 +1533,9 @@ namespace Avalonia.Media.TextFormatting
}
var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
var overhangLeading = Math.Max(0, bounds.Left - start);
var overhangTrailing = Math.Max(0, bounds.Width - widthIncludingWhitespace);
var hasOverflowed = overhangLeading + widthIncludingWhitespace + overhangTrailing > _paragraphWidth;
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
{
@ -1527,8 +1545,21 @@ namespace Avalonia.Media.TextFormatting
}
}
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
-ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
return new TextLineMetrics
{
HasOverflowed = hasOverflowed,
Height = height,
Extent = bounds.Height,
NewlineLength = newLineLength,
Start = start,
TextBaseline = -ascent,
TrailingWhitespaceLength = trailingWhitespaceLength,
Width = width,
WidthIncludingTrailingWhitespace = widthIncludingWhitespace,
OverhangLeading= overhangLeading,
OverhangTrailing= overhangTrailing,
OverhangAfter = overhangAfter
};
}
/// <summary>

52
src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs

@ -5,59 +5,65 @@
/// that holds information about ascent, descent, line gap, size and origin of the text line.
/// </summary>
public readonly record struct TextLineMetrics
{
public TextLineMetrics(bool hasOverflowed, double height, int newlineLength, double start, double textBaseline,
int trailingWhitespaceLength, double width,
double widthIncludingTrailingWhitespace)
{
HasOverflowed = hasOverflowed;
Height = height;
NewlineLength = newlineLength;
Start = start;
TextBaseline = textBaseline;
TrailingWhitespaceLength = trailingWhitespaceLength;
Width = width;
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
}
{
/// <summary>
/// Gets a value that indicates whether content of the line overflows the specified paragraph width.
/// </summary>
public bool HasOverflowed { get; }
public bool HasOverflowed { get; init; }
/// <summary>
/// Gets the height of a line of text.
/// </summary>
public double Height { get; }
public double Height { get; init; }
/// <summary>
/// Gets the number of newline characters at the end of a line.
/// </summary>
public int NewlineLength { get; }
public int NewlineLength { get; init; }
/// <summary>
/// Gets the distance from the start of a paragraph to the starting point of a line.
/// </summary>
public double Start { get; }
public double Start { get; init; }
/// <summary>
/// Gets the distance from the top to the baseline of the line of text.
/// </summary>
public double TextBaseline { get; }
public double TextBaseline { get; init; }
/// <summary>
/// Gets the number of whitespace code points beyond the last non-blank character in a line.
/// </summary>
public int TrailingWhitespaceLength { get; }
public int TrailingWhitespaceLength { get; init; }
/// <summary>
/// Gets the width of a line of text, excluding trailing whitespace characters.
/// </summary>
public double Width { get; }
public double Width { get; init; }
/// <summary>
/// Gets the width of a line of text, including trailing whitespace characters.
/// </summary>
public double WidthIncludingTrailingWhitespace { get; }
public double WidthIncludingTrailingWhitespace { get; init; }
/// <summary>
/// Gets the distance from the top-most to bottom-most black pixel in a line.
/// </summary>
public double Extent { get; init; }
/// <summary>
/// Gets the distance that black pixels extend beyond the bottom alignment edge of a line.
/// </summary>
public double OverhangAfter { get; init; }
/// <summary>
/// Gets the distance that black pixels extend prior to the left leading alignment edge of the line.
/// </summary>
public double OverhangLeading { get; init; }
/// <summary>
/// Gets the distance that black pixels extend following the right trailing alignment edge of the line.
/// </summary>
public double OverhangTrailing { get; init; }
}
}

3
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@ -169,9 +169,8 @@ namespace Avalonia.Platform
/// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="glyphInfos">The list of glyphs.</param>
/// <param name="baselineOrigin">The baseline origin of the run. Can be null.</param>
/// <param name="bounds">the conservative bounding box of the run</param>
/// <returns>An <see cref="IGlyphRunImpl"/>.</returns>
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds);
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin);
/// <summary>
/// Creates a backend-specific object using a low-level API graphics context

4
src/Avalonia.Base/StyledElement.cs

@ -420,12 +420,12 @@ namespace Avalonia
internal StyleDiagnostics GetStyleDiagnosticsInternal()
{
var styles = new List<IStyleInstance>();
var styles = new List<AppliedStyle>();
foreach (var frame in GetValueStore().Frames)
{
if (frame is IStyleInstance style)
styles.Add(style);
styles.Add(new(style));
}
return new StyleDiagnostics(styles);

3
src/Avalonia.Base/Styling/Activators/IStyleActivator.cs

@ -15,8 +15,7 @@ namespace Avalonia.Styling.Activators
/// - The activation state can be re-evaluated at any time by calling <see cref="GetIsActive"/>
/// - No error or completion messages
/// </remarks>
[Unstable]
public interface IStyleActivator : IDisposable
internal interface IStyleActivator : IDisposable
{
/// <summary>
/// Gets a value indicating whether the style is subscribed.

3
src/Avalonia.Base/Styling/Activators/IStyleActivatorSink.cs

@ -5,8 +5,7 @@ namespace Avalonia.Styling.Activators
/// <summary>
/// Receives notifications from an <see cref="IStyleActivator"/>.
/// </summary>
[Unstable]
public interface IStyleActivatorSink
internal interface IStyleActivatorSink
{
/// <summary>
/// Called when the subscribed activator value changes.

12
src/Avalonia.Base/Styling/ChildSelector.cs

@ -19,13 +19,13 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
public override bool InTemplate => _parent.InTemplate;
internal override bool InTemplate => _parent.InTemplate;
/// <inheritdoc/>
public override bool IsCombinator => true;
internal override bool IsCombinator => true;
/// <inheritdoc/>
public override Type? TargetType => null;
internal override Type? TargetType => null;
public override string ToString(Style? owner)
{
@ -37,7 +37,7 @@ namespace Avalonia.Styling
return _selectorString;
}
protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
{
var controlParent = ((ILogical)control).LogicalParent;
@ -64,7 +64,7 @@ namespace Avalonia.Styling
}
}
protected override Selector? MovePrevious() => null;
protected override Selector? MovePreviousOrParent() => _parent;
private protected override Selector? MovePrevious() => null;
private protected override Selector? MovePreviousOrParent() => _parent;
}
}

12
src/Avalonia.Base/Styling/DescendentSelector.cs

@ -17,13 +17,13 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
public override bool IsCombinator => true;
internal override bool IsCombinator => true;
/// <inheritdoc/>
public override bool InTemplate => _parent.InTemplate;
internal override bool InTemplate => _parent.InTemplate;
/// <inheritdoc/>
public override Type? TargetType => null;
internal override Type? TargetType => null;
public override string ToString(Style? owner)
{
@ -35,7 +35,7 @@ namespace Avalonia.Styling
return _selectorString;
}
protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
{
var c = (ILogical)control;
var descendantMatches = new OrActivatorBuilder();
@ -69,7 +69,7 @@ namespace Avalonia.Styling
}
}
protected override Selector? MovePrevious() => null;
protected override Selector? MovePreviousOrParent() => _parent;
private protected override Selector? MovePrevious() => null;
private protected override Selector? MovePreviousOrParent() => _parent;
}
}

24
src/Avalonia.Base/Styling/ISetter.cs

@ -1,24 +0,0 @@
using System;
using Avalonia.Metadata;
namespace Avalonia.Styling
{
/// <summary>
/// Represents a setter for a <see cref="Style"/>.
/// </summary>
[NotClientImplementable]
public interface ISetter
{
/// <summary>
/// Instances a setter on a control.
/// </summary>
/// <param name="styleInstance">The style which contains the setter.</param>
/// <param name="target">The control.</param>
/// <returns>An <see cref="ISetterInstance"/>.</returns>
/// <remarks>
/// This method should return an <see cref="ISetterInstance"/> which can be used to apply
/// the setter to the specified control.
/// </remarks>
ISetterInstance Instance(IStyleInstance styleInstance, StyledElement target);
}
}

2
src/Avalonia.Base/Styling/ISetterInstance.cs

@ -3,7 +3,7 @@
namespace Avalonia.Styling
{
/// <summary>
/// Represents an <see cref="ISetter"/> that has been instanced on a control.
/// Represents a <see cref="Setter"/> that has been instanced on a control.
/// </summary>
[Unstable]
public interface ISetterInstance

4
src/Avalonia.Base/Styling/ISetterValue.cs

@ -3,13 +3,13 @@
namespace Avalonia.Styling
{
/// <summary>
/// Customizes the behavior of a class when added as a value to an <see cref="ISetter"/>.
/// Customizes the behavior of a class when added as a value to a <see cref="SetterBase"/>.
/// </summary>
public interface ISetterValue
{
/// <summary>
/// Notifies that the object has been added as a setter value.
/// </summary>
void Initialize(ISetter setter);
void Initialize(SetterBase setter);
}
}

3
src/Avalonia.Base/Styling/IStyleInstance.cs

@ -5,8 +5,7 @@ namespace Avalonia.Styling
/// <summary>
/// Represents a <see cref="Style"/> that has been instanced on a control.
/// </summary>
[Unstable]
public interface IStyleInstance
internal interface IStyleInstance
{
/// <summary>
/// Gets the source style.

12
src/Avalonia.Base/Styling/NestingSelector.cs

@ -7,13 +7,13 @@ namespace Avalonia.Styling
/// </summary>
internal class NestingSelector : Selector
{
public override bool InTemplate => false;
public override bool IsCombinator => false;
public override Type? TargetType => null;
internal override bool InTemplate => false;
internal override bool IsCombinator => false;
internal override Type? TargetType => null;
public override string ToString(Style? owner) => owner?.Parent?.ToString() ?? "^";
protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
{
if (parent is Style s && s.Selector is not null)
{
@ -32,7 +32,7 @@ namespace Avalonia.Styling
"Nesting selector was specified but cannot determine parent selector.");
}
protected override Selector? MovePrevious() => null;
protected override Selector? MovePreviousOrParent() => null;
private protected override Selector? MovePrevious() => null;
private protected override Selector? MovePreviousOrParent() => null;
}
}

12
src/Avalonia.Base/Styling/NotSelector.cs

@ -26,13 +26,13 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
public override bool InTemplate => _argument.InTemplate;
internal override bool InTemplate => _argument.InTemplate;
/// <inheritdoc/>
public override bool IsCombinator => false;
internal override bool IsCombinator => false;
/// <inheritdoc/>
public override Type? TargetType => _previous?.TargetType;
internal override Type? TargetType => _previous?.TargetType;
/// <inheritdoc/>
public override string ToString(Style? owner)
@ -45,7 +45,7 @@ namespace Avalonia.Styling
return _selectorString;
}
protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
{
var innerResult = _argument.Match(control, parent, subscribe);
@ -66,7 +66,7 @@ namespace Avalonia.Styling
}
}
protected override Selector? MovePrevious() => _previous;
protected override Selector? MovePreviousOrParent() => _previous;
private protected override Selector? MovePrevious() => _previous;
private protected override Selector? MovePreviousOrParent() => _previous;
}
}

14
src/Avalonia.Base/Styling/NthChildSelector.cs

@ -12,7 +12,7 @@ namespace Avalonia.Styling
/// <remarks>
/// Element indices are 1-based.
/// </remarks>
public class NthChildSelector : Selector
internal class NthChildSelector : Selector
{
private const string NthChildSelectorName = "nth-child";
private const string NthLastChildSelectorName = "nth-last-child";
@ -39,16 +39,16 @@ namespace Avalonia.Styling
}
public override bool InTemplate => _previous?.InTemplate ?? false;
internal override bool InTemplate => _previous?.InTemplate ?? false;
public override bool IsCombinator => false;
internal override bool IsCombinator => false;
public override Type? TargetType => _previous?.TargetType;
internal override Type? TargetType => _previous?.TargetType;
public int Step { get; }
public int Offset { get; }
protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
{
if (!(control is ILogical logical))
{
@ -103,8 +103,8 @@ namespace Avalonia.Styling
return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
}
protected override Selector? MovePrevious() => _previous;
protected override Selector? MovePreviousOrParent() => _previous;
private protected override Selector? MovePrevious() => _previous;
private protected override Selector? MovePreviousOrParent() => _previous;
public override string ToString(Style? owner)
{

2
src/Avalonia.Base/Styling/NthLastChildSelector.cs

@ -8,7 +8,7 @@ namespace Avalonia.Styling
/// <remarks>
/// Element indices are 1-based.
/// </remarks>
public class NthLastChildSelector : NthChildSelector
internal class NthLastChildSelector : NthChildSelector
{
/// <summary>
/// Creates an instance of <see cref="NthLastChildSelector"/>

12
src/Avalonia.Base/Styling/OrSelector.cs

@ -36,13 +36,13 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
public override bool InTemplate => false;
internal override bool InTemplate => false;
/// <inheritdoc/>
public override bool IsCombinator => false;
internal override bool IsCombinator => false;
/// <inheritdoc/>
public override Type? TargetType => _targetType ??= EvaluateTargetType();
internal override Type? TargetType => _targetType ??= EvaluateTargetType();
/// <inheritdoc/>
public override string ToString(Style? owner)
@ -55,7 +55,7 @@ namespace Avalonia.Styling
return _selectorString;
}
protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
{
var activators = new OrActivatorBuilder();
var neverThisInstance = false;
@ -94,8 +94,8 @@ namespace Avalonia.Styling
}
}
protected override Selector? MovePrevious() => null;
protected override Selector? MovePreviousOrParent() => null;
private protected override Selector? MovePrevious() => null;
private protected override Selector? MovePreviousOrParent() => null;
internal override void ValidateNestingSelector(bool inControlTheme)
{

12
src/Avalonia.Base/Styling/PropertyEqualsSelector.cs

@ -28,13 +28,13 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
public override bool InTemplate => _previous?.InTemplate ?? false;
internal override bool InTemplate => _previous?.InTemplate ?? false;
/// <inheritdoc/>
public override bool IsCombinator => false;
internal override bool IsCombinator => false;
/// <inheritdoc/>
public override Type? TargetType => _previous?.TargetType;
internal override Type? TargetType => _previous?.TargetType;
/// <inheritdoc/>
public override string ToString(Style? owner)
@ -73,7 +73,7 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
{
if (subscribe)
{
@ -88,8 +88,8 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
protected override Selector? MovePreviousOrParent() => _previous;
private protected override Selector? MovePrevious() => _previous;
private protected override Selector? MovePreviousOrParent() => _previous;
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.TypeConvertionSupressWarningMessage)]
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = TrimmingMessages.TypeConvertionSupressWarningMessage)]

14
src/Avalonia.Base/Styling/Selector.cs

@ -14,7 +14,7 @@ namespace Avalonia.Styling
/// Gets a value indicating whether either this selector or a previous selector has moved
/// into a template.
/// </summary>
public abstract bool InTemplate { get; }
internal abstract bool InTemplate { get; }
/// <summary>
/// Gets a value indicating whether this selector is a combinator.
@ -22,12 +22,12 @@ namespace Avalonia.Styling
/// <remarks>
/// A combinator is a selector such as Child or Descendent which links simple selectors.
/// </remarks>
public abstract bool IsCombinator { get; }
internal abstract bool IsCombinator { get; }
/// <summary>
/// Gets the target type of the selector, if available.
/// </summary>
public abstract Type? TargetType { get; }
internal abstract Type? TargetType { get; }
/// <summary>
/// Tries to match the selector with a control.
@ -41,7 +41,7 @@ namespace Avalonia.Styling
/// or simply return an immediate result.
/// </param>
/// <returns>A <see cref="SelectorMatch"/>.</returns>
public SelectorMatch Match(StyledElement control, IStyle? parent = null, bool subscribe = true)
internal SelectorMatch Match(StyledElement control, IStyle? parent = null, bool subscribe = true)
{
// First match the selector until a combinator is found. Selectors are stored from
// right-to-left, so MatchUntilCombinator reverses this order because the type selector
@ -88,17 +88,17 @@ namespace Avalonia.Styling
/// or simply return an immediate result.
/// </param>
/// <returns>A <see cref="SelectorMatch"/>.</returns>
protected abstract SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe);
private protected abstract SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe);
/// <summary>
/// Moves to the previous selector.
/// </summary>
protected abstract Selector? MovePrevious();
private protected abstract Selector? MovePrevious();
/// <summary>
/// Moves to the previous selector or the parent selector.
/// </summary>
protected abstract Selector? MovePreviousOrParent();
private protected abstract Selector? MovePreviousOrParent();
internal virtual void ValidateNestingSelector(bool inControlTheme)
{

4
src/Avalonia.Base/Styling/SelectorMatch.cs

@ -8,7 +8,7 @@ namespace Avalonia.Styling
/// <summary>
/// Describes how a <see cref="SelectorMatch"/> matches a control and its type.
/// </summary>
public enum SelectorMatchResult
internal enum SelectorMatchResult
{
/// <summary>
/// The selector never matches this type.
@ -43,7 +43,7 @@ namespace Avalonia.Styling
/// A selector match describes whether and how a <see cref="Selector"/> matches a control, and
/// in addition whether the selector can ever match a control of the same type.
/// </remarks>
public readonly record struct SelectorMatch
internal readonly record struct SelectorMatch
{
/// <summary>
/// A selector match with the result of <see cref="SelectorMatchResult.NeverThisType"/>.

4
src/Avalonia.Base/Styling/Setter.cs

@ -14,7 +14,7 @@ namespace Avalonia.Styling
/// A <see cref="Setter"/> is used to set a <see cref="AvaloniaProperty"/> value on a
/// <see cref="AvaloniaObject"/> depending on a condition.
/// </remarks>
public class Setter : ISetter, IValueEntry, ISetterInstance, IAnimationSetter
public class Setter : SetterBase, IValueEntry, ISetterInstance, IAnimationSetter
{
private object? _value;
private DirectPropertySetterInstance? _direct;
@ -66,7 +66,7 @@ namespace Avalonia.Styling
void IValueEntry.Unsubscribe() { }
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
ISetterInstance ISetter.Instance(IStyleInstance instance, StyledElement target)
internal override ISetterInstance Instance(IStyleInstance instance, StyledElement target)
{
if (target is not AvaloniaObject ao)
throw new InvalidOperationException("Don't know how to instance a style on this type.");

12
src/Avalonia.Base/Styling/SetterBase.cs

@ -0,0 +1,12 @@
namespace Avalonia.Styling
{
/// <summary>
/// Represents the base class for value setters.
/// </summary>
public abstract class SetterBase
{
internal abstract ISetterInstance Instance(
IStyleInstance styleInstance,
StyledElement target);
}
}

6
src/Avalonia.Base/Styling/StyleBase.cs

@ -16,7 +16,7 @@ namespace Avalonia.Styling
private IResourceHost? _owner;
private StyleChildren? _children;
private IResourceDictionary? _resources;
private List<ISetter>? _setters;
private List<SetterBase>? _setters;
private List<IAnimation>? _animations;
private StyleInstance? _sharedInstance;
@ -60,7 +60,7 @@ namespace Avalonia.Styling
}
}
public IList<ISetter> Setters => _setters ??= new List<ISetter>();
public IList<SetterBase> Setters => _setters ??= new();
public IList<IAnimation> Animations => _animations ??= new List<IAnimation>();
bool IResourceNode.HasResources => _resources?.Count > 0;
@ -69,7 +69,7 @@ namespace Avalonia.Styling
internal bool HasChildren => _children?.Count > 0;
internal bool HasSettersOrAnimations => _setters?.Count > 0 || _animations?.Count > 0;
public void Add(ISetter setter) => Setters.Add(setter);
public void Add(SetterBase setter) => Setters.Add(setter);
public void Add(IStyle style) => Children.Add(style);
public event EventHandler? OwnerChanged;

12
src/Avalonia.Base/Styling/TemplateSelector.cs

@ -18,13 +18,13 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
public override bool InTemplate => true;
internal override bool InTemplate => true;
/// <inheritdoc/>
public override bool IsCombinator => true;
internal override bool IsCombinator => true;
/// <inheritdoc/>
public override Type? TargetType => null;
internal override Type? TargetType => null;
public override string ToString(Style? owner)
{
@ -36,7 +36,7 @@ namespace Avalonia.Styling
return _selectorString;
}
protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
{
var templatedParent = control.TemplatedParent as StyledElement;
@ -48,7 +48,7 @@ namespace Avalonia.Styling
return _parent.Match(templatedParent, parent, subscribe);
}
protected override Selector? MovePrevious() => null;
protected override Selector? MovePreviousOrParent() => _parent;
private protected override Selector? MovePrevious() => null;
private protected override Selector? MovePreviousOrParent() => _parent;
}
}

12
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@ -58,7 +58,7 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
public override bool InTemplate => _previous?.InTemplate ?? false;
internal override bool InTemplate => _previous?.InTemplate ?? false;
/// <summary>
/// Gets the name of the control to match.
@ -66,10 +66,10 @@ namespace Avalonia.Styling
public string? Name { get; set; }
/// <inheritdoc/>
public override Type? TargetType => _targetType ?? _previous?.TargetType;
internal override Type? TargetType => _targetType ?? _previous?.TargetType;
/// <inheritdoc/>
public override bool IsCombinator => false;
internal override bool IsCombinator => false;
/// <summary>
/// Whether the selector matches the concrete <see cref="TargetType"/> or any object which
@ -89,7 +89,7 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
{
if (TargetType != null)
{
@ -134,8 +134,8 @@ namespace Avalonia.Styling
return Name == null ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance;
}
protected override Selector? MovePrevious() => _previous;
protected override Selector? MovePreviousOrParent() => _previous;
private protected override Selector? MovePrevious() => _previous;
private protected override Selector? MovePreviousOrParent() => _previous;
private string BuildSelectorString(Style? owner)
{

72
src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs

@ -41,11 +41,19 @@ namespace Avalonia.Controls.Primitives
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="IsAlphaMaxForced"/> property.
/// Defines the <see cref="IsAlphaVisible"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsAlphaMaxForcedProperty =
public static readonly StyledProperty<bool> IsAlphaVisibleProperty =
AvaloniaProperty.Register<ColorSlider, bool>(
nameof(IsAlphaMaxForced),
nameof(IsAlphaVisible),
false);
/// <summary>
/// Defines the <see cref="IsPerceptive"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsPerceptiveProperty =
AvaloniaProperty.Register<ColorSlider, bool>(
nameof(IsPerceptive),
true);
/// <summary>
@ -56,14 +64,6 @@ namespace Avalonia.Controls.Primitives
nameof(IsRoundingEnabled),
false);
/// <summary>
/// Defines the <see cref="IsSaturationValueMaxForced"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsSaturationValueMaxForcedProperty =
AvaloniaProperty.Register<ColorSlider, bool>(
nameof(IsSaturationValueMaxForced),
true);
/// <summary>
/// Gets or sets the currently selected color in the RGB color model.
/// </summary>
@ -109,14 +109,41 @@ namespace Avalonia.Controls.Primitives
}
/// <summary>
/// Gets or sets a value indicating whether the alpha component is always forced to maximum for components
/// other than <see cref="ColorComponent"/>.
/// This ensures that the background is always visible and never transparent regardless of the actual color.
/// Gets or sets a value indicating whether the alpha component is visible and rendered.
/// When false, this ensures that the gradient is always visible and never transparent regardless of
/// the actual color. This property is ignored when the alpha component itself is being displayed.
/// </summary>
/// <remarks>
/// Setting to false means the alpha component is always forced to maximum for components other than
/// <see cref="ColorComponent"/> during rendering. This doesn't change the value of the alpha component
/// in the color – it is only for display.
/// </remarks>
public bool IsAlphaVisible
{
get => GetValue(IsAlphaVisibleProperty);
set => SetValue(IsAlphaVisibleProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the slider adapts rendering to improve user-perception
/// over exactness.
/// </summary>
public bool IsAlphaMaxForced
/// <remarks>
/// When true in the HSVA color model, this ensures that the gradient is always visible and
/// never washed out regardless of the actual color. When true in the RGBA color model, this ensures
/// the gradient always appears as red, green or blue.
/// <br/><br/>
/// For example, with Hue in the HSVA color model, the Saturation and Value components are always forced
/// to maximum values during rendering. In the RGBA color model, all components other than
/// <see cref="ColorComponent"/> are forced to minimum values during rendering.
/// <br/><br/>
/// Note this property will only adjust components other than <see cref="ColorComponent"/> during rendering.
/// This also doesn't change the values of any components in the actual color – it is only for display.
/// </remarks>
public bool IsPerceptive
{
get => GetValue(IsAlphaMaxForcedProperty);
set => SetValue(IsAlphaMaxForcedProperty, value);
get => GetValue(IsPerceptiveProperty);
set => SetValue(IsPerceptiveProperty, value);
}
/// <summary>
@ -131,16 +158,5 @@ namespace Avalonia.Controls.Primitives
get => GetValue(IsRoundingEnabledProperty);
set => SetValue(IsRoundingEnabledProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values
/// when using the HSVA color model. Only component values other than <see cref="ColorComponent"/> will be changed.
/// This ensures, for example, that the Hue background is always visible and never washed out regardless of the actual color.
/// </summary>
public bool IsSaturationValueMaxForced
{
get => GetValue(IsSaturationValueMaxForcedProperty);
set => SetValue(IsSaturationValueMaxForcedProperty, value);
}
}
}

88
src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs

@ -52,8 +52,7 @@ namespace Avalonia.Controls.Primitives
// This means under a certain alpha threshold, neither a white or black selector thumb
// should be shown and instead the default slider thumb color should be used instead.
if (Color.A < 128 &&
(IsAlphaMaxForced == false ||
ColorComponent == ColorComponent.Alpha))
(IsAlphaVisible || ColorComponent == ColorComponent.Alpha))
{
PseudoClasses.Set(pcDarkSelector, false);
PseudoClasses.Set(pcLightSelector, false);
@ -64,11 +63,11 @@ namespace Avalonia.Controls.Primitives
if (ColorModel == ColorModel.Hsva)
{
perceivedColor = GetEquivalentBackgroundColor(HsvColor).ToRgb();
perceivedColor = GetPerceptiveBackgroundColor(HsvColor).ToRgb();
}
else
{
perceivedColor = GetEquivalentBackgroundColor(Color);
perceivedColor = GetPerceptiveBackgroundColor(Color);
}
if (ColorHelper.GetRelativeLuminance(perceivedColor) <= 0.5)
@ -108,7 +107,7 @@ namespace Avalonia.Controls.Primitives
{
// As a fallback, attempt to calculate using the overall control size
// This shouldn't happen as a track is a required template part of a slider
// However, if it does, the spectrum will still be shown
// However, if it does, the spectrum gradient will still be shown
pixelWidth = Convert.ToInt32(Bounds.Width * scale);
pixelHeight = Convert.ToInt32(Bounds.Height * scale);
}
@ -122,8 +121,8 @@ namespace Avalonia.Controls.Primitives
ColorModel,
ColorComponent,
HsvColor,
IsAlphaMaxForced,
IsSaturationValueMaxForced);
IsAlphaVisible,
IsPerceptive);
if (_backgroundBitmap != null)
{
@ -316,40 +315,35 @@ namespace Avalonia.Controls.Primitives
/// </summary>
/// <param name="hsvColor">The actual color to get the equivalent background color for.</param>
/// <returns>The equivalent, perceived background color.</returns>
private HsvColor GetEquivalentBackgroundColor(HsvColor hsvColor)
private HsvColor GetPerceptiveBackgroundColor(HsvColor hsvColor)
{
var component = ColorComponent;
var isAlphaMaxForced = IsAlphaMaxForced;
var isSaturationValueMaxForced = IsSaturationValueMaxForced;
var isAlphaVisible = IsAlphaVisible;
var isPerceptive = IsPerceptive;
if (isAlphaMaxForced &&
if (isAlphaVisible == false &&
component != ColorComponent.Alpha)
{
hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V);
}
switch (component)
if (isPerceptive)
{
case ColorComponent.Component1:
return new HsvColor(
hsvColor.A,
hsvColor.H,
isSaturationValueMaxForced ? 1.0 : hsvColor.S,
isSaturationValueMaxForced ? 1.0 : hsvColor.V);
case ColorComponent.Component2:
return new HsvColor(
hsvColor.A,
hsvColor.H,
hsvColor.S,
isSaturationValueMaxForced ? 1.0 : hsvColor.V);
case ColorComponent.Component3:
return new HsvColor(
hsvColor.A,
hsvColor.H,
isSaturationValueMaxForced ? 1.0 : hsvColor.S,
hsvColor.V);
default:
return hsvColor;
switch (component)
{
case ColorComponent.Component1:
return new HsvColor(hsvColor.A, hsvColor.H, 1.0, 1.0);
case ColorComponent.Component2:
return new HsvColor(hsvColor.A, hsvColor.H, hsvColor.S, 1.0);
case ColorComponent.Component3:
return new HsvColor(hsvColor.A, hsvColor.H, 1.0, hsvColor.V);
default:
return hsvColor;
}
}
else
{
return hsvColor;
}
}
@ -359,18 +353,36 @@ namespace Avalonia.Controls.Primitives
/// </summary>
/// <param name="rgbColor">The actual color to get the equivalent background color for.</param>
/// <returns>The equivalent, perceived background color.</returns>
private Color GetEquivalentBackgroundColor(Color rgbColor)
private Color GetPerceptiveBackgroundColor(Color rgbColor)
{
var component = ColorComponent;
var isAlphaMaxForced = IsAlphaMaxForced;
var isAlphaVisible = IsAlphaVisible;
var isPerceptive = IsPerceptive;
if (isAlphaMaxForced &&
if (isAlphaVisible == false &&
component != ColorComponent.Alpha)
{
rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B);
}
return rgbColor;
if (isPerceptive)
{
switch (component)
{
case ColorComponent.Component1:
return new Color(rgbColor.A, rgbColor.R, 0, 0);
case ColorComponent.Component2:
return new Color(rgbColor.A, 0, rgbColor.G, 0);
case ColorComponent.Component3:
return new Color(rgbColor.A, 0, 0, rgbColor.B);
default:
return rgbColor;
}
}
else
{
return rgbColor;
}
}
/// <inheritdoc/>
@ -401,8 +413,8 @@ namespace Avalonia.Controls.Primitives
}
else if (change.Property == ColorComponentProperty ||
change.Property == ColorModelProperty ||
change.Property == IsAlphaMaxForcedProperty ||
change.Property == IsSaturationValueMaxForcedProperty)
change.Property == IsAlphaVisibleProperty ||
change.Property == IsPerceptiveProperty)
{
ignorePropertyChanged = true;

91
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs

@ -45,6 +45,7 @@ namespace Avalonia.Controls.Primitives
private bool _updatingColor = false;
private bool _updatingHsvColor = false;
private bool _coercedInitialColor = false;
private bool _isPointerPressed = false;
private bool _shouldShowLargeSelection = false;
private List<Hsv> _hsvValues = new List<Hsv>();
@ -601,14 +602,102 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Changes the currently selected color (always in HSV) and applies all necessary updates.
/// </summary>
/// <remarks>
/// Some additional logic is applied in certain situations to coerce and sync color values.
/// Use this method instead of update the <see cref="Color"/> or <see cref="HsvColor"/> directly.
/// </remarks>
/// <param name="newHsv">The new HSV color to change to.</param>
private void UpdateColor(Hsv newHsv)
{
_updatingColor = true;
_updatingHsvColor = true;
Rgb newRgb = newHsv.ToRgb();
double alpha = HsvColor.A;
// It is common for the ColorPicker (and therefore the Spectrum) to be initialized
// with a #00000000 color value in some use cases. This is usually used to indicate
// that no color has been selected by the user. Note that #00000000 is different than
// #00FFFFFF (Transparent).
//
// In this situation, the first time the user clicks on the spectrum the third
// component and alpha component will remain zero. This is because the spectrum only
// controls two components at any given time.
//
// This is very unintuitive from a user-standpoint as after the user clicks on the
// spectrum they must then increase the alpha and then the third component sliders
// to the desired value. In fact, until they increase these slider values no color
// will show at all since it is fully transparent and black. In almost all cases
// though the desired value is simply full color.
//
// To work around this usability issue with an initial #00000000 color, the selected
// color is coerced (only the first time) into a color with maximum third component
// value and maximum alpha. This can only happen once and only if those two components
// are already zero.
//
// Also note this is NOT currently done for #00FFFFFF (Transparent) but based on
// further usability study that case may need to be handled here as well. Right now
// Transparent is treated as a normal color value with the alpha intentionally set
// to zero so the alpha slider must still be adjusted after the spectrum.
if (!_coercedInitialColor &&
IsLoaded)
{
bool isAlphaComponentZero = (alpha == 0.0);
bool isThirdComponentZero = false;
switch (Components)
{
case ColorSpectrumComponents.HueValue:
case ColorSpectrumComponents.ValueHue:
isThirdComponentZero = (newHsv.S == 0.0);
break;
case ColorSpectrumComponents.HueSaturation:
case ColorSpectrumComponents.SaturationHue:
isThirdComponentZero = (newHsv.V == 0.0);
break;
case ColorSpectrumComponents.ValueSaturation:
case ColorSpectrumComponents.SaturationValue:
isThirdComponentZero = (newHsv.H == 0.0);
break;
}
if (isAlphaComponentZero && isThirdComponentZero)
{
alpha = 1.0;
switch (Components)
{
case ColorSpectrumComponents.HueValue:
case ColorSpectrumComponents.ValueHue:
newHsv.S = 1.0;
break;
case ColorSpectrumComponents.HueSaturation:
case ColorSpectrumComponents.SaturationHue:
newHsv.V = 1.0;
break;
case ColorSpectrumComponents.ValueSaturation:
case ColorSpectrumComponents.SaturationValue:
// Hue is mathematically NOT a special case; however, is one conceptually.
// It doesn't make sense to change the selected Hue value, so why is it set here?
// Setting to 360.0 is equivalent to the max set for other components and is
// internally wrapped back to 0.0 (since 360 degrees = 0 degrees).
// This means effectively there is no change to the hue component value.
newHsv.H = 360.0;
break;
}
_coercedInitialColor = true;
}
}
Rgb newRgb = newHsv.ToRgb();
SetCurrentValue(ColorProperty, newRgb.ToColor(alpha));
SetCurrentValue(HsvColorProperty, newHsv.ToHsvColor(alpha));

6
src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs

@ -504,6 +504,12 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the index of the selected tab/panel/page (subview).
/// </summary>
/// <remarks>
/// When using the default control theme, this property is designed to be used with the
/// <see cref="ColorViewTab"/> enum. The <see cref="ColorViewTab"/> enum defines the
/// index values of each of the three standard tabs.
/// Use like `SelectedIndex = (int)ColorViewTab.Palette`.
/// </remarks>
public int SelectedIndex
{
get => GetValue(SelectedIndexProperty);

60
src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs

@ -29,11 +29,10 @@ namespace Avalonia.Controls.Primitives
/// <param name="colorModel">The color model being used: RGBA or HSVA.</param>
/// <param name="component">The specific color component to sweep.</param>
/// <param name="baseHsvColor">The base HSV color used for components not being changed.</param>
/// <param name="isAlphaMaxForced">Fix the alpha component value to maximum during calculation.
/// This will remove any alpha/transparency from the other component backgrounds.</param>
/// <param name="isSaturationValueMaxForced">Fix the saturation and value components to maximum
/// during calculation with the HSVA color model.
/// This will ensure colors are always discernible regardless of saturation/value.</param>
/// <param name="isAlphaVisible">Whether the alpha component is visible and rendered in the bitmap.
/// This property is ignored when the alpha component itself is being rendered.</param>
/// <param name="isPerceptive">Whether the slider adapts rendering to improve user-perception over exactness.
/// This will ensure colors are always discernible.</param>
/// <returns>A new bitmap representing a gradient of color component values.</returns>
public static async Task<ArrayList<byte>> CreateComponentBitmapAsync(
int width,
@ -42,8 +41,8 @@ namespace Avalonia.Controls.Primitives
ColorModel colorModel,
ColorComponent component,
HsvColor baseHsvColor,
bool isAlphaMaxForced,
bool isSaturationValueMaxForced)
bool isAlphaVisible,
bool isPerceptive)
{
if (width == 0 || height == 0)
{
@ -67,7 +66,7 @@ namespace Avalonia.Controls.Primitives
bgraPixelDataWidth = width * 4;
// Maximize alpha component value
if (isAlphaMaxForced &&
if (isAlphaVisible == false &&
component != ColorComponent.Alpha)
{
baseHsvColor = new HsvColor(1.0, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V);
@ -79,22 +78,41 @@ namespace Avalonia.Controls.Primitives
baseRgbColor = baseHsvColor.ToRgb();
}
// Maximize Saturation and Value components when in HSVA mode
if (isSaturationValueMaxForced &&
colorModel == ColorModel.Hsva &&
// Apply any perceptive adjustments to the color
if (isPerceptive &&
component != ColorComponent.Alpha)
{
switch (component)
if (colorModel == ColorModel.Hsva)
{
case ColorComponent.Component1:
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, 1.0);
break;
case ColorComponent.Component2:
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, 1.0);
break;
case ColorComponent.Component3:
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, baseHsvColor.V);
break;
// Maximize Saturation and Value components
switch (component)
{
case ColorComponent.Component1: // Hue
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, 1.0);
break;
case ColorComponent.Component2: // Saturation
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, 1.0);
break;
case ColorComponent.Component3: // Value
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, baseHsvColor.V);
break;
}
}
else
{
// Minimize component values other than the current one
switch (component)
{
case ColorComponent.Component1: // Red
baseRgbColor = new Color(baseRgbColor.A, baseRgbColor.R, 0, 0);
break;
case ColorComponent.Component2: // Green
baseRgbColor = new Color(baseRgbColor.A, 0, baseRgbColor.G, 0);
break;
case ColorComponent.Component3: // Blue
baseRgbColor = new Color(baseRgbColor.A, 0, 0, baseRgbColor.B);
break;
}
}
}

2
src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs

@ -11,7 +11,7 @@ namespace Avalonia.Controls.Primitives
/// Contains and allows modification of Hue, Saturation and Value components.
/// </summary>
/// <remarks>
/// The is a specialized struct optimized for permanence and memory:
/// The is a specialized struct optimized for performance and memory:
/// <list type="bullet">
/// <item>This is not a read-only struct like <see cref="HsvColor"/> and allows editing the fields</item>
/// <item>Removes the alpha component unnecessary in core calculations</item>

2
src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs

@ -12,7 +12,7 @@ namespace Avalonia.Controls.Primitives
/// Contains and allows modification of Red, Green and Blue components.
/// </summary>
/// <remarks>
/// The is a specialized struct optimized for permanence and memory:
/// The is a specialized struct optimized for performance and memory:
/// <list type="bullet">
/// <item>This is not a read-only struct like <see cref="Color"/> and allows editing the fields</item>
/// <item>Removes the alpha component unnecessary in core calculations</item>

25
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

@ -113,8 +113,8 @@
<primitives:ColorSlider x:Name="ColorSpectrumThirdComponentSlider"
AutomationProperties.Name="Third Component"
Grid.Column="0"
IsAlphaMaxForced="True"
IsSaturationValueMaxForced="False"
IsAlphaVisible="False"
IsPerceptive="True"
Orientation="Vertical"
ColorModel="Hsva"
ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}"
@ -490,11 +490,11 @@
</TabItem>
</TabControl>
<!-- Previewer -->
<!-- Note that top/bottom margins have -5 to remove for drop shadow padding -->
<!-- Note that the drop shadow is allowed to extend past the control bounds -->
<primitives:ColorPreviewer Grid.Row="1"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
Margin="12,-5,12,7"
Margin="12,0,12,12"
IsVisible="{TemplateBinding IsColorPreviewVisible}" />
</Grid>
</Flyout>
@ -502,6 +502,23 @@
</DropDownButton>
</ControlTemplate>
</Setter>
<!--
<Style Selector="^ /template/ primitives|ColorSlider#ColorSpectrumThirdComponentSlider[ColorComponent=Component1]">
<Setter Property="IsPerceptive" Value="True" />
</Style>
<Style Selector="^ /template/ primitives|ColorSlider#Component1Slider[ColorModel=Rgba]">
<Setter Property="IsPerceptive" Value="False" />
</Style>
<Style Selector="^ /template/ primitives|ColorSlider#Component2Slider[ColorModel=Rgba]">
<Setter Property="IsPerceptive" Value="False" />
</Style>
<Style Selector="^ /template/ primitives|ColorSlider#Component3Slider[ColorModel=Rgba]">
<Setter Property="IsPerceptive" Value="False" />
</Style>
-->
</ControlTheme>
</ResourceDictionary>

13
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml

@ -8,7 +8,9 @@
<ControlTheme x:Key="{x:Type ColorPreviewer}"
TargetType="ColorPreviewer">
<Setter Property="Height" Value="70" />
<Setter Property="Height" Value="50" />
<!-- The preview color drop shadow is allowed to extend outside the control bounds -->
<Setter Property="ClipToBounds" Value="False" />
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorPreviewer}">
@ -21,7 +23,6 @@
Height="{StaticResource ColorPreviewerAccentSectionHeight}"
Width="{StaticResource ColorPreviewerAccentSectionWidth}"
ColumnDefinitions="*,*"
Margin="0,0,-10,0"
VerticalAlignment="Center">
<Border Grid.Column="0"
Grid.ColumnSpan="2"
@ -43,7 +44,6 @@
Height="{StaticResource ColorPreviewerAccentSectionHeight}"
Width="{StaticResource ColorPreviewerAccentSectionWidth}"
ColumnDefinitions="*,*"
Margin="-10,0,0,0"
VerticalAlignment="Center">
<Border Grid.Column="0"
Grid.ColumnSpan="2"
@ -64,10 +64,8 @@
<Border Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
BoxShadow="0 0 10 2 #BF000000"
CornerRadius="{TemplateBinding CornerRadius}"
Margin="10">
CornerRadius="{TemplateBinding CornerRadius}">
<Panel>
<Border Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
CornerRadius="{TemplateBinding CornerRadius}" />
@ -82,8 +80,7 @@
<Border CornerRadius="{TemplateBinding CornerRadius}"
IsVisible="{TemplateBinding IsAccentColorsVisible, Converter={x:Static BoolConverters.Not}}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="0,10,0,10">
VerticalAlignment="Stretch">
<Panel>
<Border Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
CornerRadius="{TemplateBinding CornerRadius}" />

25
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@ -360,8 +360,8 @@
<primitives:ColorSlider x:Name="ColorSpectrumThirdComponentSlider"
AutomationProperties.Name="Third Component"
Grid.Column="0"
IsAlphaMaxForced="True"
IsSaturationValueMaxForced="False"
IsAlphaVisible="False"
IsPerceptive="True"
Orientation="Vertical"
ColorModel="Hsva"
ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}"
@ -737,15 +737,32 @@
</TabItem>
</TabControl>
<!-- Previewer -->
<!-- Note that top/bottom margins have -5 to remove for drop shadow padding -->
<!-- Note that the drop shadow is allowed to extend past the control bounds -->
<primitives:ColorPreviewer Grid.Row="1"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
Margin="12,-5,12,7"
Margin="12,0,12,12"
IsVisible="{TemplateBinding IsColorPreviewVisible}" />
</Grid>
</ControlTemplate>
</Setter>
<!--
<Style Selector="^ /template/ primitives|ColorSlider#ColorSpectrumThirdComponentSlider[ColorComponent=Component1]">
<Setter Property="IsPerceptive" Value="True" />
</Style>
<Style Selector="^ /template/ primitives|ColorSlider#Component1Slider[ColorModel=Rgba]">
<Setter Property="IsPerceptive" Value="False" />
</Style>
<Style Selector="^ /template/ primitives|ColorSlider#Component2Slider[ColorModel=Rgba]">
<Setter Property="IsPerceptive" Value="False" />
</Style>
<Style Selector="^ /template/ primitives|ColorSlider#Component3Slider[ColorModel=Rgba]">
<Setter Property="IsPerceptive" Value="False" />
</Style>
-->
</ControlTheme>
</ResourceDictionary>

25
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml

@ -112,8 +112,8 @@
<primitives:ColorSlider x:Name="ColorSpectrumThirdComponentSlider"
AutomationProperties.Name="Third Component"
Grid.Column="0"
IsAlphaMaxForced="True"
IsSaturationValueMaxForced="False"
IsAlphaVisible="False"
IsPerceptive="True"
Orientation="Vertical"
ColorModel="Hsva"
ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}"
@ -489,11 +489,11 @@
</TabItem>
</TabControl>
<!-- Previewer -->
<!-- Note that top/bottom margins have -5 to remove for drop shadow padding -->
<!-- Note that the drop shadow is allowed to extend past the control bounds -->
<primitives:ColorPreviewer Grid.Row="1"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
Margin="12,-5,12,7"
Margin="12,0,12,12"
IsVisible="{TemplateBinding IsColorPreviewVisible}" />
</Grid>
</Flyout>
@ -501,6 +501,23 @@
</DropDownButton>
</ControlTemplate>
</Setter>
<!--
<Style Selector="^ /template/ primitives|ColorSlider#ColorSpectrumThirdComponentSlider[ColorComponent=Component1]">
<Setter Property="IsPerceptive" Value="True" />
</Style>
<Style Selector="^ /template/ primitives|ColorSlider#Component1Slider[ColorModel=Rgba]">
<Setter Property="IsPerceptive" Value="False" />
</Style>
<Style Selector="^ /template/ primitives|ColorSlider#Component2Slider[ColorModel=Rgba]">
<Setter Property="IsPerceptive" Value="False" />
</Style>
<Style Selector="^ /template/ primitives|ColorSlider#Component3Slider[ColorModel=Rgba]">
<Setter Property="IsPerceptive" Value="False" />
</Style>
-->
</ControlTheme>
</ResourceDictionary>

13
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml

@ -8,7 +8,9 @@
<ControlTheme x:Key="{x:Type ColorPreviewer}"
TargetType="ColorPreviewer">
<Setter Property="Height" Value="70" />
<Setter Property="Height" Value="50" />
<!-- The preview color drop shadow is allowed to extend outside the control bounds -->
<Setter Property="ClipToBounds" Value="False" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorPreviewer}">
@ -21,7 +23,6 @@
Height="{StaticResource ColorPreviewerAccentSectionHeight}"
Width="{StaticResource ColorPreviewerAccentSectionWidth}"
ColumnDefinitions="*,*"
Margin="0,0,-10,0"
VerticalAlignment="Center">
<Border Grid.Column="0"
Grid.ColumnSpan="2"
@ -43,7 +44,6 @@
Height="{StaticResource ColorPreviewerAccentSectionHeight}"
Width="{StaticResource ColorPreviewerAccentSectionWidth}"
ColumnDefinitions="*,*"
Margin="-10,0,0,0"
VerticalAlignment="Center">
<Border Grid.Column="0"
Grid.ColumnSpan="2"
@ -64,10 +64,8 @@
<Border Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
BoxShadow="0 0 10 2 #BF000000"
CornerRadius="{TemplateBinding CornerRadius}"
Margin="10">
CornerRadius="{TemplateBinding CornerRadius}">
<Panel>
<Border Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
CornerRadius="{TemplateBinding CornerRadius}" />
@ -82,8 +80,7 @@
<Border CornerRadius="{TemplateBinding CornerRadius}"
IsVisible="{TemplateBinding IsAccentColorsVisible, Converter={x:Static BoolConverters.Not}}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="0,10,0,10">
VerticalAlignment="Stretch">
<Panel>
<Border Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
CornerRadius="{TemplateBinding CornerRadius}" />

25
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml

@ -322,8 +322,8 @@
<primitives:ColorSlider x:Name="ColorSpectrumThirdComponentSlider"
AutomationProperties.Name="Third Component"
Grid.Column="0"
IsAlphaMaxForced="True"
IsSaturationValueMaxForced="False"
IsAlphaVisible="False"
IsPerceptive="True"
Orientation="Vertical"
ColorModel="Hsva"
ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}"
@ -699,15 +699,32 @@
</TabItem>
</TabControl>
<!-- Previewer -->
<!-- Note that top/bottom margins have -5 to remove for drop shadow padding -->
<!-- Note that the drop shadow is allowed to extend past the control bounds -->
<primitives:ColorPreviewer Grid.Row="1"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
Margin="12,-5,12,7"
Margin="12,0,12,12"
IsVisible="{TemplateBinding IsColorPreviewVisible}" />
</Grid>
</ControlTemplate>
</Setter>
<!--
<Style Selector="^ /template/ primitives|ColorSlider#ColorSpectrumThirdComponentSlider[ColorComponent=Component1]">
<Setter Property="IsPerceptive" Value="True" />
</Style>
<Style Selector="^ /template/ primitives|ColorSlider#Component1Slider[ColorModel=Rgba]">
<Setter Property="IsPerceptive" Value="False" />
</Style>
<Style Selector="^ /template/ primitives|ColorSlider#Component2Slider[ColorModel=Rgba]">
<Setter Property="IsPerceptive" Value="False" />
</Style>
<Style Selector="^ /template/ primitives|ColorSlider#Component3Slider[ColorModel=Rgba]">
<Setter Property="IsPerceptive" Value="False" />
</Style>
-->
</ControlTheme>
</ResourceDictionary>

2
src/Avalonia.Controls/Calendar/CalendarItem.cs

@ -171,7 +171,7 @@ namespace Avalonia.Controls.Primitives
var childCount = Calendar.RowsPerMonth + Calendar.RowsPerMonth * Calendar.ColumnsPerMonth;
using var children = new PooledList<Control>(childCount);
for (int i = 0; i < Calendar.RowsPerMonth; i++)
for (int i = 0; i < Calendar.ColumnsPerMonth; i++)
{
if (DayTitleTemplate?.Build() is Control cell)
{

2
src/Avalonia.Controls/ContextMenu.cs

@ -285,7 +285,7 @@ namespace Avalonia.Controls
}
}
void ISetterValue.Initialize(ISetter setter)
void ISetterValue.Initialize(SetterBase setter)
{
// ContextMenu can be assigned to the ContextMenu property in a setter. This overrides
// the behavior defined in Control which requires controls to be wrapped in a <template>.

2
src/Avalonia.Controls/Control.cs

@ -213,7 +213,7 @@ namespace Avalonia.Controls
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
/// <inheritdoc/>
void ISetterValue.Initialize(ISetter setter)
void ISetterValue.Initialize(SetterBase setter)
{
if (setter is Setter s && s.Property == ContextFlyoutProperty)
{

8
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -341,7 +341,7 @@ namespace Avalonia.Controls.Presenters
var top = 0d;
var left = 0.0;
var textHeight = TextLayout.Bounds.Height;
var textHeight = TextLayout.Height;
if (Bounds.Height < textHeight)
{
@ -571,14 +571,14 @@ namespace Avalonia.Controls.Presenters
InvalidateArrange();
var measuredSize = TextLayout.Bounds.Size;
var textWidth = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing;
return measuredSize;
return new Size(textWidth, TextLayout.Height);
}
protected override Size ArrangeOverride(Size finalSize)
{
var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
var textWidth = Math.Ceiling(TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing);
if (finalSize.Width < textWidth)
{

4
src/Avalonia.Controls/SelectableTextBlock.cs

@ -323,8 +323,8 @@ namespace Avalonia.Controls
var point = e.GetPosition(this) - new Point(padding.Left, padding.Top);
point = new Point(
MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.Bounds.Width, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Height, 0)));
MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Height, 0)));
var hit = TextLayout.HitTestPoint(point);
var textPosition = hit.TextPosition;

8
src/Avalonia.Controls/TextBlock.cs

@ -567,7 +567,7 @@ namespace Avalonia.Controls
var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
var top = padding.Top;
var textHeight = TextLayout.Bounds.Height;
var textHeight = TextLayout.Height;
if (Bounds.Height < textHeight)
{
@ -588,7 +588,7 @@ namespace Avalonia.Controls
protected virtual void RenderTextLayout(DrawingContext context, Point origin)
{
TextLayout.Draw(context, origin);
TextLayout.Draw(context, origin + new Point(TextLayout.OverhangLeading, 0));
}
private bool _clearTextInternal;
@ -702,7 +702,9 @@ namespace Avalonia.Controls
}
}
return TextLayout.Bounds.Size.Inflate(padding);
var width = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing;
return new Size(width, TextLayout.Height).Inflate(padding);
}
protected override Size ArrangeOverride(Size finalSize)

119
src/Avalonia.Controls/TextBox.cs

@ -19,6 +19,8 @@ using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Automation.Peers;
using Avalonia.Threading;
using Avalonia.Platform;
using System.Reflection;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Controls
{
@ -100,7 +102,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="SelectionStart"/> property
/// </summary>
public static readonly StyledProperty<int> SelectionStartProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(SelectionStart),
AvaloniaProperty.Register<TextBox, int>(nameof(SelectionStart),
coerce: CoerceCaretIndex);
/// <summary>
@ -475,7 +477,7 @@ namespace Avalonia.Controls
get => GetValue(SelectionEndProperty);
set => SetValue(SelectionEndProperty, value);
}
private void OnSelectionEndChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdateCommandStates();
@ -486,7 +488,7 @@ namespace Avalonia.Controls
SetCurrentValue(CaretIndexProperty, value);
}
}
/// <summary>
/// Gets or sets the maximum character length of the TextBox
/// </summary>
@ -536,7 +538,7 @@ namespace Avalonia.Controls
private static string? CoerceText(AvaloniaObject sender, string? value)
{
var textBox = (TextBox)sender;
// Before #9490, snapshot here was done AFTER text change - this doesn't make sense
// since intial state would never be no text and you'd always have to make a text
// change before undo would be available
@ -988,7 +990,7 @@ namespace Avalonia.Controls
{
var textBuilder = StringBuilderCache.Acquire(Math.Max(currentText.Length, newLength));
textBuilder.Append(currentText);
var caretIndex = CaretIndex;
if (selectionLength != 0)
@ -1050,7 +1052,7 @@ namespace Avalonia.Controls
if (clipboard == null)
return;
await clipboard.SetTextAsync(text);
DeleteSelection();
}
@ -1417,7 +1419,7 @@ namespace Avalonia.Controls
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (_presenter == null )
if (_presenter == null)
{
return;
}
@ -1434,11 +1436,10 @@ namespace Avalonia.Controls
_presenter.MoveCaretToPoint(point);
var index = _presenter.CaretIndex;
var caretIndex = _presenter.CaretIndex;
var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
SetCurrentValue(CaretIndexProperty, index);
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
switch (e.ClickCount)
{
@ -1447,49 +1448,44 @@ namespace Avalonia.Controls
{
if (_wordSelectionStart >= 0)
{
var previousWord = StringUtils.PreviousWord(text, index);
if (index > _wordSelectionStart)
{
SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
}
UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd);
if (index < _wordSelectionStart || previousWord == _wordSelectionStart)
{
SetCurrentValue(SelectionStartProperty, previousWord);
}
SetCurrentValue(SelectionStartProperty, selectionStart);
SetCurrentValue(SelectionEndProperty, selectionEnd);
}
else
{
SetCurrentValue(SelectionStartProperty, Math.Min(oldIndex, index));
SetCurrentValue(SelectionEndProperty, Math.Max(oldIndex, index));
SetCurrentValue(SelectionEndProperty, caretIndex);
}
}
else
{
if(_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd)
{
SetCurrentValue(SelectionStartProperty, index);
SetCurrentValue(SelectionEndProperty, index);
_wordSelectionStart = -1;
}
SetCurrentValue(SelectionStartProperty, caretIndex);
SetCurrentValue(SelectionEndProperty, caretIndex);
_wordSelectionStart = -1;
}
break;
case 2:
case 2:
if (!StringUtils.IsStartOfWord(text, index))
if (!StringUtils.IsStartOfWord(text, caretIndex))
{
SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, index));
selectionStart = StringUtils.PreviousWord(text, caretIndex);
}
_wordSelectionStart = SelectionStart;
if (!StringUtils.IsEndOfWord(text, caretIndex))
{
selectionEnd = StringUtils.NextWord(text, caretIndex);
}
if (!StringUtils.IsEndOfWord(text, index))
if (selectionStart != selectionEnd)
{
SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
_wordSelectionStart = selectionStart;
}
SetCurrentValue(SelectionStartProperty, selectionStart);
SetCurrentValue(SelectionEndProperty, selectionEnd);
break;
case 3:
_wordSelectionStart = -1;
@ -1519,30 +1515,19 @@ namespace Avalonia.Controls
MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
_presenter.MoveCaretToPoint(point);
_presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var text = Text;
if (text != null && _wordSelectionStart >= 0)
if (_wordSelectionStart >= 0)
{
var distance = caretIndex - _wordSelectionStart;
if (distance <= 0)
{
SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, caretIndex));
}
UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd);
if (distance >= 0)
{
if(SelectionStart != _wordSelectionStart)
{
SetCurrentValue(SelectionStartProperty, _wordSelectionStart);
}
SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, caretIndex));
}
SetCurrentValue(SelectionStartProperty, selectionStart);
SetCurrentValue(SelectionEndProperty, selectionEnd);
}
else
{
@ -1551,6 +1536,32 @@ namespace Avalonia.Controls
}
}
private void UpdateWordSelectionRange(int caretIndex, ref int selectionStart, ref int selectionEnd)
{
var text = Text;
if (string.IsNullOrEmpty(text))
{
return;
}
if (caretIndex > _wordSelectionStart)
{
var nextWord = StringUtils.NextWord(text, caretIndex);
selectionEnd = nextWord;
selectionStart = _wordSelectionStart;
}
else
{
var previousWord = StringUtils.PreviousWord(text, caretIndex);
selectionStart = previousWord;
selectionEnd = StringUtils.NextWord(text, _wordSelectionStart);
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
if (_presenter == null)

2
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -343,7 +343,7 @@ namespace Avalonia.Controls
{
var items = Items;
if (_isInLayout || index < 0 || index >= items.Count || _realizedElements is null)
if (_isInLayout || index < 0 || index >= items.Count || _realizedElements is null || !IsEffectivelyVisible)
return null;
if (GetRealizedElement(index) is Control element)

2
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -64,7 +64,7 @@ namespace Avalonia.Diagnostics.ViewModels
// We need to place styles without activator first, such styles will be overwritten by ones with activators.
foreach (var appliedStyle in styleDiagnostics.AppliedStyles.OrderBy(s => s.HasActivator))
{
var styleSource = appliedStyle.Source;
var styleSource = appliedStyle.Style;
var setters = new List<SetterViewModel>();

4
src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs

@ -5,11 +5,11 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class StyleViewModel : ViewModelBase
{
private readonly IStyleInstance _styleInstance;
private readonly AppliedStyle _styleInstance;
private bool _isActive;
private bool _isVisible;
public StyleViewModel(IStyleInstance styleInstance, string name, List<SetterViewModel> setters)
public StyleViewModel(AppliedStyle styleInstance, string name, List<SetterViewModel> setters)
{
_styleInstance = styleInstance;
IsVisible = true;

9
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -126,10 +126,9 @@ namespace Avalonia.Headless
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
IReadOnlyList<GlyphInfo> glyphInfos,
Point baselineOrigin,
Rect bounds)
Point baselineOrigin)
{
return new HeadlessGlyphRunStub(glyphTypeface, fontRenderingEmSize, baselineOrigin, bounds);
return new HeadlessGlyphRunStub(glyphTypeface, fontRenderingEmSize, baselineOrigin);
}
internal class HeadlessGlyphRunStub : IGlyphRunImpl
@ -137,13 +136,11 @@ namespace Avalonia.Headless
public HeadlessGlyphRunStub(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
Point baselineOrigin,
Rect bounds)
Point baselineOrigin)
{
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
BaselineOrigin = baselineOrigin;
Bounds =bounds;
}
public Rect Bounds { get; }

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs

@ -125,7 +125,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
var selfType = context.ParentNodes().OfType<XamlAstConstructableObjectNode>().First().Type.GetClrType();
// When using self bindings with setters we need to change target type to resolved selector type.
if (context.GetAvaloniaTypes().ISetter.IsAssignableFrom(selfType))
if (context.GetAvaloniaTypes().SetterBase.IsAssignableFrom(selfType))
{
selfType = context.ParentNodes().OfType<AvaloniaXamlIlTargetTypeMetadataNode>().First().TargetType.GetClrType();
}

4
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -104,7 +104,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType TextDecorationCollection { get; }
public IXamlType TextDecorations { get; }
public IXamlType TextTrimming { get; }
public IXamlType ISetter { get; }
public IXamlType SetterBase { get; }
public IXamlType IStyle { get; }
public IXamlType StyleInclude { get; }
public IXamlType ResourceInclude { get; }
@ -244,7 +244,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
TextDecorationCollection = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorationCollection");
TextDecorations = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorations");
TextTrimming = cfg.TypeSystem.GetType("Avalonia.Media.TextTrimming");
ISetter = cfg.TypeSystem.GetType("Avalonia.Styling.ISetter");
SetterBase = cfg.TypeSystem.GetType("Avalonia.Styling.SetterBase");
IStyle = cfg.TypeSystem.GetType("Avalonia.Styling.IStyle");
StyleInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.StyleInclude");
ResourceInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.ResourceInclude");

2
src/Markup/Avalonia.Markup/Data/TemplateBinding.cs

@ -109,7 +109,7 @@ namespace Avalonia.Data
}
/// <inheritdoc/>
void ISetterValue.Initialize(ISetter setter) => _isSetterValue = true;
void ISetterValue.Initialize(SetterBase setter) => _isSetterValue = true;
protected override void Subscribed()
{

36
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
@ -16,7 +17,7 @@ namespace Avalonia.Skia
private readonly Dictionary<SKFontEdging, SKTextBlob> _textBlobCache = new(1);
public GlyphRunImpl(IGlyphTypeface glyphTypeface, double fontRenderingEmSize,
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds)
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
{
if (glyphTypeface == null)
{
@ -48,9 +49,30 @@ namespace Avalonia.Skia
currentX += glyphInfos[i].GlyphAdvance;
}
_glyphTypefaceImpl.SKFont.Size = (float)fontRenderingEmSize;
var runBounds = new Rect();
var glyphBounds = ArrayPool<SKRect>.Shared.Rent(glyphInfos.Count);
_glyphTypefaceImpl.SKFont.GetGlyphWidths(_glyphIndices, null, glyphBounds);
currentX = 0;
for (var i = 0; i < glyphInfos.Count; i++)
{
var gBounds = glyphBounds[i];
var advance = glyphInfos[i].GlyphAdvance;
runBounds = runBounds.Union(new Rect(currentX + gBounds.Left, baselineOrigin.Y + gBounds.Top, gBounds.Width, gBounds.Height));
currentX += advance;
}
ArrayPool<SKRect>.Shared.Return(glyphBounds);
FontRenderingEmSize = fontRenderingEmSize;
BaselineOrigin = baselineOrigin;
Bounds = bounds;
Bounds = runBounds;
}
public IGlyphTypeface GlyphTypeface => _glyphTypefaceImpl;
@ -83,16 +105,12 @@ namespace Avalonia.Skia
return textBlob;
}
var font = SKFontCache.Shared.Get();
var font = _glyphTypefaceImpl.SKFont;
font.LinearMetrics = true;
font.Hinting = SKFontHinting.Full;
font.Subpixel = edging == SKFontEdging.SubpixelAntialias;
font.Edging = edging;
font.Hinting = SKFontHinting.Full;
font.Size = (float)FontRenderingEmSize;
font.Typeface = _glyphTypefaceImpl.Typeface;
font.Embolden = (_glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0;
font.SkewX = (_glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0;
var builder = SKTextBlobBuilderCache.Shared.Get();
@ -101,8 +119,6 @@ namespace Avalonia.Skia
runBuffer.SetPositions(_glyphPositions);
runBuffer.SetGlyphs(_glyphIndices);
SKFontCache.Shared.Return(font);
textBlob = builder.Build();
SKTextBlobBuilderCache.Shared.Return(builder);

44
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -9,24 +9,28 @@ namespace Avalonia.Skia
internal class GlyphTypefaceImpl : IGlyphTypeface
{
private bool _isDisposed;
private readonly SKTypeface _typeface;
public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations)
{
Typeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
_typeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
SKFont = new SKFont(typeface)
{
LinearMetrics = true,
Embolden = (fontSimulations & FontSimulations.Bold) != 0,
SkewX = (fontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0
};
Face = new Face(GetTable)
{
UnitsPerEm = Typeface.UnitsPerEm
UnitsPerEm = typeface.UnitsPerEm
};
Font = new Font(Face);
Font.SetFunctionsOpenType();
var metrics = Typeface.ToFont().Metrics;
const double defaultFontRenderingEmSize = 12.0;
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.HorizontalAscender, out var ascent);
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.HorizontalDescender, out var descent);
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.HorizontalLineGap, out var lineGap);
@ -34,10 +38,10 @@ namespace Avalonia.Skia
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughSize);
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlineOffset);
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineSize);
Metrics = new FontMetrics
{
DesignEmHeight = (short)Typeface.UnitsPerEm,
DesignEmHeight = (short)Face.UnitsPerEm,
Ascent = -ascent,
Descent = -descent,
LineGap = lineGap,
@ -45,25 +49,25 @@ namespace Avalonia.Skia
UnderlineThickness = underlineSize,
StrikethroughPosition = -strikethroughOffset,
StrikethroughThickness = strikethroughSize,
IsFixedPitch = Typeface.IsFixedPitch
IsFixedPitch = typeface.IsFixedPitch
};
GlyphCount = Typeface.GlyphCount;
GlyphCount = typeface.GlyphCount;
FontSimulations = fontSimulations;
Weight = (FontWeight)Typeface.FontWeight;
Weight = (FontWeight)typeface.FontWeight;
Style = Typeface.FontSlant.ToAvalonia();
Style = typeface.FontSlant.ToAvalonia();
Stretch = (FontStretch)Typeface.FontStyle.Width;
Stretch = (FontStretch)typeface.FontStyle.Width;
}
public Face Face { get; }
public Font Font { get; }
public SKTypeface Typeface { get; }
public SKFont SKFont { get; }
public FontSimulations FontSimulations { get; }
@ -73,7 +77,7 @@ namespace Avalonia.Skia
public int GlyphCount { get; }
public string FamilyName => Typeface.FamilyName;
public string FamilyName => _typeface.FamilyName;
public FontWeight Weight { get; }
@ -89,7 +93,7 @@ namespace Avalonia.Skia
{
return false;
}
metrics = new GlyphMetrics
{
XBearing = extents.XBearing,
@ -97,7 +101,7 @@ namespace Avalonia.Skia
Width = extents.Width,
Height = extents.Height
};
return true;
}
@ -156,13 +160,13 @@ namespace Avalonia.Skia
private Blob? GetTable(Face face, Tag tag)
{
var size = Typeface.GetTableSize(tag);
var size = _typeface.GetTableSize(tag);
var data = Marshal.AllocCoTaskMem(size);
var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data));
return Typeface.TryGetTableData(tag, 0, size, data) ?
return _typeface.TryGetTableData(tag, 0, size, data) ?
new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null;
}
@ -192,7 +196,7 @@ namespace Avalonia.Skia
public bool TryGetTable(uint tag, out byte[] table)
{
return Typeface.TryGetTableData(tag, out table);
return _typeface.TryGetTableData(tag, out table);
}
}
}

11
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -77,13 +77,10 @@ namespace Avalonia.Skia
var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize;
var skFont = SKFontCache.Shared.Get();
var skFont = glyphTypeface.SKFont;
skFont.Typeface = glyphTypeface.Typeface;
skFont.Size = fontRenderingEmSize;
skFont.Edging = SKFontEdging.Alias;
skFont.Hinting = SKFontHinting.None;
skFont.LinearMetrics = true;
SKPath path = new SKPath();
@ -102,8 +99,6 @@ namespace Avalonia.Skia
currentX += glyphRun.GlyphInfos[i].GlyphAdvance;
}
SKFontCache.Shared.Return(skFont);
return new StreamGeometryImpl(path, path);
}
@ -201,9 +196,9 @@ namespace Avalonia.Skia
}
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize,
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds)
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
{
return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin, bounds);
return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin);
}
}
}

13
src/Skia/Avalonia.Skia/SKFontCache.cs

@ -1,13 +0,0 @@
using System.Collections.Concurrent;
using SkiaSharp;
namespace Avalonia.Skia
{
/// <summary>
/// Cache for SKFonts.
/// </summary>
internal class SKFontCache : SKCacheBase<SKFont, SKFontCache>
{
}
}

2
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@ -13,6 +13,7 @@ namespace Avalonia.Skia
{
switch (interpolationMode)
{
case BitmapInterpolationMode.Unspecified:
case BitmapInterpolationMode.LowQuality:
return SKFilterQuality.Low;
case BitmapInterpolationMode.MediumQuality:
@ -20,7 +21,6 @@ namespace Avalonia.Skia
case BitmapInterpolationMode.HighQuality:
return SKFilterQuality.High;
case BitmapInterpolationMode.None:
case BitmapInterpolationMode.Unspecified:
return SKFilterQuality.None;
default:
throw new ArgumentOutOfRangeException(nameof(interpolationMode), interpolationMode, null);

4
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -160,9 +160,9 @@ namespace Avalonia.Direct2D1
public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl();
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<IGeometryImpl> children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) => new CombinedGeometryImpl(combineMode, g1, g2);
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds)
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
{
return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin, bounds);
return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin);
}
class D2DApi : IPlatformRenderInterfaceContext

2
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -149,6 +149,7 @@ namespace Avalonia.Direct2D1.Media
{
switch (interpolationMode)
{
case BitmapInterpolationMode.Unspecified:
case BitmapInterpolationMode.LowQuality:
return InterpolationMode.Linear;
case BitmapInterpolationMode.MediumQuality:
@ -156,7 +157,6 @@ namespace Avalonia.Direct2D1.Media
case BitmapInterpolationMode.HighQuality:
return InterpolationMode.HighQualityCubic;
case BitmapInterpolationMode.None:
case BitmapInterpolationMode.Unspecified:
return InterpolationMode.NearestNeighbor;
default:
throw new ArgumentOutOfRangeException(nameof(interpolationMode), interpolationMode, null);

9
src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs

@ -20,13 +20,12 @@ namespace Avalonia.Direct2D1.Media
private SharpDX.DirectWrite.GlyphRun? _glyphRun;
public GlyphRunImpl(IGlyphTypeface glyphTypeface, double fontRenderingEmSize,
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds)
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
{
_glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
BaselineOrigin = baselineOrigin;
Bounds = bounds;
var glyphCount = glyphInfos.Count;
@ -62,6 +61,12 @@ namespace Avalonia.Direct2D1.Media
AscenderOffset = (float)y
};
}
var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
var height = glyphTypeface.Metrics.LineSpacing * scale;
Bounds = new Rect(baselineOrigin.X, 0, width, height);
}
public SharpDX.DirectWrite.GlyphRun GlyphRun

29
tests/Avalonia.Base.UnitTests/Media/ColorTests.cs

@ -335,5 +335,34 @@ namespace Avalonia.Base.UnitTests.Media
Assert.True(dataPoint.Item2 == parsedColor);
}
}
[Fact]
public void Hsv_To_From_Hsl_Conversion()
{
// Note that conversion of values more representative of actual colors is not done due to rounding error
// It would be necessary to introduce a different equality comparison that accounts for rounding differences in values
// This is a result of the math in the conversion itself
// RGB doesn't have this problem because it uses whole numbers
var data = new Tuple<HsvColor, HslColor>[]
{
Tuple.Create(new HsvColor(1.0, 0.0, 0.0, 0.0), new HslColor(1.0, 0.0, 0.0, 0.0)),
Tuple.Create(new HsvColor(1.0, 359.0, 1.0, 1.0), new HslColor(1.0, 359.0, 1.0, 0.5)),
Tuple.Create(new HsvColor(1.0, 128.0, 0.0, 0.0), new HslColor(1.0, 128.0, 0.0, 0.0)),
Tuple.Create(new HsvColor(1.0, 128.0, 0.0, 1.0), new HslColor(1.0, 128.0, 0.0, 1.0)),
Tuple.Create(new HsvColor(1.0, 128.0, 1.0, 1.0), new HslColor(1.0, 128.0, 1.0, 0.5)),
Tuple.Create(new HsvColor(0.23, 0.5, 1.0, 1.0), new HslColor(0.23, 0.5, 1.0, 0.5)),
};
foreach (var dataPoint in data)
{
var convertedHsl = dataPoint.Item1.ToHsl();
var convertedHsv = dataPoint.Item2.ToHsv();
Assert.Equal(convertedHsv, dataPoint.Item1);
Assert.Equal(convertedHsl, dataPoint.Item2);
}
}
}
}

36
tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs

@ -37,62 +37,62 @@ namespace Avalonia.Benchmarks.Styling
}
[Benchmark]
public SelectorMatch IsSelector_NoMatch()
public void IsSelector_NoMatch()
{
return _isCalendarSelector.Match(_notMatchingControl);
_isCalendarSelector.Match(_notMatchingControl);
}
[Benchmark]
public SelectorMatch IsSelector_Match()
public void IsSelector_Match()
{
return _isCalendarSelector.Match(_matchingControl);
_isCalendarSelector.Match(_matchingControl);
}
[Benchmark]
public SelectorMatch ClassSelector_NoMatch()
public void ClassSelector_NoMatch()
{
return _classSelector.Match(_notMatchingControl);
_classSelector.Match(_notMatchingControl);
}
[Benchmark]
public SelectorMatch ClassSelector_Match()
public void ClassSelector_Match()
{
return _classSelector.Match(_matchingControl);
_classSelector.Match(_matchingControl);
}
[Benchmark]
public SelectorMatch OrSelector_One_Match()
public void OrSelector_One_Match()
{
return _orSelectorTwo.Match(_matchingControl);
_orSelectorTwo.Match(_matchingControl);
}
[Benchmark]
public SelectorMatch OrSelector_Five_Match()
public void OrSelector_Five_Match()
{
return _orSelectorFive.Match(_matchingControl);
_orSelectorFive.Match(_matchingControl);
}
}
internal class AlwaysMatchSelector : Selector
{
public override bool InTemplate => false;
internal override bool InTemplate => false;
public override bool IsCombinator => false;
internal override bool IsCombinator => false;
public override Type TargetType => null;
internal override Type TargetType => null;
public override string ToString(Style owner)
{
return "Always";
}
protected override SelectorMatch Evaluate(StyledElement control, IStyle parent, bool subscribe)
private protected override SelectorMatch Evaluate(StyledElement control, IStyle parent, bool subscribe)
{
return SelectorMatch.AlwaysThisType;
}
protected override Selector MovePrevious() => null;
private protected override Selector MovePrevious() => null;
protected override Selector MovePreviousOrParent() => null;
private protected override Selector MovePreviousOrParent() => null;
}
}

34
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

@ -657,6 +657,40 @@ namespace Avalonia.Controls.UnitTests
root.LayoutManager.ExecuteLayoutPass();
}
[Fact]
public void ScrollIntoView_On_Effectively_Invisible_Panel_Does_Not_Create_Ghost_Elements()
{
var items = new[] { "foo", "bar", "baz" };
var (target, _, itemsControl) = CreateUnrootedTarget(items: items);
var container = new Decorator { Margin = new Thickness(100), Child = itemsControl };
var root = new TestRoot(true, container);
root.LayoutManager.ExecuteInitialLayoutPass();
// Clear the items and do a layout to recycle all elements.
itemsControl.ItemsSource = null;
root.LayoutManager.ExecuteLayoutPass();
// Should have no realized elements and 3 unrealized elements.
Assert.Equal(0, target.GetRealizedElements().Count);
Assert.Equal(3, target.Children.Count);
// Make the panel effectively invisible and set items.
container.IsVisible = false;
itemsControl.ItemsSource = items;
// Try to scroll into view while effectively invisible.
target.ScrollIntoView(0);
// Make the panel visible and layout.
container.IsVisible = true;
root.LayoutManager.ExecuteLayoutPass();
// Should have 3 realized elements and no unrealized elements.
Assert.Equal(3, target.GetRealizedElements().Count);
Assert.Equal(3, target.Children.Count);
}
private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
{
return target.GetRealizedElements()

2
tests/Avalonia.RenderTests/Controls/TextBlockTests.cs

@ -121,7 +121,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Decorator target = new Decorator
{
Padding = new Thickness(8),
Width = 180,
Width = 190,
Height = 80,
Child = new StackPanel()

12
tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs

@ -95,8 +95,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
var fmt = Create(input, fontSize);
Assert.Equal(expWidth, fmt.Bounds.Width, 2);
Assert.Equal(expHeight, fmt.Bounds.Height, 2);
Assert.Equal(expWidth, fmt.WidthIncludingTrailingWhitespace, 2);
Assert.Equal(expHeight, fmt.Height, 2);
}
[Theory]
@ -253,8 +253,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
Assert.Equal(exr.Height, r.Height, 2);
}
}
[Fact]
[Fact]
public async Task TextLayout_Basic()
{
// Skip test on OSX: text rendering is subtly different.
@ -274,7 +274,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
Background = Brushes.White,
Child = new DrawnControl(c =>
{
var textRect = t.Bounds;
var textRect = new Rect(0, 0, t.WidthIncludingTrailingWhitespace, t.Height);
var bounds = new Rect(0, 0, 200, 200);
var rect = bounds.CenterRect(textRect);
c.DrawRectangle(Brushes.Yellow, null, rect);
@ -306,7 +306,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
Background = Brushes.White,
Child = new DrawnControl(c =>
{
var textRect = t.Bounds;
var textRect = new Rect(0, 0, t.WidthIncludingTrailingWhitespace, t.Height);
var bounds = new Rect(0, 0, 200, 200);
var rect = bounds.CenterRect(textRect);
var rotate = Matrix.CreateTranslation(-100, -100) *

6
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@ -597,7 +597,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(1, layout.TextLines.Count);
Assert.Equal(lineHeight, layout.Bounds.Height);
Assert.Equal(lineHeight, layout.Height);
}
}
@ -725,7 +725,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var selectedRect = rects[0];
Assert.Equal(selectedText.Bounds.Width, selectedRect.Width, 2);
Assert.Equal(selectedText.WidthIncludingTrailingWhitespace, selectedRect.Width, 2);
}
}
@ -847,7 +847,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
12,
Brushes.Black);
Assert.True(layout.Bounds.Height > 0);
Assert.True(layout.Height > 0);
}
}

22
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -708,6 +708,28 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[Fact]
public void Should_Get_CharacterHit_For_Distance_With_TextEndOfLine()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new SingleBufferTextSource("Hello World", defaultProperties, true);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, 1000,
new GenericTextParagraphProperties(defaultProperties));
var characterHit = textLine.GetCharacterHitFromDistance(1000);
Assert.Equal(10, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
}
}
private class MixedTextBufferTextSource : ITextSource
{
public TextRun? GetTextRun(int textSourceIndex)

4
tests/Avalonia.UnitTests/StyleHelpers.cs

@ -6,9 +6,9 @@ namespace Avalonia.UnitTests
{
public static class StyleHelpers
{
public static SelectorMatchResult TryAttach(Style style, StyledElement element, object? host = null)
public static void TryAttach(Style style, StyledElement element, object? host = null)
{
return style.TryAttach(element, host ?? element, PropertyStore.FrameType.Style);
style.TryAttach(element, host ?? element, PropertyStore.FrameType.Style);
}
}
}

BIN
tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 B

After

Width:  |  Height:  |  Size: 773 B

BIN
tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Fill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 116 KiB

BIN
tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_UniformToFill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Uniform_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_Fill.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 115 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 B

After

Width:  |  Height:  |  Size: 250 B

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 B

After

Width:  |  Height:  |  Size: 528 B

BIN
tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Fill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 112 KiB

BIN
tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_UniformToFill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Uniform_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_Fill.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 111 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Loading…
Cancel
Save