Browse Source

Merge branch 'master' into disableSetProcessName-feature

pull/9085/head
Dan Walmsley 3 years ago
committed by GitHub
parent
commit
067022a53f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Avalonia.sln
  2. 5
      Directory.Build.targets
  3. 4
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  4. 4
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  5. 21
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  6. 1
      samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs
  7. 4
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  8. 2
      src/Avalonia.Base/Avalonia.Base.csproj
  9. 6
      src/Avalonia.Base/Collections/AvaloniaDictionary.cs
  10. 30
      src/Avalonia.Base/Interactivity/RoutedEventArgs.cs
  11. 3
      src/Avalonia.Base/Media/FormattedText.cs
  12. 77
      src/Avalonia.Base/Media/GlyphRun.cs
  13. 12
      src/Avalonia.Base/Media/PathMarkupParser.cs
  14. 19
      src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
  15. 7
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  16. 21
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  17. 9
      src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
  18. 9
      src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
  19. 37
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  20. 6
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  21. 28
      src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs
  22. 2
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
  23. 484
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  24. 486
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
  25. 41
      src/Avalonia.Controls/Control.cs
  26. 40
      src/Avalonia.Controls/Documents/InlineCollection.cs
  27. 6
      src/Avalonia.Controls/Documents/Span.cs
  28. 20
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  29. 374
      src/Avalonia.Controls/SelectableTextBlock.cs
  30. 83
      src/Avalonia.Controls/SizeChangedEventArgs.cs
  31. 323
      src/Avalonia.Controls/TextBlock.cs
  32. 12
      src/Avalonia.Controls/TextBox.cs
  33. 42
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  34. 2
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  35. 14
      src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml
  36. 18
      src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml
  37. 1
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  38. 14
      src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml
  39. 18
      src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml
  40. 2
      src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
  41. 4
      src/Avalonia.Themes.Simple/Controls/TextBox.xaml
  42. 5
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  43. 12
      src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs
  44. 2
      src/Shared/StringCompatibilityExtensions.cs
  45. 4
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  46. 126
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  47. 4
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  48. 131
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  49. 2
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  50. 17
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  51. 15
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  52. 132
      tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs
  53. 121
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
  54. 2
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  55. 6
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  56. 12
      tests/Avalonia.UnitTests/MockGlyphRun.cs
  57. 2
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

1
Avalonia.sln

@ -41,6 +41,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE
src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
src\Avalonia.Base\Compatibility\StringCompatibilityExtensions.cs = src\Avalonia.Base\Compatibility\StringCompatibilityExtensions.cs
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}"

5
Directory.Build.targets

@ -0,0 +1,5 @@
<Project>
<PropertyGroup Condition="$(NETCoreSdkVersion.StartsWith('7.0'))">
<DefineConstants>$(DefineConstants);NET7SDK</DefineConstants>
</PropertyGroup>
</Project>

4
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -385,7 +385,7 @@
return true;
}
-(void)resignKeyWindow
-(void)windowDidResignKey:(NSNotification *)notification
{
if(_parent)
_parent->BaseEvents->Deactivated();
@ -393,8 +393,6 @@
[self showAppMenuOnly];
[self invalidateShadow];
[super resignKeyWindow];
}
- (void)windowDidMove:(NSNotification *_Nonnull)notification

4
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -63,7 +63,7 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) {
START_COM_CALL;
@autoreleasepool {
_isDialog = isDialog;
_isDialog = isDialog || _parent != nullptr;
WindowBaseImpl::Show(activate, isDialog);
@ -96,6 +96,8 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) {
auto cparent = dynamic_cast<WindowImpl *>(parent);
_parent = cparent;
_isDialog = _parent != nullptr;
if(_parent != nullptr && Window != nullptr){
// If one tries to show a child window with a minimized parent window, then the parent window will be

21
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@ -24,18 +24,19 @@
HsvColor="hsv(120, 1, 1)"
Margin="0,50,0,0">
<ColorPicker.Palette>
<controls:FlatColorPalette />
<controls:FlatHalfColorPalette />
</ColorPicker.Palette>
</ColorPicker>
<Grid Grid.Column="2"
Grid.Row="0"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<ColorSpectrum x:Name="ColorSpectrum1"
Grid.Row="0"
Color="Red"
CornerRadius="10"
Height="256"
Width="256" />
<!-- HSV Sliders -->
<ColorSlider Grid.Row="1"
Margin="0,10,0,0"
ColorComponent="Component1"
@ -53,7 +54,21 @@
ColorComponent="Alpha"
ColorModel="Hsva"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
<ColorPreviewer Grid.Row="5"
<!-- RGB Sliders -->
<!--<ColorSlider Grid.Row="5"
Margin="0,10,0,0"
ColorComponent="Component1"
ColorModel="Rgba"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
<ColorSlider Grid.Row="6"
ColorComponent="Component2"
ColorModel="Rgba"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
<ColorSlider Grid.Row="7"
ColorComponent="Component3"
ColorModel="Rgba"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />-->
<ColorPreviewer Grid.Row="8"
IsAccentColorsVisible="False"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
</Grid>

1
samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs

@ -20,6 +20,7 @@ namespace ControlCatalog.Pages
Color = Colors.Blue,
Margin = new Thickness(0, 50, 0, 0),
HorizontalAlignment = HorizontalAlignment.Center,
Palette = new MaterialHalfColorPalette(),
};
Grid.SetColumn(colorPicker, 2);
Grid.SetRow(colorPicker, 1);

4
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -118,7 +118,7 @@
</StackPanel>
</Border>
<Border>
<RichTextBlock SelectionBrush="LightBlue" IsTextSelectionEnabled="True" Margin="10" TextWrapping="Wrap">
<SelectableTextBlock SelectionBrush="LightBlue" Margin="10" TextWrapping="Wrap">
This <Span FontWeight="Bold">is</Span> a
<Span Background="Silver" Foreground="Maroon">TextBlock</Span>
with <Span TextDecorations="Underline">several</Span>
@ -126,7 +126,7 @@
<Span Foreground="Blue">
using a <Bold>variety</Bold> of <Italic>styles</Italic>
</Span>.
</RichTextBlock>
</SelectableTextBlock>
</Border>
</WrapPanel>
</StackPanel>

2
src/Avalonia.Base/Avalonia.Base.csproj

@ -23,6 +23,7 @@
<Import Project="..\..\build\SourceGenerators.props" />
<ItemGroup>
<Compile Include="..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
<Compile Include="..\Shared\StringCompatibilityExtensions.cs" Link="Compatibility\StringCompatibilityExtensions.cs" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">
@ -46,6 +47,7 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Compatibility\" />
<Folder Include="Rendering\Composition\Utils" />
</ItemGroup>
</Project>

6
src/Avalonia.Base/Collections/AvaloniaDictionary.cs

@ -81,7 +81,7 @@ namespace Avalonia.Collections
if (replace)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));
if (CollectionChanged != null)
{
@ -148,7 +148,7 @@ namespace Avalonia.Collections
{
_inner.Remove(key);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));
if (CollectionChanged != null)
{
@ -208,7 +208,7 @@ namespace Avalonia.Collections
private void NotifyAdd(TKey key, TValue value)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));
if (CollectionChanged != null)

30
src/Avalonia.Base/Interactivity/RoutedEventArgs.cs

@ -2,29 +2,59 @@ using System;
namespace Avalonia.Interactivity
{
/// <summary>
/// Provides state information and data specific to a routed event.
/// </summary>
public class RoutedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="RoutedEventArgs"/> class.
/// </summary>
public RoutedEventArgs()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RoutedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
public RoutedEventArgs(RoutedEvent? routedEvent)
{
RoutedEvent = routedEvent;
}
/// <summary>
/// Initializes a new instance of the <see cref="RoutedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
/// <param name="source">The source object that raised the routed event.</param>
public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
{
RoutedEvent = routedEvent;
Source = source;
}
/// <summary>
/// Gets or sets a value indicating whether the routed event has already been handled.
/// </summary>
/// <remarks>
/// Once handled, a routed event should be ignored.
/// </remarks>
public bool Handled { get; set; }
/// <summary>
/// Gets or sets the routed event associated with these event args.
/// </summary>
public RoutedEvent? RoutedEvent { get; set; }
/// <summary>
/// Gets or sets the routing strategy (direct, bubbling, or tunneling) of the routed event.
/// </summary>
public RoutingStrategies Route { get; set; }
/// <summary>
/// Gets or sets the source object that raised the routed event.
/// </summary>
public IInteractive? Source { get; set; }
}
}

3
src/Avalonia.Base/Media/FormattedText.cs

@ -93,7 +93,8 @@ namespace Avalonia.Media
runProps,
TextWrapping.WrapWithOverflow,
0, // line height not specified
0 // indentation not specified
0, // indentation not specified
0
);
InvalidateMetrics();

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

@ -170,7 +170,7 @@ namespace Avalonia.Media
}
/// <summary>
/// Gets the scale of the current <see cref="Media.GlyphTypeface"/>
/// Gets the scale of the current <see cref="IGlyphTypeface"/>
/// </summary>
internal double Scale => FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight;
@ -860,82 +860,9 @@ namespace Avalonia.Media
private IGlyphRunImpl CreateGlyphRunImpl()
{
IGlyphRunImpl glyphRunImpl;
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
var count = GlyphIndices.Count;
var scale = (float)(FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight);
if (GlyphOffsets == null)
{
if (GlyphTypeface.Metrics.IsFixedPitch)
{
var buffer = platformRenderInterface.AllocateGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count);
var glyphs = buffer.GlyphIndices;
for (int i = 0; i < glyphs.Length; i++)
{
glyphs[i] = GlyphIndices[i];
}
glyphRunImpl = buffer.Build();
}
else
{
var buffer = platformRenderInterface.AllocateHorizontalGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count);
var glyphs = buffer.GlyphIndices;
var positions = buffer.GlyphPositions;
var width = 0d;
for (var i = 0; i < count; i++)
{
positions[i] = (float)width;
if (GlyphAdvances == null)
{
width += GlyphTypeface.GetGlyphAdvance(GlyphIndices[i]) * scale;
}
else
{
width += GlyphAdvances[i];
}
glyphs[i] = GlyphIndices[i];
}
glyphRunImpl = buffer.Build();
}
}
else
{
var buffer = platformRenderInterface.AllocatePositionedGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count);
var glyphs = buffer.GlyphIndices;
var glyphPositions = buffer.GlyphPositions;
var currentX = 0.0;
for (var i = 0; i < count; i++)
{
var glyphOffset = GlyphOffsets[i];
glyphPositions[i] = new PointF((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
if (GlyphAdvances == null)
{
currentX += GlyphTypeface.GetGlyphAdvance(GlyphIndices[i]) * scale;
}
else
{
currentX += GlyphAdvances[i];
}
glyphs[i] = GlyphIndices[i];
}
glyphRunImpl = buffer.Build();
}
return glyphRunImpl;
return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
}
void IDisposable.Dispose()

12
src/Avalonia.Base/Media/PathMarkupParser.cs

@ -188,7 +188,11 @@ namespace Avalonia.Media
_isOpen = true;
}
private void SetFillRule(scoped ref ReadOnlySpan<char> span)
private void SetFillRule(
#if NET7SDK
scoped
#endif
ref ReadOnlySpan<char> span)
{
ThrowIfDisposed();
@ -452,7 +456,11 @@ namespace Avalonia.Media
return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || span[0] == '.' || char.IsDigit(span[0]));
}
private static bool ReadArgument(scoped ref ReadOnlySpan<char> remaining, out ReadOnlySpan<char> argument)
private static bool ReadArgument(
#if NET7SDK
scoped
#endif
ref ReadOnlySpan<char> remaining, out ReadOnlySpan<char> argument)
{
remaining = SkipWhitespace(remaining);
if (remaining.IsEmpty)

19
src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs

@ -17,15 +17,18 @@
/// <param name="textAlignment">logical horizontal alignment</param>
/// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param>
/// <param name="letterSpacing">letter spacing</param>
public GenericTextParagraphProperties(TextRunProperties defaultTextRunProperties,
TextAlignment textAlignment = TextAlignment.Left,
TextWrapping textWrap = TextWrapping.NoWrap,
double lineHeight = 0)
double lineHeight = 0,
double letterSpacing = 0)
{
DefaultTextRunProperties = defaultTextRunProperties;
_textAlignment = textAlignment;
_textWrap = textWrap;
_lineHeight = lineHeight;
LetterSpacing = letterSpacing;
}
/// <summary>
@ -39,6 +42,7 @@
/// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param>
/// <param name="indent">line indentation</param>
/// <param name="letterSpacing">letter spacing</param>
public GenericTextParagraphProperties(
FlowDirection flowDirection,
TextAlignment textAlignment,
@ -47,8 +51,8 @@
TextRunProperties defaultTextRunProperties,
TextWrapping textWrap,
double lineHeight,
double indent
)
double indent,
double letterSpacing)
{
_flowDirection = flowDirection;
_textAlignment = textAlignment;
@ -57,6 +61,7 @@
DefaultTextRunProperties = defaultTextRunProperties;
_textWrap = textWrap;
_lineHeight = lineHeight;
LetterSpacing = letterSpacing;
Indent = indent;
}
@ -72,7 +77,8 @@
textParagraphProperties.DefaultTextRunProperties,
textParagraphProperties.TextWrapping,
textParagraphProperties.LineHeight,
textParagraphProperties.Indent)
textParagraphProperties.Indent,
textParagraphProperties.LetterSpacing)
{
}
@ -131,6 +137,11 @@
/// </summary>
public override double Indent { get; }
/// <summary>
/// The letter spacing
/// </summary>
public override double LetterSpacing { get; }
/// <summary>
/// Set text flow direction
/// </summary>

7
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -249,7 +249,8 @@ namespace Avalonia.Media.TextFormatting
var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
currentRun.Properties.FontRenderingEmSize,
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab);
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
@ -505,7 +506,7 @@ namespace Avalonia.Media.TextFormatting
case { } drawableTextRun:
{
if (currentWidth + drawableTextRun.Size.Width > paragraphWidth)
if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
{
goto found;
}
@ -668,7 +669,7 @@ namespace Avalonia.Media.TextFormatting
if (!breakFound)
{
currentLength += currentRun.Text.Length;
currentLength += currentRun.TextSourceLength;
continue;
}

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

@ -31,6 +31,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <param name="maxLines">The maximum number of text lines.</param>
/// <param name="textStyleOverrides">The text style overrides.</param>
public TextLayout(
@ -46,12 +47,13 @@ namespace Avalonia.Media.TextFormatting
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
double letterSpacing = 0,
int maxLines = 0,
IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides = null)
{
_paragraphProperties =
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, flowDirection, lineHeight);
textDecorations, flowDirection, lineHeight, letterSpacing);
_textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
@ -63,6 +65,8 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight;
LetterSpacing = letterSpacing;
MaxLines = maxLines;
TextLines = CreateTextLines();
@ -77,6 +81,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <param name="maxLines">The maximum number of text lines.</param>
public TextLayout(
ITextSource textSource,
@ -85,6 +90,7 @@ namespace Avalonia.Media.TextFormatting
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
double letterSpacing = 0,
int maxLines = 0)
{
_textSource = textSource;
@ -99,6 +105,8 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight;
LetterSpacing = letterSpacing;
MaxLines = maxLines;
TextLines = CreateTextLines();
@ -128,6 +136,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public int MaxLines { get; }
/// <summary>
/// Gets the text spacing.
/// </summary>
public double LetterSpacing { get; }
/// <summary>
/// Gets the text lines.
/// </summary>
@ -374,15 +387,17 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textDecorations">The text decorations.</param>
/// <param name="flowDirection">The text flow direction.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <returns></returns>
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping,
TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight)
TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight,
double letterSpacing)
{
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
return new GenericTextParagraphProperties(flowDirection, textAlignment, true, false,
textRunStyle, textWrapping, lineHeight, 0);
textRunStyle, textWrapping, lineHeight, 0, letterSpacing);
}
/// <summary>

9
src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs

@ -57,7 +57,7 @@
public abstract double Indent { get; }
/// <summary>
/// Paragraph indentation
/// Get the paragraph indentation.
/// </summary>
public virtual double ParagraphIndent
{
@ -65,11 +65,16 @@
}
/// <summary>
/// Default Incremental Tab
/// Gets the default incremental tab width.
/// </summary>
public virtual double DefaultIncrementalTab
{
get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; }
}
/// <summary>
/// Gets the letter spacing.
/// </summary>
public virtual double LetterSpacing { get; }
}
}

9
src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs

@ -12,13 +12,15 @@ namespace Avalonia.Media.TextFormatting
double fontRenderingEmSize = 12,
sbyte bidiLevel = 0,
CultureInfo? culture = null,
double incrementalTabWidth = 0)
double incrementalTabWidth = 0,
double letterSpacing = 0)
{
Typeface = typeface;
FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel;
Culture = culture;
IncrementalTabWidth = incrementalTabWidth;
LetterSpacing = letterSpacing;
}
/// <summary>
@ -45,5 +47,10 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public double IncrementalTabWidth { get; }
/// <summary>
/// Get the letter spacing.
/// </summary>
public double LetterSpacing { get; }
}
}

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

@ -171,40 +171,15 @@ namespace Avalonia.Platform
IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride);
/// <summary>
/// Allocates a platform glyph run buffer.
/// Creates a platform implementation of a glyph run.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="length">The length.</param>
/// <returns>An <see cref="IGlyphRunBuffer"/>.</returns>
/// <remarks>
/// This buffer only holds glyph indices.
/// </remarks>
IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
/// <summary>
/// Allocates a horizontal platform glyph run buffer.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="length">The length.</param>
/// <returns>An <see cref="IGlyphRunBuffer"/>.</returns>
/// <remarks>
/// This buffer holds glyph indices and glyph advances.
/// </remarks>
IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
/// <summary>
/// Allocates a positioned platform glyph run buffer.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="length">The length.</param>
/// <returns>An <see cref="IGlyphRunBuffer"/>.</returns>
/// <remarks>
/// This buffer holds glyph indices, glyph advances and glyph positions.
/// </remarks>
IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
/// <param name="glyphIndices">The glyph indices.</param>
/// <param name="glyphAdvances">The glyph advances.</param>
/// <param name="glyphOffsets">The glyph offsets.</param>
/// <returns></returns>
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double>? glyphAdvances, IReadOnlyList<Vector>? glyphOffsets);
/// <summary>
/// Gets a value indicating whether the platform directly supports rectangles with rounded corners.

6
src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -8,7 +8,11 @@ namespace Avalonia.Utilities
#endif
static class IdentifierParser
{
public static ReadOnlySpan<char> ParseIdentifier(this scoped ref CharacterReader r)
public static ReadOnlySpan<char> ParseIdentifier(this
#if NET7SDK
scoped
#endif
ref CharacterReader r)
{
if (IsValidIdentifierStart(r.Peek))
{

28
src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs

@ -14,33 +14,5 @@ namespace Avalonia.Controls
public ColorPicker() : base()
{
}
/// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
// Until this point the ColorPicker itself is responsible to process property updates.
// This, for example, syncs Color with HsvColor and updates primitive controls.
//
// However, when the template is created, hand-off this change processing to the
// ColorView within the control template itself. Remember ColorPicker derives from
// ColorView so we don't want two instances of the same logic fighting each other.
// It is best to hand-off to the ColorView in the control template because that is the
// primary point of user-interaction for the overall control. It also simplifies binding.
//
// Keep in mind this hand-off is not possible until the template controls are created
// which is done after the ColorPicker is instantiated. The ColorPicker must still
// process updates before the template is applied to ensure all property changes in
// XAML or object initializers are handled correctly. Otherwise, there can be bugs
// such as setting the Color property doesn't work because the HsvColor is never updated
// and then the Color value is lost once the template loads (and the template ColorView
// takes over).
//
// In order to complete this hand-off, completely ignore property changes here in the
// ColorPicker. This means the ColorView in the control template is now responsible to
// process property changes and handle primary calculations.
base.ignorePropertyChanged = true;
}
}
}

2
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs

@ -240,7 +240,7 @@ namespace Avalonia.Controls.Primitives
public ColorComponent ThirdComponent
{
get => GetValue(ThirdComponentProperty);
private set => SetValue(ThirdComponentProperty, value);
protected set => SetValue(ThirdComponentProperty, value);
}
}
}

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

@ -1,8 +1,15 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Avalonia.Controls"
xmlns:converters="using:Avalonia.Controls.Converters"
xmlns:primitives="using:Avalonia.Controls.Primitives"
xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker"
x:CompileBindings="True">
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml" />
</ResourceDictionary.MergedDictionaries>
<ControlTheme x:Key="{x:Type ColorPicker}"
TargetType="ColorPicker">
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
@ -43,39 +50,454 @@
</DropDownButton.Content>
<DropDownButton.Flyout>
<Flyout FlyoutPresenterClasses="nopadding">
<ColorView x:Name="FlyoutColorView"
Color="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
ColorModel="{Binding ColorModel, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
ColorSpectrumComponents="{TemplateBinding ColorSpectrumComponents}"
ColorSpectrumShape="{TemplateBinding ColorSpectrumShape}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
IsAlphaEnabled="{TemplateBinding IsAlphaEnabled}"
IsAlphaVisible="{TemplateBinding IsAlphaVisible}"
IsColorComponentsVisible="{TemplateBinding IsColorComponentsVisible}"
IsColorModelVisible="{TemplateBinding IsColorModelVisible}"
IsColorPaletteVisible="{TemplateBinding IsColorPaletteVisible}"
IsColorPreviewVisible="{TemplateBinding IsColorPreviewVisible}"
IsColorSpectrumVisible="{TemplateBinding IsColorSpectrumVisible}"
IsColorSpectrumSliderVisible="{TemplateBinding IsColorSpectrumSliderVisible}"
IsComponentSliderVisible="{TemplateBinding IsComponentSliderVisible}"
IsComponentTextInputVisible="{TemplateBinding IsComponentTextInputVisible}"
IsHexInputVisible="{TemplateBinding IsHexInputVisible}"
MaxHue="{TemplateBinding MaxHue}"
MaxSaturation="{TemplateBinding MaxSaturation}"
MaxValue="{TemplateBinding MaxValue}"
MinHue="{TemplateBinding MinHue}"
MinSaturation="{TemplateBinding MinSaturation}"
MinValue="{TemplateBinding MinValue}"
PaletteColors="{TemplateBinding PaletteColors}"
PaletteColumnCount="{TemplateBinding PaletteColumnCount}"
Palette="{TemplateBinding Palette}"
SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<ColorView.Resources>
<!-- The following is copy-pasted from the ColorView's control template.
It MUST always be kept in sync with the ColorView (which is master).
Note the only changes are resources specific to the ColorPicker. -->
<Grid RowDefinitions="Auto,Auto">
<Grid.Resources>
<!-- This radius must follow OverlayCornerRadius -->
<CornerRadius x:Key="ColorViewTabBackgroundCornerRadius">5,5,0,0</CornerRadius>
</ColorView.Resources>
</ColorView>
</Grid.Resources>
<!-- Backgrounds -->
<!-- TODO: Background="{DynamicResource ColorViewTabBackgroundBrush}" -->
<Border x:Name="TabBackgroundBorder"
Grid.Row="0"
Grid.RowSpan="2"
Height="48"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
BorderBrush="{DynamicResource ColorViewTabBorderBrush}"
CornerRadius="{DynamicResource ColorViewTabBackgroundCornerRadius}" />
<Border x:Name="ContentBackgroundBorder"
Grid.Row="0"
Grid.RowSpan="2"
Margin="0,48,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource BottomCornerRadiusFilterConverter}}"
Background="{DynamicResource ColorViewContentBackgroundBrush}"
BorderBrush="{DynamicResource ColorViewContentBorderBrush}"
BorderThickness="0,1,0,0" />
<TabControl x:Name="PART_TabControl"
Grid.Row="0"
Height="338"
Width="350"
Padding="0"
SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<TabControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="0"
Rows="1" />
</ItemsPanelTemplate>
</TabControl.ItemsPanel>
<!-- Spectrum Tab -->
<TabItem Theme="{StaticResource ColorViewTabItemTheme}"
IsVisible="{TemplateBinding IsColorSpectrumVisible}">
<TabItem.Header>
<Border Height="{DynamicResource ColorViewTabStripHeight}">
<PathIcon Width="20"
Height="20"
Data="{DynamicResource ColorViewSpectrumIconGeometry}" />
</Border>
</TabItem.Header>
<Grid RowDefinitions="*"
Margin="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
MinWidth="32" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"
MinWidth="32" />
</Grid.ColumnDefinitions>
<primitives:ColorSlider x:Name="ColorSpectrumThirdComponentSlider"
AutomationProperties.Name="Third Component"
Grid.Column="0"
IsAlphaMaxForced="True"
IsSaturationValueMaxForced="False"
Orientation="Vertical"
ColorModel="Hsva"
ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Margin="0,0,12,0"
IsVisible="{TemplateBinding IsColorSpectrumSliderVisible}" />
<primitives:ColorSpectrum x:Name="ColorSpectrum"
Grid.Column="1"
Components="{TemplateBinding ColorSpectrumComponents}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
MinHue="{TemplateBinding MinHue}"
MaxHue="{TemplateBinding MaxHue}"
MinSaturation="{TemplateBinding MinSaturation}"
MaxSaturation="{TemplateBinding MaxSaturation}"
MinValue="{TemplateBinding MinValue}"
MaxValue="{TemplateBinding MaxValue}"
Shape="{TemplateBinding ColorSpectrumShape}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<primitives:ColorSlider x:Name="ColorSpectrumAlphaSlider"
AutomationProperties.Name="Alpha Component"
Grid.Column="2"
Orientation="Vertical"
ColorModel="Hsva"
ColorComponent="Alpha"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Margin="12,0,0,0"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<primitives:ColorSlider.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
</MultiBinding>
</primitives:ColorSlider.IsVisible>
</primitives:ColorSlider>
</Grid>
</TabItem>
<!-- Palette Tab -->
<TabItem Theme="{StaticResource ColorViewTabItemTheme}"
IsVisible="{TemplateBinding IsColorPaletteVisible}">
<TabItem.Header>
<Border Height="{DynamicResource ColorViewTabStripHeight}">
<PathIcon Width="20"
Height="20"
Data="{DynamicResource ColorViewPaletteIconGeometry}" />
</Border>
</TabItem.Header>
<ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
Items="{TemplateBinding PaletteColors}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
UseLayoutRounding="False"
Margin="12">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type Color}">
<Border AutomationProperties.Name="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
ToolTip.Tip="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border.Background>
<SolidColorBrush Color="{Binding}" />
</Border.Background>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding $parent[ColorView].PaletteColumnCount}" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</TabItem>
<!-- Components Tab -->
<TabItem Theme="{StaticResource ColorViewTabItemTheme}"
IsVisible="{TemplateBinding IsColorComponentsVisible}">
<TabItem.Header>
<Border Height="{DynamicResource ColorViewTabStripHeight}">
<PathIcon Width="20"
Height="20"
Data="{DynamicResource ColorViewComponentsIconGeometry}" />
</Border>
</TabItem.Header>
<Grid ColumnDefinitions="Auto,Auto,*"
RowDefinitions="Auto,24,1*,1*,1*,1*,12"
Margin="12">
<!-- Top color model & Hex input -->
<Grid Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="0"
ColumnDefinitions="1*,12,1*">
<!-- Content RGB/HSV names are hard-coded and considered universal -->
<!-- RadioButtons are styled to look like a 'SegmentedControl' or 'ButtonGroup' -->
<Grid ColumnDefinitions="1*,1*"
IsVisible="{TemplateBinding IsColorModelVisible}">
<RadioButton x:Name="RgbRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
Grid.Column="0"
Content="RGB"
CornerRadius="4,0,0,4"
BorderThickness="1,1,0,1"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" />
<RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
Grid.Column="1"
Content="HSV"
CornerRadius="0,4,4,0"
BorderThickness="0,1,1,1"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" />
</Grid>
<Grid x:Name="HexInputGrid"
Grid.Column="2"
IsVisible="{TemplateBinding IsHexInputVisible}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
Height="32"
Background="{DynamicResource TextControlBackgroundDisabled}"
BorderBrush="{DynamicResource TextControlBorderBrush}"
BorderThickness="1,1,0,1"
CornerRadius="4,0,0,4">
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="#"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<!-- Color updated in code-behind -->
<TextBox x:Name="PART_HexTextBox"
Grid.Column="1"
AutomationProperties.Name="Hexadecimal Color"
Height="32"
MaxLength="8"
HorizontalAlignment="Stretch"
CornerRadius="0,4,4,0" />
</Grid>
</Grid>
<!-- Color component editing controls -->
<!-- Component 1 RGB:Red HSV:Hue -->
<Border Grid.Column="0"
Grid.Row="2"
Height="{Binding ElementName=Component1NumericUpDown, Path=Bounds.Height}"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Background="{DynamicResource TextControlBackgroundDisabled}"
BorderBrush="{DynamicResource TextControlBorderBrush}"
BorderThickness="1,1,0,1"
CornerRadius="4,0,0,4"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentTextInputVisible}">
<Panel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="R"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="H"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
</Panel>
</Border>
<NumericUpDown x:Name="Component1NumericUpDown"
Grid.Column="1"
Grid.Row="2"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,4,4,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=Component1Slider}"
Maximum="{Binding Maximum, ElementName=Component1Slider}"
Value="{Binding Value, ElementName=Component1Slider}"
IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
<primitives:ColorSlider x:Name="Component1Slider"
Grid.Column="2"
Grid.Row="2"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Component1"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentSliderVisible}" />
<!-- Component 2 RGB:Green HSV:Saturation -->
<Border Grid.Column="0"
Grid.Row="3"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Height="{Binding ElementName=Component2NumericUpDown, Path=Bounds.Height}"
Background="{DynamicResource TextControlBackgroundDisabled}"
BorderBrush="{DynamicResource TextControlBorderBrush}"
BorderThickness="1,1,0,1"
CornerRadius="4,0,0,4"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentTextInputVisible}">
<Panel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="G"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="S"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
</Panel>
</Border>
<NumericUpDown x:Name="Component2NumericUpDown"
Grid.Column="1"
Grid.Row="3"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,4,4,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=Component2Slider}"
Maximum="{Binding Maximum, ElementName=Component2Slider}"
Value="{Binding Value, ElementName=Component2Slider}"
IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
<primitives:ColorSlider x:Name="Component2Slider"
Grid.Column="2"
Grid.Row="3"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Component2"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentSliderVisible}" />
<!-- Component 3 RGB:Blue HSV:Value -->
<Border Grid.Column="0"
Grid.Row="4"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Height="{Binding ElementName=Component3NumericUpDown, Path=Bounds.Height}"
Background="{DynamicResource TextControlBackgroundDisabled}"
BorderBrush="{DynamicResource TextControlBorderBrush}"
BorderThickness="1,1,0,1"
CornerRadius="4,0,0,4"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentTextInputVisible}">
<Panel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="B"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="V"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
</Panel>
</Border>
<NumericUpDown x:Name="Component3NumericUpDown"
Grid.Column="1"
Grid.Row="4"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,4,4,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=Component3Slider}"
Maximum="{Binding Maximum, ElementName=Component3Slider}"
Value="{Binding Value, ElementName=Component3Slider}"
IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
<primitives:ColorSlider x:Name="Component3Slider"
Grid.Column="2"
Grid.Row="4"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Component3"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentSliderVisible}" />
<!-- Alpha Component -->
<Border Grid.Column="0"
Grid.Row="5"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Height="{Binding ElementName=AlphaComponentNumericUpDown, Path=Bounds.Height}"
Background="{DynamicResource TextControlBackgroundDisabled}"
BorderBrush="{DynamicResource TextControlBorderBrush}"
BorderThickness="1,1,0,1"
CornerRadius="4,0,0,4"
VerticalAlignment="Center"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<TextBlock x:Name="AlphaComponentTextBlock"
Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="A"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsComponentTextInputVisible" />
</MultiBinding>
</Border.IsVisible>
</Border>
<NumericUpDown x:Name="AlphaComponentNumericUpDown"
Grid.Column="1"
Grid.Row="5"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,4,4,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=AlphaComponentSlider}"
Maximum="{Binding Maximum, ElementName=AlphaComponentSlider}"
Value="{Binding Value, ElementName=AlphaComponentSlider}"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<NumericUpDown.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsComponentTextInputVisible" />
</MultiBinding>
</NumericUpDown.IsVisible>
</NumericUpDown>
<primitives:ColorSlider x:Name="AlphaComponentSlider"
Grid.Column="2"
Grid.Row="5"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Alpha"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<primitives:ColorSlider.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsComponentSliderVisible" />
</MultiBinding>
</primitives:ColorSlider.IsVisible>
</primitives:ColorSlider>
</Grid>
</TabItem>
</TabControl>
<!-- Previewer -->
<!-- Note that top/bottom margins have -5 to remove for drop shadow padding -->
<primitives:ColorPreviewer Grid.Row="1"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
Margin="12,-5,12,7"
IsVisible="{TemplateBinding IsColorPreviewVisible}" />
</Grid>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>

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

@ -1,8 +1,15 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Avalonia.Controls"
xmlns:converters="using:Avalonia.Controls.Converters"
xmlns:primitives="using:Avalonia.Controls.Primitives"
xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker"
x:CompileBindings="True">
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml" />
</ResourceDictionary.MergedDictionaries>
<ControlTheme x:Key="{x:Type ColorPicker}"
TargetType="ColorPicker">
<Setter Property="CornerRadius" Value="0" />
@ -42,40 +49,455 @@
</Panel>
</DropDownButton.Content>
<DropDownButton.Flyout>
<Flyout FlyoutPresenterClasses="nopadding">
<ColorView x:Name="FlyoutColorView"
Color="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
ColorModel="{Binding ColorModel, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
ColorSpectrumComponents="{TemplateBinding ColorSpectrumComponents}"
ColorSpectrumShape="{TemplateBinding ColorSpectrumShape}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
IsAlphaEnabled="{TemplateBinding IsAlphaEnabled}"
IsAlphaVisible="{TemplateBinding IsAlphaVisible}"
IsColorComponentsVisible="{TemplateBinding IsColorComponentsVisible}"
IsColorModelVisible="{TemplateBinding IsColorModelVisible}"
IsColorPaletteVisible="{TemplateBinding IsColorPaletteVisible}"
IsColorPreviewVisible="{TemplateBinding IsColorPreviewVisible}"
IsColorSpectrumVisible="{TemplateBinding IsColorSpectrumVisible}"
IsColorSpectrumSliderVisible="{TemplateBinding IsColorSpectrumSliderVisible}"
IsComponentSliderVisible="{TemplateBinding IsComponentSliderVisible}"
IsComponentTextInputVisible="{TemplateBinding IsComponentTextInputVisible}"
IsHexInputVisible="{TemplateBinding IsHexInputVisible}"
MaxHue="{TemplateBinding MaxHue}"
MaxSaturation="{TemplateBinding MaxSaturation}"
MaxValue="{TemplateBinding MaxValue}"
MinHue="{TemplateBinding MinHue}"
MinSaturation="{TemplateBinding MinSaturation}"
MinValue="{TemplateBinding MinValue}"
PaletteColors="{TemplateBinding PaletteColors}"
PaletteColumnCount="{TemplateBinding PaletteColumnCount}"
Palette="{TemplateBinding Palette}"
SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<ColorView.Resources>
<Flyout>
<!-- The following is copy-pasted from the ColorView's control template.
It MUST always be kept in sync with the ColorView (which is master).
Note the only changes are resources specific to the ColorPicker. -->
<Grid RowDefinitions="Auto,Auto">
<Grid.Resources>
<!-- This radius must follow OverlayCornerRadius -->
<CornerRadius x:Key="ColorViewTabBackgroundCornerRadius">0,0,0,0</CornerRadius>
</ColorView.Resources>
</ColorView>
</Grid.Resources>
<!-- Backgrounds -->
<!-- TODO: Background="{DynamicResource ColorViewTabBackgroundBrush}" -->
<Border x:Name="TabBackgroundBorder"
Grid.Row="0"
Grid.RowSpan="2"
Height="48"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
BorderBrush="{DynamicResource ColorViewTabBorderBrush}"
CornerRadius="{DynamicResource ColorViewTabBackgroundCornerRadius}" />
<Border x:Name="ContentBackgroundBorder"
Grid.Row="0"
Grid.RowSpan="2"
Margin="0,48,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource BottomCornerRadiusFilterConverter}}"
Background="{DynamicResource ColorViewContentBackgroundBrush}"
BorderBrush="{DynamicResource ColorViewContentBorderBrush}"
BorderThickness="0,1,0,0" />
<TabControl x:Name="PART_TabControl"
Grid.Row="0"
Height="338"
Width="350"
Padding="0"
SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<TabControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="0"
Rows="1" />
</ItemsPanelTemplate>
</TabControl.ItemsPanel>
<!-- Spectrum Tab -->
<TabItem Theme="{StaticResource ColorViewTabItemTheme}"
IsVisible="{TemplateBinding IsColorSpectrumVisible}">
<TabItem.Header>
<Border Height="{DynamicResource ColorViewTabStripHeight}">
<PathIcon Width="20"
Height="20"
Data="{DynamicResource ColorViewSpectrumIconGeometry}" />
</Border>
</TabItem.Header>
<Grid RowDefinitions="*"
Margin="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
MinWidth="32" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"
MinWidth="32" />
</Grid.ColumnDefinitions>
<primitives:ColorSlider x:Name="ColorSpectrumThirdComponentSlider"
AutomationProperties.Name="Third Component"
Grid.Column="0"
IsAlphaMaxForced="True"
IsSaturationValueMaxForced="False"
Orientation="Vertical"
ColorModel="Hsva"
ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Margin="0,0,12,0"
IsVisible="{TemplateBinding IsColorSpectrumSliderVisible}" />
<primitives:ColorSpectrum x:Name="ColorSpectrum"
Grid.Column="1"
Components="{TemplateBinding ColorSpectrumComponents}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
MinHue="{TemplateBinding MinHue}"
MaxHue="{TemplateBinding MaxHue}"
MinSaturation="{TemplateBinding MinSaturation}"
MaxSaturation="{TemplateBinding MaxSaturation}"
MinValue="{TemplateBinding MinValue}"
MaxValue="{TemplateBinding MaxValue}"
Shape="{TemplateBinding ColorSpectrumShape}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<primitives:ColorSlider x:Name="ColorSpectrumAlphaSlider"
AutomationProperties.Name="Alpha Component"
Grid.Column="2"
Orientation="Vertical"
ColorModel="Hsva"
ColorComponent="Alpha"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Margin="12,0,0,0"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<primitives:ColorSlider.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
</MultiBinding>
</primitives:ColorSlider.IsVisible>
</primitives:ColorSlider>
</Grid>
</TabItem>
<!-- Palette Tab -->
<TabItem Theme="{StaticResource ColorViewTabItemTheme}"
IsVisible="{TemplateBinding IsColorPaletteVisible}">
<TabItem.Header>
<Border Height="{DynamicResource ColorViewTabStripHeight}">
<PathIcon Width="20"
Height="20"
Data="{DynamicResource ColorViewPaletteIconGeometry}" />
</Border>
</TabItem.Header>
<ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
Items="{TemplateBinding PaletteColors}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
UseLayoutRounding="False"
Margin="12">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type Color}">
<Border AutomationProperties.Name="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
ToolTip.Tip="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border.Background>
<SolidColorBrush Color="{Binding}" />
</Border.Background>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding $parent[ColorView].PaletteColumnCount}" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</TabItem>
<!-- Components Tab -->
<TabItem Theme="{StaticResource ColorViewTabItemTheme}"
IsVisible="{TemplateBinding IsColorComponentsVisible}">
<TabItem.Header>
<Border Height="{DynamicResource ColorViewTabStripHeight}">
<PathIcon Width="20"
Height="20"
Data="{DynamicResource ColorViewComponentsIconGeometry}" />
</Border>
</TabItem.Header>
<Grid ColumnDefinitions="Auto,Auto,*"
RowDefinitions="Auto,24,1*,1*,1*,1*,12"
Margin="12">
<!-- Top color model & Hex input -->
<Grid Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="0"
ColumnDefinitions="1*,12,1*">
<!-- Content RGB/HSV names are hard-coded and considered universal -->
<!-- RadioButtons are styled to look like a 'SegmentedControl' or 'ButtonGroup' -->
<Grid ColumnDefinitions="1*,1*"
IsVisible="{TemplateBinding IsColorModelVisible}">
<RadioButton x:Name="RgbRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
Grid.Column="0"
Content="RGB"
CornerRadius="0,0,0,0"
BorderThickness="1,1,0,1"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" />
<RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
Grid.Column="1"
Content="HSV"
CornerRadius="0,0,0,0"
BorderThickness="0,1,1,1"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" />
</Grid>
<Grid x:Name="HexInputGrid"
Grid.Column="2"
IsVisible="{TemplateBinding IsHexInputVisible}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
Height="32"
Background="{DynamicResource ThemeControlMidBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1,1,0,1"
CornerRadius="0,0,0,0">
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="#"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<!-- Color updated in code-behind -->
<TextBox x:Name="PART_HexTextBox"
Grid.Column="1"
AutomationProperties.Name="Hexadecimal Color"
Height="32"
MaxLength="8"
HorizontalAlignment="Stretch"
CornerRadius="0,0,0,0" />
</Grid>
</Grid>
<!-- Color component editing controls -->
<!-- Component 1 RGB:Red HSV:Hue -->
<Border Grid.Column="0"
Grid.Row="2"
Height="{Binding ElementName=Component1NumericUpDown, Path=Bounds.Height}"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Background="{DynamicResource ThemeControlMidBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1,1,0,1"
CornerRadius="0,0,0,0"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentTextInputVisible}">
<Panel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="R"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="H"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
</Panel>
</Border>
<NumericUpDown x:Name="Component1NumericUpDown"
Grid.Column="1"
Grid.Row="2"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,0,0,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=Component1Slider}"
Maximum="{Binding Maximum, ElementName=Component1Slider}"
Value="{Binding Value, ElementName=Component1Slider}"
IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
<primitives:ColorSlider x:Name="Component1Slider"
Grid.Column="2"
Grid.Row="2"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Component1"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentSliderVisible}" />
<!-- Component 2 RGB:Green HSV:Saturation -->
<Border Grid.Column="0"
Grid.Row="3"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Height="{Binding ElementName=Component2NumericUpDown, Path=Bounds.Height}"
Background="{DynamicResource ThemeControlMidBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1,1,0,1"
CornerRadius="0,0,0,0"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentTextInputVisible}">
<Panel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="G"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="S"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
</Panel>
</Border>
<NumericUpDown x:Name="Component2NumericUpDown"
Grid.Column="1"
Grid.Row="3"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,0,0,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=Component2Slider}"
Maximum="{Binding Maximum, ElementName=Component2Slider}"
Value="{Binding Value, ElementName=Component2Slider}"
IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
<primitives:ColorSlider x:Name="Component2Slider"
Grid.Column="2"
Grid.Row="3"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Component2"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentSliderVisible}" />
<!-- Component 3 RGB:Blue HSV:Value -->
<Border Grid.Column="0"
Grid.Row="4"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Height="{Binding ElementName=Component3NumericUpDown, Path=Bounds.Height}"
Background="{DynamicResource ThemeControlMidBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1,1,0,1"
CornerRadius="0,0,0,0"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentTextInputVisible}">
<Panel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="B"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="V"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
</Panel>
</Border>
<NumericUpDown x:Name="Component3NumericUpDown"
Grid.Column="1"
Grid.Row="4"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,0,0,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=Component3Slider}"
Maximum="{Binding Maximum, ElementName=Component3Slider}"
Value="{Binding Value, ElementName=Component3Slider}"
IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
<primitives:ColorSlider x:Name="Component3Slider"
Grid.Column="2"
Grid.Row="4"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Component3"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentSliderVisible}" />
<!-- Alpha Component -->
<Border Grid.Column="0"
Grid.Row="5"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Height="{Binding ElementName=AlphaComponentNumericUpDown, Path=Bounds.Height}"
Background="{DynamicResource ThemeControlMidBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1,1,0,1"
CornerRadius="0,0,0,0"
VerticalAlignment="Center"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<TextBlock x:Name="AlphaComponentTextBlock"
Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="A"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsComponentTextInputVisible" />
</MultiBinding>
</Border.IsVisible>
</Border>
<NumericUpDown x:Name="AlphaComponentNumericUpDown"
Grid.Column="1"
Grid.Row="5"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,0,0,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=AlphaComponentSlider}"
Maximum="{Binding Maximum, ElementName=AlphaComponentSlider}"
Value="{Binding Value, ElementName=AlphaComponentSlider}"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<NumericUpDown.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsComponentTextInputVisible" />
</MultiBinding>
</NumericUpDown.IsVisible>
</NumericUpDown>
<primitives:ColorSlider x:Name="AlphaComponentSlider"
Grid.Column="2"
Grid.Row="5"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Alpha"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<primitives:ColorSlider.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsComponentSliderVisible" />
</MultiBinding>
</primitives:ColorSlider.IsVisible>
</primitives:ColorSlider>
</Grid>
</TabItem>
</TabControl>
<!-- Previewer -->
<!-- Note that top/bottom margins have -5 to remove for drop shadow padding -->
<primitives:ColorPreviewer Grid.Row="1"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
Margin="12,-5,12,7"
IsVisible="{TemplateBinding IsColorPreviewVisible}" />
</Grid>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>

41
src/Avalonia.Controls/Control.cs

@ -84,6 +84,13 @@ namespace Avalonia.Controls
nameof(Unloaded),
RoutingStrategies.Direct);
/// <summary>
/// Defines the <see cref="SizeChanged"/> event.
/// </summary>
public static readonly RoutedEvent<SizeChangedEventArgs> SizeChangedEvent =
RoutedEvent.Register<Control, SizeChangedEventArgs>(
nameof(SizeChanged), RoutingStrategies.Direct);
/// <summary>
/// Defines the <see cref="FlowDirection"/> property.
/// </summary>
@ -211,6 +218,15 @@ namespace Avalonia.Controls
remove => RemoveHandler(UnloadedEvent, value);
}
/// <summary>
/// Occurs when the bounds (actual size) of the control have changed.
/// </summary>
public event EventHandler<SizeChangedEventArgs>? SizeChanged
{
add => AddHandler(SizeChangedEvent, value);
remove => RemoveHandler(SizeChangedEvent, value);
}
public new IControl? Parent => (IControl?)base.Parent;
/// <summary>
@ -530,14 +546,35 @@ namespace Avalonia.Controls
}
}
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == FlowDirectionProperty)
if (change.Property == BoundsProperty)
{
var oldValue = change.GetOldValue<Rect>();
var newValue = change.GetNewValue<Rect>();
// Bounds is a Rect with an X/Y Position as well as Height/Width.
// This means it is possible for the Rect to change position but not size.
// Therefore, we want to explicity check only the size and raise an event
// only when that size has changed.
if (newValue.Size != oldValue.Size)
{
var sizeChangedEventArgs = new SizeChangedEventArgs(
SizeChangedEvent,
source: this,
previousSize: new Size(oldValue.Width, oldValue.Height),
newSize: new Size(newValue.Width, newValue.Height));
RaiseEvent(sizeChangedEventArgs);
}
}
else if (change.Property == FlowDirectionProperty)
{
InvalidateMirrorTransform();
foreach (var visual in VisualChildren)
{
if (visual is Control child)

40
src/Avalonia.Controls/Documents/InlineCollection.cs

@ -12,7 +12,7 @@ namespace Avalonia.Controls.Documents
[WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList<Inline>
{
private ILogical? _parent;
private IAvaloniaList<ILogical>? _logicalChildren;
private IInlineHost? _inlineHost;
/// <summary>
@ -24,28 +24,30 @@ namespace Avalonia.Controls.Documents
this.ForEachItem(
x =>
{
((ISetLogicalParent)x).SetParent(Parent);
{
x.InlineHost = InlineHost;
LogicalChildren?.Add(x);
Invalidate();
},
x =>
{
((ISetLogicalParent)x).SetParent(null);
LogicalChildren?.Remove(x);
x.InlineHost = InlineHost;
Invalidate();
},
() => throw new NotSupportedException());
}
internal ILogical? Parent
internal IAvaloniaList<ILogical>? LogicalChildren
{
get => _parent;
get => _logicalChildren;
set
{
_parent = value;
var oldValue = _logicalChildren;
_logicalChildren = value;
OnParentChanged(value);
OnParentChanged(oldValue, value);
}
}
@ -70,6 +72,11 @@ namespace Avalonia.Controls.Documents
{
get
{
if (Count == 0)
{
return null;
}
var builder = StringBuilderCache.Acquire();
foreach (var inline in this)
@ -111,7 +118,7 @@ namespace Avalonia.Controls.Documents
private void AddText(string text)
{
if (Parent is RichTextBlock textBlock && !textBlock.HasComplexContent)
if (LogicalChildren is TextBlock textBlock && !textBlock.HasComplexContent)
{
textBlock._text += text;
}
@ -123,7 +130,7 @@ namespace Avalonia.Controls.Documents
private void OnAdd()
{
if (Parent is RichTextBlock textBlock)
if (LogicalChildren is TextBlock textBlock)
{
if (!textBlock.HasComplexContent && !string.IsNullOrEmpty(textBlock._text))
{
@ -152,20 +159,21 @@ namespace Avalonia.Controls.Documents
Invalidated?.Invoke(this, EventArgs.Empty);
}
private void OnParentChanged(ILogical? parent)
private void OnParentChanged(IAvaloniaList<ILogical>? oldParent, IAvaloniaList<ILogical>? newParent)
{
foreach (var child in this)
{
var oldParent = child.Parent;
if (oldParent != parent)
if (oldParent != newParent)
{
if (oldParent != null)
{
((ISetLogicalParent)child).SetParent(null);
oldParent.Remove(child);
}
((ISetLogicalParent)child).SetParent(parent);
if(newParent != null)
{
newParent.Add(child);
}
}
}
}

6
src/Avalonia.Controls/Documents/Span.cs

@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents
{
Inlines = new InlineCollection
{
Parent = this
LogicalChildren = LogicalChildren
};
}
@ -78,14 +78,14 @@ namespace Avalonia.Controls.Documents
{
if (oldValue is not null)
{
oldValue.Parent = null;
oldValue.LogicalChildren = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate();
}
if (newValue is not null)
{
newValue.Parent = this;
newValue.LogicalChildren = LogicalChildren;
newValue.InlineHost = InlineHost;
newValue.Invalidated += (s, e) => InlineHost?.Invalidate();
}

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

@ -80,6 +80,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<double> LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner<TextPresenter>();
/// <summary>
/// Defines the <see cref="LetterSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> LetterSpacingProperty =
TextBlock.LetterSpacingProperty.AddOwner<TextPresenter>();
/// <summary>
/// Defines the <see cref="Background"/> property.
/// </summary>
@ -212,6 +218,15 @@ namespace Avalonia.Controls.Presenters
set => SetValue(LineHeightProperty, value);
}
/// <summary>
/// Gets or sets the letter spacing.
/// </summary>
public double LetterSpacing
{
get => GetValue(LetterSpacingProperty);
set => SetValue(LetterSpacingProperty, value);
}
/// <summary>
/// Gets or sets the text alignment.
/// </summary>
@ -333,7 +348,7 @@ namespace Avalonia.Controls.Presenters
var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment,
TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
flowDirection: FlowDirection, lineHeight: LineHeight);
flowDirection: FlowDirection, lineHeight: LineHeight, letterSpacing: LetterSpacing);
return textLayout;
}
@ -916,6 +931,9 @@ namespace Avalonia.Controls.Presenters
case nameof(TextAlignment):
case nameof(TextWrapping):
case nameof(LineHeight):
case nameof(LetterSpacing):
case nameof(SelectionStart):
case nameof(SelectionEnd):
case nameof(SelectionForegroundBrush):

374
src/Avalonia.Controls/RichTextBlock.cs → src/Avalonia.Controls/SelectableTextBlock.cs

@ -8,7 +8,6 @@ using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls
@ -16,67 +15,53 @@ namespace Avalonia.Controls
/// <summary>
/// A control that displays a block of formatted text.
/// </summary>
public class RichTextBlock : TextBlock, IInlineHost
public class SelectableTextBlock : TextBlock, IInlineHost
{
public static readonly StyledProperty<bool> IsTextSelectionEnabledProperty =
AvaloniaProperty.Register<RichTextBlock, bool>(nameof(IsTextSelectionEnabled), false);
public static readonly DirectProperty<RichTextBlock, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
public static readonly DirectProperty<SelectableTextBlock, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
nameof(SelectionStart),
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
public static readonly DirectProperty<RichTextBlock, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
public static readonly DirectProperty<SelectableTextBlock, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
nameof(SelectionEnd),
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
public static readonly DirectProperty<RichTextBlock, string> SelectedTextProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, string>(
public static readonly DirectProperty<SelectableTextBlock, string> SelectedTextProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, string>(
nameof(SelectedText),
o => o.SelectedText);
public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue);
AvaloniaProperty.Register<SelectableTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue);
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly StyledProperty<InlineCollection?> InlinesProperty =
AvaloniaProperty.Register<RichTextBlock, InlineCollection?>(
nameof(Inlines));
public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
public static readonly DirectProperty<SelectableTextBlock, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, bool>(
nameof(CanCopy),
o => o.CanCopy);
public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
RoutedEvent.Register<RichTextBlock, RoutedEventArgs>(
RoutedEvent.Register<SelectableTextBlock, RoutedEventArgs>(
nameof(CopyingToClipboard), RoutingStrategies.Bubble);
private bool _canCopy;
private int _selectionStart;
private int _selectionEnd;
private int _wordSelectionStart = -1;
private IReadOnlyList<TextRun>? _textRuns;
static RichTextBlock()
static SelectableTextBlock()
{
FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true);
AffectsRender<RichTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty, IsTextSelectionEnabledProperty);
FocusableProperty.OverrideDefaultValue(typeof(SelectableTextBlock), true);
AffectsRender<SelectableTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty);
}
public RichTextBlock()
public event EventHandler<RoutedEventArgs>? CopyingToClipboard
{
Inlines = new InlineCollection
{
Parent = this,
InlineHost = this
};
add => AddHandler(CopyingToClipboardEvent, value);
remove => RemoveHandler(CopyingToClipboardEvent, value);
}
/// <summary>
@ -99,6 +84,8 @@ namespace Avalonia.Controls
if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
}
}
}
@ -114,6 +101,8 @@ namespace Avalonia.Controls
if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
}
}
}
@ -126,25 +115,6 @@ namespace Avalonia.Controls
get => GetSelection();
}
/// <summary>
/// Gets or sets a value that indicates whether text selection is enabled, either through user action or calling selection-related API.
/// </summary>
public bool IsTextSelectionEnabled
{
get => GetValue(IsTextSelectionEnabledProperty);
set => SetValue(IsTextSelectionEnabledProperty, value);
}
/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection? Inlines
{
get => GetValue(InlinesProperty);
set => SetValue(InlinesProperty, value);
}
/// <summary>
/// Property for determining if the Copy command can be executed.
/// </summary>
@ -154,20 +124,12 @@ namespace Avalonia.Controls
private set => SetAndRaise(CanCopyProperty, ref _canCopy, value);
}
public event EventHandler<RoutedEventArgs>? CopyingToClipboard
{
add => AddHandler(CopyingToClipboardEvent, value);
remove => RemoveHandler(CopyingToClipboardEvent, value);
}
internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
/// <summary>
/// Copies the current selection to the Clipboard.
/// </summary>
public async void Copy()
{
if (_canCopy || !IsTextSelectionEnabled)
if (!_canCopy)
{
return;
}
@ -188,45 +150,13 @@ namespace Avalonia.Controls
await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
.SetTextAsync(text);
}
}
protected override void RenderTextLayout(DrawingContext context, Point origin)
{
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var selectionBrush = SelectionBrush;
var selectionEnabled = IsTextSelectionEnabled;
if (selectionEnabled && selectionStart != selectionEnd && selectionBrush != null)
{
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
var rects = TextLayout.HitTestTextRange(start, length);
using (context.PushPostTransform(Matrix.CreateTranslation(origin)))
{
foreach (var rect in rects)
{
context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
}
}
}
base.RenderTextLayout(context, origin);
}
}
/// <summary>
/// Select all text in the TextBox
/// </summary>
public void SelectAll()
{
if (!IsTextSelectionEnabled)
{
return;
}
var text = Text;
SelectionStart = 0;
@ -238,94 +168,52 @@ namespace Avalonia.Controls
/// </summary>
public void ClearSelection()
{
if (!IsTextSelectionEnabled)
{
return;
}
SelectionEnd = SelectionStart;
}
protected void AddText(string? text)
protected override void OnGotFocus(GotFocusEventArgs e)
{
if (string.IsNullOrEmpty(text))
{
return;
}
if (!HasComplexContent && string.IsNullOrEmpty(_text))
{
_text = text;
}
else
{
if (!string.IsNullOrEmpty(_text))
{
Inlines?.Add(_text);
_text = null;
}
Inlines?.Add(text);
}
}
base.OnGotFocus(e);
protected override string? GetText()
{
return _text ?? Inlines?.Text;
UpdateCommandStates();
}
protected override void SetText(string? text)
protected override void OnLostFocus(RoutedEventArgs e)
{
var oldValue = GetText();
base.OnLostFocus(e);
AddText(text);
if ((ContextFlyout == null || !ContextFlyout.IsOpen) &&
(ContextMenu == null || !ContextMenu.IsOpen))
{
ClearSelection();
}
RaisePropertyChanged(TextProperty, oldValue, text);
UpdateCommandStates();
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected override TextLayout CreateTextLayout(string? text)
protected override void RenderTextLayout(DrawingContext context, Point origin)
{
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var defaultProperties = new GenericTextRunProperties(
typeface,
FontSize,
TextDecorations,
Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
ITextSource textSource;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var selectionBrush = SelectionBrush;
if (_textRuns != null)
if (selectionStart != selectionEnd && selectionBrush != null)
{
textSource = new InlinesTextSource(_textRuns);
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
}
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
return new TextLayout(
textSource,
paragraphProperties,
TextTrimming,
_constraint.Width,
_constraint.Height,
maxLines: MaxLines,
lineHeight: LineHeight);
}
var rects = TextLayout.HitTestTextRange(start, length);
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
using (context.PushPostTransform(Matrix.CreateTranslation(origin)))
{
foreach (var rect in rects)
{
context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
}
}
}
ClearSelection();
base.RenderTextLayout(context, origin);
}
protected override void OnKeyDown(KeyEventArgs e)
@ -352,11 +240,6 @@ namespace Avalonia.Controls
{
base.OnPointerPressed(e);
if (!IsTextSelectionEnabled)
{
return;
}
var text = Text;
var clickInfo = e.GetCurrentPoint(this);
@ -435,11 +318,6 @@ namespace Avalonia.Controls
{
base.OnPointerMoved(e);
if (!IsTextSelectionEnabled)
{
return;
}
// selection should not change during pointer move if the user right clicks
if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
@ -486,11 +364,6 @@ namespace Avalonia.Controls
{
base.OnPointerReleased(e);
if (!IsTextSelectionEnabled)
{
return;
}
if (e.Pointer.Captured != this)
{
return;
@ -521,100 +394,15 @@ namespace Avalonia.Controls
e.Pointer.Capture(null);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(Inlines):
{
OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
InvalidateTextLayout();
break;
}
}
}
protected override Size MeasureOverride(Size availableSize)
private void UpdateCommandStates()
{
if(_textRuns != null)
{
LogicalChildren.Clear();
VisualChildren.Clear();
_textRuns = null;
}
if (Inlines != null && Inlines.Count > 0)
{
var inlines = Inlines;
var textRuns = new List<TextRun>();
foreach (var inline in inlines)
{
inline.BuildTextRun(textRuns);
}
foreach (var textRun in textRuns)
{
if (textRun is EmbeddedControlRun controlRun &&
controlRun.Control is Control control)
{
LogicalChildren.Add(control);
VisualChildren.Add(control);
control.Measure(Size.Infinity);
}
}
_textRuns = textRuns;
}
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (HasComplexContent)
{
var currentY = 0.0;
foreach (var textLine in TextLayout.TextLines)
{
var currentX = textLine.Start;
foreach (var run in textLine.TextRuns)
{
if (run is DrawableTextRun drawable)
{
if (drawable is EmbeddedControlRun controlRun
&& controlRun.Control is Control control)
{
control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
}
currentX += drawable.Size.Width;
}
}
var text = GetSelection();
currentY += textLine.Height;
}
}
return base.ArrangeOverride(finalSize);
CanCopy = !string.IsNullOrEmpty(text);
}
private string GetSelection()
{
if (!IsTextSelectionEnabled)
{
return "";
}
var text = GetText();
if (string.IsNullOrEmpty(text))
@ -638,59 +426,5 @@ namespace Avalonia.Controls
return selectedText;
}
private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
{
if (oldValue is not null)
{
oldValue.Parent = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InvalidateTextLayout();
}
if (newValue is not null)
{
newValue.Parent = this;
newValue.InlineHost = this;
newValue.Invalidated += (s, e) => InvalidateTextLayout();
}
}
void IInlineHost.Invalidate()
{
InvalidateTextLayout();
}
private readonly struct InlinesTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;
public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
{
_textRuns = textRuns;
}
public TextRun? GetTextRun(int textSourceIndex)
{
var currentPosition = 0;
foreach (var textRun in _textRuns)
{
if (textRun.TextSourceLength == 0)
{
continue;
}
if (currentPosition >= textSourceIndex)
{
return textRun;
}
currentPosition += textRun.TextSourceLength;
}
return null;
}
}
}
}

83
src/Avalonia.Controls/SizeChangedEventArgs.cs

@ -0,0 +1,83 @@
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Utilities;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data specific to a SizeChanged event.
/// </summary>
public class SizeChangedEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="SizeChangedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
public SizeChangedEventArgs(RoutedEvent? routedEvent)
: base (routedEvent)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SizeChangedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
/// <param name="source">The source object that raised the routed event.</param>
public SizeChangedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
: base(routedEvent, source)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SizeChangedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
/// <param name="source">The source object that raised the routed event.</param>
/// <param name="previousSize">The previous size (or bounds) of the object.</param>
/// <param name="newSize">The new size (or bounds) of the object.</param>
public SizeChangedEventArgs(
RoutedEvent? routedEvent,
IInteractive? source,
Size previousSize,
Size newSize)
: base(routedEvent, source)
{
PreviousSize = previousSize;
NewSize = newSize;
}
/// <summary>
/// Gets a value indicating whether the height of the new size is considered
/// different than the previous size height.
/// </summary>
/// <remarks>
/// This will take into account layout epsilon and will not be true if both
/// heights are considered equivalent for layout purposes. Remember there can
/// be small variations in the calculations between layout cycles due to
/// rounding and precision even when the size has not otherwise changed.
/// </remarks>
public bool HeightChanged => !MathUtilities.AreClose(NewSize.Height, PreviousSize.Height, LayoutHelper.LayoutEpsilon);
/// <summary>
/// Gets the new size (or bounds) of the object.
/// </summary>
public Size NewSize { get; init; }
/// <summary>
/// Gets the previous size (or bounds) of the object.
/// </summary>
public Size PreviousSize { get; init; }
/// <summary>
/// Gets a value indicating whether the width of the new size is considered
/// different than the previous size width.
/// </summary>
/// <remarks>
/// This will take into account layout epsilon and will not be true if both
/// widths are considered equivalent for layout purposes. Remember there can
/// be small variations in the calculations between layout cycles due to
/// rounding and precision even when the size has not otherwise changed.
/// </remarks>
public bool WidthChanged => !MathUtilities.AreClose(NewSize.Width, PreviousSize.Width, LayoutHelper.LayoutEpsilon);
}
}

323
src/Avalonia.Controls/TextBlock.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents;
using Avalonia.Layout;
@ -12,7 +13,7 @@ namespace Avalonia.Controls
/// <summary>
/// A control that displays a block of text.
/// </summary>
public class TextBlock : Control, IAddChild<string>
public class TextBlock : Control, IInlineHost
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -81,6 +82,15 @@ namespace Avalonia.Controls
validate: IsValidLineHeight,
inherits: true);
/// <summary>
/// Defines the <see cref="LetterSpacing"/> property.
/// </summary>
public static readonly AttachedProperty<double> LetterSpacingProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, double>(
nameof(LetterSpacing),
0,
inherits: true);
/// <summary>
/// Defines the <see cref="MaxLines"/> property.
/// </summary>
@ -96,15 +106,15 @@ namespace Avalonia.Controls
public static readonly DirectProperty<TextBlock, string?> TextProperty =
AvaloniaProperty.RegisterDirect<TextBlock, string?>(
nameof(Text),
o => o.Text,
(o, v) => o.Text = v);
o => o.GetText(),
(o, v) => o.SetText(v));
/// <summary>
/// Defines the <see cref="TextAlignment"/> property.
/// </summary>
public static readonly AttachedProperty<TextAlignment> TextAlignmentProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextAlignment>(
nameof(TextAlignment),
nameof(TextAlignment),
defaultValue: TextAlignment.Start,
inherits: true);
@ -112,14 +122,14 @@ namespace Avalonia.Controls
/// Defines the <see cref="TextWrapping"/> property.
/// </summary>
public static readonly AttachedProperty<TextWrapping> TextWrappingProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextWrapping>(nameof(TextWrapping),
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextWrapping>(nameof(TextWrapping),
inherits: true);
/// <summary>
/// Defines the <see cref="TextTrimming"/> property.
/// </summary>
public static readonly AttachedProperty<TextTrimming> TextTrimmingProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextTrimming>(nameof(TextTrimming),
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextTrimming>(nameof(TextTrimming),
defaultValue: TextTrimming.None,
inherits: true);
@ -129,9 +139,17 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations));
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly StyledProperty<InlineCollection?> InlinesProperty =
AvaloniaProperty.Register<TextBlock, InlineCollection?>(
nameof(Inlines));
internal string? _text;
protected TextLayout? _textLayout;
protected Size _constraint;
private IReadOnlyList<TextRun>? _textRuns;
/// <summary>
/// Initializes static members of the <see cref="TextBlock"/> class.
@ -139,10 +157,19 @@ namespace Avalonia.Controls
static TextBlock()
{
ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty);
}
public TextBlock()
{
Inlines = new InlineCollection
{
LogicalChildren = LogicalChildren,
InlineHost = this
};
}
/// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary>
@ -244,6 +271,15 @@ namespace Avalonia.Controls
set => SetValue(LineHeightProperty, value);
}
/// <summary>
/// Gets or sets the letter spacing.
/// </summary>
public double LetterSpacing
{
get => GetValue(LetterSpacingProperty);
set => SetValue(LetterSpacingProperty, value);
}
/// <summary>
/// Gets or sets the maximum number of text lines.
/// </summary>
@ -288,9 +324,21 @@ namespace Avalonia.Controls
get => GetValue(TextDecorationsProperty);
set => SetValue(TextDecorationsProperty, value);
}
/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection? Inlines
{
get => GetValue(InlinesProperty);
set => SetValue(InlinesProperty, value);
}
protected override bool BypassFlowDirectionPolicies => true;
internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
/// <summary>
/// The BaselineOffset property provides an adjustment to baseline offset
/// </summary>
@ -445,6 +493,35 @@ namespace Avalonia.Controls
control.SetValue(LineHeightProperty, height);
}
/// <summary>
/// Reads the attached property from the given element
/// </summary>
/// <param name="control">The element to which to read the attached property.</param>
public static double GetLetterSpacing(Control control)
{
if (control == null)
{
throw new ArgumentNullException(nameof(control));
}
return control.GetValue(LetterSpacingProperty);
}
/// <summary>
/// Writes the attached property LetterSpacing to the given element.
/// </summary>
/// <param name="control">The element to which to write the attached property.</param>
/// <param name="letterSpacing">The property value to set</param>
public static void SetLetterSpacing(Control control, double letterSpacing)
{
if (control == null)
{
throw new ArgumentNullException(nameof(control));
}
control.SetValue(LetterSpacingProperty, letterSpacing);
}
/// <summary>
/// Reads the attached property from the given element
/// </summary>
@ -513,19 +590,30 @@ namespace Avalonia.Controls
TextLayout.Draw(context, origin);
}
void IAddChild<string>.AddChild(string text)
{
_text = text;
}
protected virtual string? GetText()
{
return _text;
return _text ?? Inlines?.Text;
}
protected virtual void SetText(string? text)
{
SetAndRaise(TextProperty, ref _text, text);
if (Inlines != null && Inlines.Count > 0)
{
var oldValue = Inlines.Text;
if (!string.IsNullOrEmpty(text))
{
Inlines.Add(text);
}
text = Inlines.Text;
RaisePropertyChanged(TextProperty, oldValue, text);
}
else
{
SetAndRaise(TextProperty, ref _text, text);
}
}
/// <summary>
@ -534,17 +622,30 @@ namespace Avalonia.Controls
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual TextLayout CreateTextLayout(string? text)
{
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var defaultProperties = new GenericTextRunProperties(
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
typeface,
FontSize,
TextDecorations,
Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
defaultProperties, TextWrapping, LineHeight, 0, LetterSpacing);
ITextSource textSource;
if (_textRuns != null)
{
textSource = new InlinesTextSource(_textRuns);
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
}
return new TextLayout(
new SimpleTextSource((text ?? "").AsMemory(), defaultProperties),
textSource,
paragraphProperties,
TextTrimming,
_constraint.Width,
@ -560,6 +661,8 @@ namespace Avalonia.Controls
{
_textLayout = null;
InvalidateVisual();
InvalidateMeasure();
}
@ -573,7 +676,46 @@ namespace Avalonia.Controls
_textLayout = null;
InvalidateArrange();
var inlines = Inlines;
if (HasComplexContent)
{
if (_textRuns != null)
{
foreach (var textRun in _textRuns)
{
if (textRun is EmbeddedControlRun controlRun &&
controlRun.Control is Control control)
{
VisualChildren.Remove(control);
LogicalChildren.Remove(control);
}
}
}
var textRuns = new List<TextRun>();
foreach (var inline in inlines!)
{
inline.BuildTextRun(textRuns);
}
foreach (var textRun in textRuns)
{
if (textRun is EmbeddedControlRun controlRun &&
controlRun.Control is Control control)
{
VisualChildren.Add(control);
LogicalChildren.Add(control);
control.Measure(Size.Infinity);
}
}
_textRuns = textRuns;
}
var measuredSize = TextLayout.Bounds.Size.Inflate(padding);
@ -584,16 +726,11 @@ namespace Avalonia.Controls
{
var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
if(finalSize.Width < textWidth)
if (finalSize.Width < textWidth)
{
finalSize = finalSize.WithWidth(textWidth);
}
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
{
return finalSize;
}
var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
@ -602,6 +739,32 @@ namespace Avalonia.Controls
_textLayout = null;
if (HasComplexContent)
{
var currentY = padding.Top;
foreach (var textLine in TextLayout.TextLines)
{
var currentX = padding.Left + textLine.Start;
foreach (var run in textLine.TextRuns)
{
if (run is DrawableTextRun drawable)
{
if (drawable is EmbeddedControlRun controlRun
&& controlRun.Control is Control control)
{
control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
}
currentX += drawable.Size.Width;
}
}
currentY += textLine.Height;
}
}
return finalSize;
}
@ -610,42 +773,71 @@ namespace Avalonia.Controls
return new TextBlockAutomationPeer(this);
}
private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof (FontSize):
case nameof (FontWeight):
case nameof (FontStyle):
case nameof (FontFamily):
case nameof (FontStretch):
case nameof(FontSize):
case nameof(FontWeight):
case nameof(FontStyle):
case nameof(FontFamily):
case nameof(FontStretch):
case nameof (TextWrapping):
case nameof (TextTrimming):
case nameof (TextAlignment):
case nameof(TextWrapping):
case nameof(TextTrimming):
case nameof(TextAlignment):
case nameof (FlowDirection):
case nameof(FlowDirection):
case nameof (Padding):
case nameof (LineHeight):
case nameof (LetterSpacing):
case nameof (MaxLines):
case nameof (Text):
case nameof (TextDecorations):
case nameof (Foreground):
{
InvalidateTextLayout();
break;
}
case nameof(Text):
case nameof(TextDecorations):
case nameof(Foreground):
{
InvalidateTextLayout();
break;
}
case nameof(Inlines):
{
OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
InvalidateTextLayout();
break;
}
}
}
private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;
private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
{
if (oldValue is not null)
{
oldValue.LogicalChildren = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InvalidateTextLayout();
}
if (newValue is not null)
{
newValue.LogicalChildren = LogicalChildren;
newValue.InlineHost = this;
newValue.Invalidated += (s, e) => InvalidateTextLayout();
}
}
void IInlineHost.Invalidate()
{
InvalidateTextLayout();
}
protected readonly struct SimpleTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
@ -674,5 +866,46 @@ namespace Avalonia.Controls
return new TextCharacters(runText, _defaultProperties);
}
}
private readonly struct InlinesTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;
public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
{
_textRuns = textRuns;
}
public IReadOnlyList<TextRun> TextRuns => _textRuns;
public TextRun? GetTextRun(int textSourceIndex)
{
var currentPosition = 0;
foreach (var textRun in _textRuns)
{
if (textRun.TextSourceLength == 0)
{
continue;
}
if (textSourceIndex >= currentPosition + textRun.TextSourceLength)
{
currentPosition += textRun.TextSourceLength;
continue;
}
if (textRun is TextCharacters)
{
return new TextCharacters(textRun.Text.Skip(Math.Max(0, textSourceIndex - currentPosition)), textRun.Properties!);
}
return textRun;
}
return null;
}
}
}
}

12
src/Avalonia.Controls/TextBox.cs

@ -114,6 +114,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner<TextBox>();
/// <summary>
/// Defines see <see cref="TextBlock.LetterSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> LetterSpacingProperty =
TextBlock.LetterSpacingProperty.AddOwner<TextBox>();
public static readonly StyledProperty<string?> WatermarkProperty =
AvaloniaProperty.Register<TextBox, string?>(nameof(Watermark));
@ -378,6 +384,12 @@ namespace Avalonia.Controls
set => SetValue(MaxLinesProperty, value);
}
public double LetterSpacing
{
get => GetValue(LetterSpacingProperty);
set => SetValue(LetterSpacingProperty, value);
}
/// <summary>
/// Gets or sets the line height.
/// </summary>

42
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -115,19 +115,16 @@ namespace Avalonia.Headless
return new HeadlessGeometryStub(new Rect(glyphRun.Size));
}
public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{
return new HeadlessGlyphRunBufferStub();
return new HeadlessGlyphRunStub();
}
public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
return new HeadlessHorizontalGlyphRunBufferStub();
}
public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
class HeadlessGlyphRunStub : IGlyphRunImpl
{
return new HeadlessPositionedGlyphRunBufferStub();
public void Dispose()
{
}
}
class HeadlessGeometryStub : IGeometryImpl
@ -213,33 +210,6 @@ namespace Avalonia.Headless
public Matrix Transform { get; }
}
class HeadlessGlyphRunBufferStub : IGlyphRunBuffer
{
public Span<ushort> GlyphIndices => Span<ushort>.Empty;
public IGlyphRunImpl Build()
{
return new HeadlessGlyphRunStub();
}
}
class HeadlessHorizontalGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IHorizontalGlyphRunBuffer
{
public Span<float> GlyphPositions => Span<float>.Empty;
}
class HeadlessPositionedGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IPositionedGlyphRunBuffer
{
public Span<System.Drawing.PointF> GlyphPositions => Span<System.Drawing.PointF>.Empty;
}
class HeadlessGlyphRunStub : IGlyphRunImpl
{
public void Dispose()
{
}
}
class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl
{
public HeadlessStreamingGeometryStub() : base(Rect.Empty)

2
src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml

@ -68,7 +68,7 @@
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/Slider.xaml" />
<!-- ManagedFileChooser comes last because it uses (and overrides) styles for a multitude of other controls...the dialogs were originally UserControls, after all -->
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml"/>
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>

14
src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml

@ -1,14 +0,0 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<RichTextBlock IsTextSelectionEnabled="True" Text="Preview"/>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type RichTextBlock}" TargetType="RichTextBlock">
<Style Selector="^[IsTextSelectionEnabled=True]">
<Setter Property="Cursor" Value="IBeam" />
</Style>
</ControlTheme>
</ResourceDictionary>

18
src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml

@ -0,0 +1,18 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<SelectableTextBlock Text="Preview" />
</Design.PreviewWith>
<MenuFlyout x:Key="SelectableTextBlockContextFlyout" Placement="Bottom">
<MenuItem x:Name="SelectableTextBlockContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[SelectableTextBlock].Copy}"
IsEnabled="{Binding $parent[SelectableTextBlock].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}" />
</MenuFlyout>
<ControlTheme x:Key="{x:Type SelectableTextBlock}" TargetType="SelectableTextBlock">
<Style Selector="^[IsEnabled=True]">
<Setter Property="Cursor" Value="IBeam" />
<Setter Property="ContextFlyout" Value="{StaticResource SelectableTextBlockContextFlyout}" />
</Style>
</ControlTheme>
</ResourceDictionary>

1
src/Avalonia.Themes.Fluent/Controls/TextBox.xaml

@ -161,6 +161,7 @@
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}"
LineHeight="{TemplateBinding LineHeight}"
LetterSpacing="{TemplateBinding LetterSpacing}"
PasswordChar="{TemplateBinding PasswordChar}"
RevealPassword="{TemplateBinding RevealPassword}"
SelectionBrush="{TemplateBinding SelectionBrush}"

14
src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml

@ -1,14 +0,0 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<RichTextBlock IsTextSelectionEnabled="True"
Text="Preview" />
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type RichTextBlock}"
TargetType="RichTextBlock">
<Style Selector="^[IsTextSelectionEnabled=True]">
<Setter Property="Cursor" Value="IBeam" />
</Style>
</ControlTheme>
</ResourceDictionary>

18
src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml

@ -0,0 +1,18 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<SelectableTextBlock Text="Preview" />
</Design.PreviewWith>
<MenuFlyout x:Key="SelectableTextBlockContextFlyout" Placement="Bottom">
<MenuItem x:Name="SelectableTextBlockContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[SelectableTextBlock].Copy}"
IsEnabled="{Binding $parent[SelectableTextBlock].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}" />
</MenuFlyout>
<ControlTheme x:Key="{x:Type SelectableTextBlock}" TargetType="SelectableTextBlock">
<Style Selector="^[IsEnabled=True]">
<Setter Property="Cursor" Value="IBeam" />
<Setter Property="ContextFlyout" Value="{StaticResource SelectableTextBlockContextFlyout}" />
</Style>
</ControlTheme>
</ResourceDictionary>

2
src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml

@ -64,7 +64,7 @@
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/SplitView.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/SplitButton.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/RichTextBlock.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>

4
src/Avalonia.Themes.Simple/Controls/TextBox.xaml

@ -149,14 +149,14 @@
CaretBrush="{TemplateBinding CaretBrush}"
CaretIndex="{TemplateBinding CaretIndex}"
LineHeight="{TemplateBinding LineHeight}"
LetterSpacing="{TemplateBinding LetterSpacing}"
PasswordChar="{TemplateBinding PasswordChar}"
RevealPassword="{TemplateBinding RevealPassword}"
SelectionBrush="{TemplateBinding SelectionBrush}"
SelectionEnd="{TemplateBinding SelectionEnd}"
SelectionForegroundBrush="{TemplateBinding SelectionForegroundBrush}"
SelectionStart="{TemplateBinding SelectionStart}"
Text="{TemplateBinding Text,
Mode=TwoWay}"
Text="{TemplateBinding Text,Mode=TwoWay}"
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}" />
</Panel>

5
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -9,6 +9,7 @@
<NoWarn>CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\Shared\StringCompatibilityExtensions.cs" Link="Compatibility\StringCompatibilityExtensions.cs" />
<Compile Include="AvaloniaXamlLoader.cs" />
<Compile Include="Converters\AvaloniaUriTypeConverter.cs" />
<Compile Include="Converters\ColorToBrushConverter.cs" />
@ -68,4 +69,8 @@
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Markup.Xaml.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
<ItemGroup>
<Folder Include="Compatibility\" />
</ItemGroup>
</Project>

12
src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs

@ -168,7 +168,11 @@ namespace Avalonia.Markup.Parsers
}
}
private static State ParseAttachedProperty(scoped ref CharacterReader r, List<INode> nodes)
private static State ParseAttachedProperty(
#if NET7SDK
scoped
#endif
ref CharacterReader r, List<INode> nodes)
{
var (ns, owner) = ParseTypeName(ref r);
@ -318,7 +322,11 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember;
}
private static TypeName ParseTypeName(scoped ref CharacterReader r)
private static TypeName ParseTypeName(
#if NET7SDK
scoped
#endif
ref CharacterReader r)
{
ReadOnlySpan<char> ns, typeName;
ns = ReadOnlySpan<char>.Empty;

2
src/Avalonia.Base/Compatibility/StringCompatibilityExtensions.cs → src/Shared/StringCompatibilityExtensions.cs

@ -3,7 +3,7 @@
namespace System;
#if !NET6_0_OR_GREATER
public static class StringCompatibilityExtensions
internal static class StringCompatibilityExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Contains(this string str, char search) =>

4
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -69,10 +69,6 @@ namespace Avalonia.Skia
public int GlyphCount { get; }
public bool IsFakeBold { get; }
public bool IsFakeItalic { get; }
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{
metrics = default;

126
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -12,8 +12,6 @@ using Avalonia.OpenGL.Imaging;
using Avalonia.Platform;
using Avalonia.Media.Imaging;
using SkiaSharp;
using System.Runtime.InteropServices;
using System.Drawing;
namespace Avalonia.Skia
{
@ -79,7 +77,7 @@ namespace Avalonia.Skia
var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize)
{
Size = fontRenderingEmSize,
Edging = SKFontEdging.Antialias,
Edging = SKFontEdging.Alias,
Hinting = SKFontHinting.None,
LinearMetrics = true
};
@ -244,85 +242,91 @@ namespace Avalonia.Skia
"Current GPU acceleration backend does not support OpenGL integration");
}
public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
=> new SKGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{
if (glyphTypeface == null)
{
throw new ArgumentNullException(nameof(glyphTypeface));
}
public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
=> new SKHorizontalGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
if (glyphIndices == null)
{
throw new ArgumentNullException(nameof(glyphIndices));
}
public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
=> new SKPositionedGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl;
private abstract class SKGlyphRunBufferBase : IGlyphRunBuffer
{
protected readonly SKTextBlobBuilder _builder;
protected readonly SKFont _font;
var font = new SKFont
{
LinearMetrics = true,
Subpixel = true,
Edging = SKFontEdging.SubpixelAntialias,
Hinting = SKFontHinting.Full,
Size = (float)fontRenderingEmSize,
Typeface = glyphTypefaceImpl.Typeface,
Embolden = (glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0,
SkewX = (glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0
};
var builder = new SKTextBlobBuilder();
public SKGlyphRunBufferBase(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
var count = glyphIndices.Count;
if(glyphOffsets != null && glyphAdvances != null)
{
_builder = new SKTextBlobBuilder();
var runBuffer = builder.AllocatePositionedRun(font, count);
var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
var glyphSpan = runBuffer.GetGlyphSpan();
var positionSpan = runBuffer.GetPositionSpan();
_font = new SKFont
{
Subpixel = true,
Edging = SKFontEdging.SubpixelAntialias,
Hinting = SKFontHinting.Full,
LinearMetrics = true,
Size = fontRenderingEmSize,
Typeface = glyphTypefaceImpl.Typeface,
Embolden = glyphTypefaceImpl.IsFakeBold,
SkewX = glyphTypefaceImpl.IsFakeItalic ? -0.2f : 0
};
}
var currentX = 0.0;
public abstract Span<ushort> GlyphIndices { get; }
for (int i = 0; i < glyphOffsets.Count; i++)
{
var offset = glyphOffsets[i];
public IGlyphRunImpl Build()
{
return new GlyphRunImpl(_builder.Build());
}
}
glyphSpan[i] = glyphIndices[i];
private sealed class SKGlyphRunBuffer : SKGlyphRunBufferBase
{
private readonly SKRunBuffer _buffer;
positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y);
public SKGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length)
{
_buffer = _builder.AllocateRun(_font, length, 0, 0);
currentX += glyphAdvances[i];
}
}
else
{
if(glyphAdvances != null)
{
var runBuffer = builder.AllocateHorizontalRun(font, count, 0);
public override Span<ushort> GlyphIndices => _buffer.GetGlyphSpan();
}
var glyphSpan = runBuffer.GetGlyphSpan();
var positionSpan = runBuffer.GetPositionSpan();
private sealed class SKHorizontalGlyphRunBuffer : SKGlyphRunBufferBase, IHorizontalGlyphRunBuffer
{
private readonly SKHorizontalRunBuffer _buffer;
var currentX = 0.0;
public SKHorizontalGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length)
{
_buffer = _builder.AllocateHorizontalRun(_font, length, 0);
}
for (int i = 0; i < glyphOffsets.Count; i++)
{
glyphSpan[i] = glyphIndices[i];
public override Span<ushort> GlyphIndices => _buffer.GetGlyphSpan();
positionSpan[i] = (float)currentX;
public Span<float> GlyphPositions => _buffer.GetPositionSpan();
}
currentX += glyphAdvances[i];
}
}
else
{
var runBuffer = builder.AllocateRun(font, count, 0, 0);
private sealed class SKPositionedGlyphRunBuffer : SKGlyphRunBufferBase, IPositionedGlyphRunBuffer
{
private readonly SKPositionedRunBuffer _buffer;
var glyphSpan = runBuffer.GetGlyphSpan();
public SKPositionedGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length)
{
_buffer = _builder.AllocatePositionedRun(_font, length);
for (int i = 0; i < glyphOffsets.Count; i++)
{
glyphSpan[i] = glyphIndices[i];
}
}
}
public override Span<ushort> GlyphIndices => _buffer.GetGlyphSpan();
public Span<PointF> GlyphPositions => MemoryMarshal.Cast<SKPoint, PointF>(_buffer.GetPositionSpan());
return new GlyphRunImpl(builder.Build());
}
}
}

4
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -60,11 +60,11 @@ namespace Avalonia.Skia
var glyphCluster = (int)(sourceInfo.Cluster);
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing;
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if(glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
if(text.Buffer.Span[glyphCluster] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');

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

@ -10,10 +10,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using GlyphRun = Avalonia.Media.GlyphRun;
using TextAlignment = Avalonia.Media.TextAlignment;
using SharpDX.Mathematics.Interop;
using System.Runtime.InteropServices;
using System.Drawing;
namespace Avalonia
{
@ -160,6 +157,72 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{
var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
var glyphCount = glyphIndices.Count;
var run = new SharpDX.DirectWrite.GlyphRun
{
FontFace = glyphTypefaceImpl.FontFace,
FontSize = (float)fontRenderingEmSize
};
var indices = new short[glyphCount];
for (var i = 0; i < glyphCount; i++)
{
indices[i] = (short)glyphIndices[i];
}
run.Indices = indices;
run.Advances = new float[glyphCount];
var scale = (float)(fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight);
if (glyphAdvances == null)
{
for (var i = 0; i < glyphCount; i++)
{
var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[i]) * scale;
run.Advances[i] = advance;
}
}
else
{
for (var i = 0; i < glyphCount; i++)
{
var advance = (float)glyphAdvances[i];
run.Advances[i] = advance;
}
}
if (glyphOffsets == null)
{
return new GlyphRunImpl(run);
}
run.Offsets = new GlyphOffset[glyphCount];
for (var i = 0; i < glyphCount; i++)
{
var (x, y) = glyphOffsets[i];
run.Offsets[i] = new GlyphOffset
{
AdvanceOffset = (float)x,
AscenderOffset = (float)y
};
}
return new GlyphRunImpl(run);
}
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{
if (glyphRun.GlyphTypeface is not GlyphTypefaceImpl glyphTypeface)
@ -260,68 +323,6 @@ namespace Avalonia.Direct2D1
return new WicBitmapImpl(format, alphaFormat, data, size, dpi, stride);
}
private class DWGlyphRunBuffer : IGlyphRunBuffer
{
protected readonly SharpDX.DirectWrite.GlyphRun _dwRun;
public DWGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
_dwRun = new SharpDX.DirectWrite.GlyphRun
{
FontFace = glyphTypefaceImpl.FontFace,
FontSize = fontRenderingEmSize,
Indices = new short[length]
};
}
public Span<ushort> GlyphIndices => MemoryMarshal.Cast<short, ushort>(_dwRun.Indices.AsSpan());
public IGlyphRunImpl Build()
{
return new GlyphRunImpl(_dwRun);
}
}
private class DWHorizontalGlyphRunBuffer : DWGlyphRunBuffer, IHorizontalGlyphRunBuffer
{
public DWHorizontalGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
: base(glyphTypeface, fontRenderingEmSize, length)
{
_dwRun.Advances = new float[length];
}
public Span<float> GlyphPositions => _dwRun.Advances.AsSpan();
}
private class DWPositionedGlyphRunBuffer : DWGlyphRunBuffer, IPositionedGlyphRunBuffer
{
public DWPositionedGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
: base(glyphTypeface, fontRenderingEmSize, length)
{
_dwRun.Advances = new float[length];
_dwRun.Offsets = new GlyphOffset[length];
}
public Span<PointF> GlyphPositions => MemoryMarshal.Cast<GlyphOffset, PointF>(_dwRun.Offsets.AsSpan());
}
public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
return new DWGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
}
public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
return new DWHorizontalGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
}
public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
return new DWPositionedGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
}
public bool SupportsIndividualRoundRects => false;
public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul;

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

@ -64,7 +64,7 @@ namespace Avalonia.Direct2D1.Media
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if (glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
if (text.Buffer.Span[glyphCluster] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');

17
tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs

@ -72,7 +72,7 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{
throw new NotImplementedException();
}
@ -126,21 +126,6 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException();
}
public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
throw new NotImplementedException();
}
public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
throw new NotImplementedException();
}
public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
throw new NotImplementedException();
}
class MockStreamGeometry : IStreamGeometryImpl
{
private MockStreamGeometryContext _impl = new MockStreamGeometryContext();

15
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -5,6 +5,7 @@ using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Avalonia.Media.Imaging;
using Microsoft.Diagnostics.Runtime;
namespace Avalonia.Benchmarks
{
@ -117,19 +118,9 @@ namespace Avalonia.Benchmarks
return new MockStreamGeometryImpl();
}
public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{
throw new NotImplementedException();
}
public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
throw new NotImplementedException();
}
public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
throw new NotImplementedException();
return new MockGlyphRun();
}
public bool SupportsIndividualRoundRects => true;

132
tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs

@ -1,132 +0,0 @@
using Avalonia.Controls.Documents;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class RichTextBlockTests
{
[Fact]
public void Changing_InlinesCollection_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new RichTextBlock();
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
target.Inlines.Add(new Run("Hello"));
Assert.False(target.IsMeasureValid);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
}
}
[Fact]
public void Changing_Inlines_Properties_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new RichTextBlock();
var inline = new Run("Hello");
target.Inlines.Add(inline);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
inline.Foreground = Brushes.Green;
Assert.False(target.IsMeasureValid);
}
}
[Fact]
public void Changing_Inlines_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new RichTextBlock();
var inlines = new InlineCollection { new Run("Hello") };
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
target.Inlines = inlines;
Assert.False(target.IsMeasureValid);
}
}
[Fact]
public void Changing_Inlines_Should_Reset_Inlines_Parent()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new RichTextBlock();
var run = new Run("Hello");
target.Inlines.Add(run);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
target.Inlines = null;
Assert.Null(run.Parent);
target.Inlines = new InlineCollection { run };
Assert.Equal(target, run.Parent);
}
}
[Fact]
public void InlineUIContainer_Child_Schould_Be_Arranged()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var target = new RichTextBlock();
var button = new Button { Content = "12345678" };
button.Template = new FuncControlTemplate<Button>((parent, scope) =>
new TextBlock
{
Name = "PART_ContentPresenter",
[!TextBlock.TextProperty] = parent[!ContentControl.ContentProperty],
}.RegisterInNameScope(scope)
);
target.Inlines!.Add("123456");
target.Inlines.Add(new InlineUIContainer(button));
target.Inlines.Add("123456");
target.Measure(Size.Infinity);
Assert.True(button.IsMeasureValid);
Assert.Equal(80, button.DesiredSize.Width);
target.Arrange(new Rect(new Size(200, 50)));
Assert.True(button.IsArrangeValid);
Assert.Equal(60, button.Bounds.Left);
}
}
}
}

121
tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Rendering;
@ -60,5 +61,125 @@ namespace Avalonia.Controls.UnitTests
renderer.Verify(x => x.AddDirty(target), Times.Once);
}
[Fact]
public void Changing_InlinesCollection_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new TextBlock();
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
target.Inlines.Add(new Run("Hello"));
Assert.False(target.IsMeasureValid);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
}
}
[Fact]
public void Changing_Inlines_Properties_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new TextBlock();
var inline = new Run("Hello");
target.Inlines.Add(inline);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
inline.Foreground = Brushes.Green;
Assert.False(target.IsMeasureValid);
}
}
[Fact]
public void Changing_Inlines_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new TextBlock();
var inlines = new InlineCollection { new Run("Hello") };
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
target.Inlines = inlines;
Assert.False(target.IsMeasureValid);
}
}
[Fact]
public void Changing_Inlines_Should_Reset_Inlines_Parent()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new TextBlock();
var run = new Run("Hello");
target.Inlines.Add(run);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
target.Inlines = null;
Assert.Null(run.Parent);
target.Inlines = new InlineCollection { run };
Assert.Equal(target, run.Parent);
}
}
[Fact]
public void InlineUIContainer_Child_Schould_Be_Arranged()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var target = new TextBlock();
var button = new Button { Content = "12345678" };
button.Template = new FuncControlTemplate<Button>((parent, scope) =>
new TextBlock
{
Name = "PART_ContentPresenter",
[!TextBlock.TextProperty] = parent[!ContentControl.ContentProperty],
}.RegisterInNameScope(scope)
);
target.Inlines!.Add("123456");
target.Inlines.Add(new InlineUIContainer(button));
target.Inlines.Add("123456");
target.Measure(Size.Infinity);
Assert.True(button.IsMeasureValid);
Assert.Equal(80, button.DesiredSize.Width);
target.Arrange(new Rect(new Size(200, 50)));
Assert.True(button.IsArrangeValid);
Assert.Equal(60, button.Bounds.Left);
}
}
}
}

2
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -425,7 +425,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties = new GenericTextParagraphProperties(flowDirection, textAlignment, true, true,
defaultProperties, TextWrapping.NoWrap, 0, 0);
defaultProperties, TextWrapping.NoWrap, 0, 0, 0);
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();

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

@ -878,7 +878,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, 0, 200,
new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0));
new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left,
true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
var textBounds = textLine.GetTextBounds(0, 3);
@ -924,7 +925,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, 0, 200,
new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0));
new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left,
true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
var textBounds = textLine.GetTextBounds(0, 4);

12
tests/Avalonia.UnitTests/MockGlyphRun.cs

@ -0,0 +1,12 @@
using Avalonia.Platform;
namespace Avalonia.UnitTests
{
public class MockGlyphRun : IGlyphRunImpl
{
public void Dispose()
{
}
}
}

2
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -142,7 +142,7 @@ namespace Avalonia.UnitTests
throw new NotImplementedException();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{
return Mock.Of<IGlyphRunImpl>();
}

Loading…
Cancel
Save