Browse Source

Merge branch 'master' into features/NetAnalyzers/CA1822

pull/9189/head
Giuseppe Lippolis 4 years ago
parent
commit
203837096e
  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. 11
      native/Avalonia.Native/src/OSX/app.mm
  6. 2
      native/Avalonia.Native/src/OSX/common.h
  7. 41
      native/Avalonia.Native/src/OSX/main.mm
  8. 2
      nukebuild/_build.csproj
  9. 2
      readme.md
  10. 21
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  11. 1
      samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs
  12. 17
      samples/ControlCatalog/Pages/ExpanderPage.xaml
  13. 4
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  14. 2
      src/Avalonia.Base/Avalonia.Base.csproj
  15. 6
      src/Avalonia.Base/Collections/AvaloniaDictionary.cs
  16. 16
      src/Avalonia.Base/Controls/NameScopeEventArgs.cs
  17. 2
      src/Avalonia.Base/Input/DragEventArgs.cs
  18. 5
      src/Avalonia.Base/Input/GotFocusEventArgs.cs
  19. 5
      src/Avalonia.Base/Input/KeyEventArgs.cs
  20. 2
      src/Avalonia.Base/Input/PointerDeltaEventArgs.cs
  21. 12
      src/Avalonia.Base/Input/PointerEventArgs.cs
  22. 2
      src/Avalonia.Base/Input/PointerWheelEventArgs.cs
  23. 6
      src/Avalonia.Base/Input/ScrollGestureEventArgs.cs
  24. 2
      src/Avalonia.Base/Input/TappedEventArgs.cs
  25. 4
      src/Avalonia.Base/Input/TextInputEventArgs.cs
  26. 5
      src/Avalonia.Base/Input/VectorEventArgs.cs
  27. 30
      src/Avalonia.Base/Interactivity/RoutedEventArgs.cs
  28. 2
      src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs
  29. 27
      src/Avalonia.Base/Media/FontSimulations.cs
  30. 3
      src/Avalonia.Base/Media/FormattedText.cs
  31. 24
      src/Avalonia.Base/Media/GlyphMetrics.cs
  32. 77
      src/Avalonia.Base/Media/GlyphRun.cs
  33. 15
      src/Avalonia.Base/Media/IGlyphTypeface.cs
  34. 18
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  35. 14
      src/Avalonia.Base/Media/Imaging/IBitmap.cs
  36. 12
      src/Avalonia.Base/Media/PathMarkupParser.cs
  37. 19
      src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
  38. 36
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  39. 21
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  40. 9
      src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
  41. 9
      src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
  42. 14
      src/Avalonia.Base/Platform/IBitmapImpl.cs
  43. 37
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  44. 2
      src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs
  45. 6
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  46. 28
      src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs
  47. 2
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
  48. 484
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  49. 486
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
  50. 41
      src/Avalonia.Controls/Control.cs
  51. 40
      src/Avalonia.Controls/Documents/InlineCollection.cs
  52. 6
      src/Avalonia.Controls/Documents/Span.cs
  53. 3
      src/Avalonia.Controls/ListBox.cs
  54. 20
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  55. 45
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  56. 374
      src/Avalonia.Controls/SelectableTextBlock.cs
  57. 83
      src/Avalonia.Controls/SizeChangedEventArgs.cs
  58. 323
      src/Avalonia.Controls/TextBlock.cs
  59. 12
      src/Avalonia.Controls/TextBox.cs
  60. 30
      src/Avalonia.Controls/TreeViewItem.cs
  61. 46
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  62. 13
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  63. 2
      src/Avalonia.MicroCom/Avalonia.MicroCom.csproj
  64. 2
      src/Avalonia.Native/Avalonia.Native.csproj
  65. 2
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  66. 8
      src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
  67. 2
      src/Avalonia.Native/avn.idl
  68. 35
      src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
  69. 36
      src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
  70. 179
      src/Avalonia.Themes.Fluent/Controls/Expander.xaml
  71. 2
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  72. 14
      src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml
  73. 18
      src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml
  74. 1
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  75. 14
      src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml
  76. 18
      src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml
  77. 2
      src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
  78. 4
      src/Avalonia.Themes.Simple/Controls/TextBox.xaml
  79. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  80. 34
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTargetTypeMetadataTransformer.cs
  81. 25
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs
  82. 5
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  83. 12
      src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs
  84. 2
      src/Shared/StringCompatibilityExtensions.cs
  85. 16
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  86. 28
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  87. 4
      src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs
  88. 33
      src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
  89. 4
      src/Skia/Avalonia.Skia/ImmutableBitmap.cs
  90. 126
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  91. 8
      src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
  92. 4
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  93. 8
      src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
  94. 1
      src/Web/Avalonia.Web/Avalonia.Web.csproj
  95. 4
      src/Web/Avalonia.Web/Avalonia.Web.props
  96. 30
      src/Web/Avalonia.Web/Avalonia.Web.targets
  97. 76
      src/Web/Avalonia.Web/AvaloniaView.cs
  98. 11
      src/Web/Avalonia.Web/BrowserTopLevelImpl.cs
  99. 2
      src/Web/Avalonia.Web/Interop/CanvasHelper.cs
  100. 6
      src/Web/Avalonia.Web/Interop/InputHelper.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

11
native/Avalonia.Native/src/OSX/app.mm

@ -95,11 +95,14 @@ ComPtr<IAvnApplicationEvents> _events;
}
@end
extern void InitializeAvnApp(IAvnApplicationEvents* events)
extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDelegate)
{
NSApplication* app = [AvnApplication sharedApplication];
id delegate = [[AvnAppDelegate alloc] initWithEvents:events];
[app setDelegate:delegate];
if(!disableAppDelegate)
{
NSApplication* app = [AvnApplication sharedApplication];
id delegate = [[AvnAppDelegate alloc] initWithEvents:events];
[app setDelegate:delegate];
}
}
HRESULT AvnApplicationCommands::HideApp()

2
native/Avalonia.Native/src/OSX/common.h

@ -32,7 +32,7 @@ extern void SetServicesMenu (IAvnMenu* menu);
extern IAvnMenu* GetAppMenu ();
extern NSMenuItem* GetAppMenuItem ();
extern void InitializeAvnApp(IAvnApplicationEvents* events);
extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDelegate);
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
extern NSPoint ToNSPoint (AvnPoint p);
extern NSRect ToNSRect (AvnRect r);

41
native/Avalonia.Native/src/OSX/main.mm

@ -3,6 +3,8 @@
#include "common.h"
static NSString* s_appTitle = @"Avalonia";
static int disableSetProcessName = 0;
static bool disableAppDelegate = false;
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
@ -111,11 +113,17 @@ public:
@autoreleasepool
{
auto appTitle = [NSString stringWithUTF8String: utf8String];
[[NSProcessInfo processInfo] setProcessName:appTitle];
SetProcessName(appTitle);
if (disableSetProcessName == 0)
{
[[NSProcessInfo processInfo] setProcessName:appTitle];
SetProcessName(appTitle);
}
if (disableSetProcessName == 1)
{
auto rootMenu = [NSApp mainMenu];
[rootMenu setTitle:appTitle];
}
return S_OK;
}
@ -133,6 +141,27 @@ public:
}
}
virtual HRESULT SetDisableSetProcessName(int disable) override
{
START_COM_CALL;
@autoreleasepool
{
disableSetProcessName = disable;
return S_OK;
}
}
virtual HRESULT SetDisableAppDelegate(int disable) override
{
START_COM_CALL;
@autoreleasepool {
disableAppDelegate = disable;
return S_OK;
}
}
};
/// See "Using POSIX Threads in a Cocoa Application" section here:
@ -175,7 +204,7 @@ public:
@autoreleasepool{
[[ThreadingInitializer new] do];
}
InitializeAvnApp(events);
InitializeAvnApp(events, disableAppDelegate);
return S_OK;
};

2
nukebuild/_build.csproj

@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="6.2.1" />
<PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " />
<PackageReference Include="MicroCom.CodeGenerator" Version="0.10.4" />
<PackageReference Include="MicroCom.CodeGenerator" Version="0.11.0" />
<!-- Keep in sync with Avalonia.Build.Tasks -->
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
<PackageReference Include="SourceLink" Version="1.1.0" GeneratePathProperty="true" />

2
readme.md

@ -104,7 +104,7 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
## Commercial Support
We have a range of [support plans available](https://avaloniaui.net/support.html) for those looking to partner with the creators of Avalonia, enabling access to the best support at every step of the development process.
We have a range of [support plans available](https://avaloniaui.net/support) for those looking to partner with the creators of Avalonia, enabling access to the best support at every step of the development process.
*Please note that donations are not considered payment for commercial support agreements. Please contact us to discuss your needs first. [team@avaloniaui.net](mailto://team@avaloniaui.net)*
## .NET Foundation

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);

17
samples/ControlCatalog/Pages/ExpanderPage.xaml

@ -32,6 +32,23 @@
<TextBlock>Expanded content</TextBlock>
</StackPanel>
</Expander>
<Expander ExpandDirection="Down"
CornerRadius="{Binding CornerRadius}">
<Expander.Header>
<Button Content="Control in Header" />
</Expander.Header>
<StackPanel>
<TextBlock>Expanded content</TextBlock>
</StackPanel>
</Expander>
<Expander Header="Disabled"
IsEnabled="False"
ExpandDirection="Down"
CornerRadius="{Binding CornerRadius}">
<StackPanel>
<TextBlock>Expanded content</TextBlock>
</StackPanel>
</Expander>
<CheckBox IsChecked="{Binding Rounded}">Rounded</CheckBox>
</StackPanel>
</StackPanel>

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)

16
src/Avalonia.Base/Controls/NameScopeEventArgs.cs

@ -1,16 +0,0 @@
using System;
namespace Avalonia.Controls
{
public class NameScopeEventArgs : EventArgs
{
public NameScopeEventArgs(string name, object element)
{
Name = name;
Element = element;
}
public string Name { get; }
public object Element { get; }
}
}

2
src/Avalonia.Base/Input/DragEventArgs.cs

@ -32,7 +32,7 @@ namespace Avalonia.Input
return point;
}
public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
internal DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
: base(routedEvent)
{
Data = data;

5
src/Avalonia.Base/Input/GotFocusEventArgs.cs

@ -7,6 +7,11 @@ namespace Avalonia.Input
/// </summary>
public class GotFocusEventArgs : RoutedEventArgs
{
internal GotFocusEventArgs()
{
}
/// <summary>
/// Gets or sets a value indicating how the change in focus occurred.
/// </summary>

5
src/Avalonia.Base/Input/KeyEventArgs.cs

@ -5,6 +5,11 @@ namespace Avalonia.Input
{
public class KeyEventArgs : RoutedEventArgs
{
internal KeyEventArgs()
{
}
public IKeyboardDevice? Device { get; set; }
public Key Key { get; set; }

2
src/Avalonia.Base/Input/PointerDeltaEventArgs.cs

@ -7,7 +7,7 @@ namespace Avalonia.Input
{
public Vector Delta { get; set; }
public PointerDeltaEventArgs(RoutedEvent routedEvent, IInteractive? source,
internal PointerDeltaEventArgs(RoutedEvent routedEvent, IInteractive? source,
IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(routedEvent, source, pointer, rootVisual, rootVisualPosition,

12
src/Avalonia.Base/Input/PointerEventArgs.cs

@ -13,7 +13,7 @@ namespace Avalonia.Input
private readonly PointerPointProperties _properties;
private readonly Lazy<IReadOnlyList<RawPointerPoint>?>? _previousPoints;
public PointerEventArgs(RoutedEvent routedEvent,
internal PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source,
IPointer pointer,
IVisual? rootVisual, Point rootVisualPosition,
@ -30,8 +30,8 @@ namespace Avalonia.Input
Timestamp = timestamp;
KeyModifiers = modifiers;
}
public PointerEventArgs(RoutedEvent routedEvent,
internal PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source,
IPointer pointer,
IVisual? rootVisual, Point rootVisualPosition,
@ -124,7 +124,7 @@ namespace Avalonia.Input
public class PointerPressedEventArgs : PointerEventArgs
{
public PointerPressedEventArgs(
internal PointerPressedEventArgs(
IInteractive source,
IPointer pointer,
IVisual rootVisual, Point rootVisualPosition,
@ -143,7 +143,7 @@ namespace Avalonia.Input
public class PointerReleasedEventArgs : PointerEventArgs
{
public PointerReleasedEventArgs(
internal PointerReleasedEventArgs(
IInteractive source, IPointer pointer,
IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers,
@ -164,7 +164,7 @@ namespace Avalonia.Input
{
public IPointer Pointer { get; }
public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent)
internal PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent)
{
Pointer = pointer;
Source = source;

2
src/Avalonia.Base/Input/PointerWheelEventArgs.cs

@ -7,7 +7,7 @@ namespace Avalonia.Input
{
public Vector Delta { get; set; }
public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual,
internal PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual,
Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition,

6
src/Avalonia.Base/Input/ScrollGestureEventArgs.cs

@ -9,8 +9,8 @@ namespace Avalonia.Input
private static int _nextId = 1;
public static int GetNextFreeId() => _nextId++;
public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent)
internal ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent)
{
Id = id;
Delta = delta;
@ -21,7 +21,7 @@ namespace Avalonia.Input
{
public int Id { get; }
public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
internal ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
{
Id = id;
}

2
src/Avalonia.Base/Input/TappedEventArgs.cs

@ -7,7 +7,7 @@ namespace Avalonia.Input
{
private readonly PointerEventArgs lastPointerEventArgs;
public TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs)
internal TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs)
: base(routedEvent)
{
this.lastPointerEventArgs = lastPointerEventArgs;

4
src/Avalonia.Base/Input/TextInputEventArgs.cs

@ -4,6 +4,10 @@ namespace Avalonia.Input
{
public class TextInputEventArgs : RoutedEventArgs
{
internal TextInputEventArgs()
{
}
public IKeyboardDevice? Device { get; set; }
public string? Text { get; set; }

5
src/Avalonia.Base/Input/VectorEventArgs.cs

@ -5,6 +5,11 @@ namespace Avalonia.Input
{
public class VectorEventArgs : RoutedEventArgs
{
internal VectorEventArgs()
{
}
public Vector Vector { get; set; }
}
}

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; }
}
}

2
src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs

@ -7,7 +7,7 @@ namespace Avalonia.Layout
/// </summary>
public class EffectiveViewportChangedEventArgs : EventArgs
{
public EffectiveViewportChangedEventArgs(Rect effectiveViewport)
internal EffectiveViewportChangedEventArgs(Rect effectiveViewport)
{
EffectiveViewport = effectiveViewport;
}

27
src/Avalonia.Base/Media/FontSimulations.cs

@ -0,0 +1,27 @@
using System;
namespace Avalonia.Media
{
/// <summary>
/// Specifies algorithmic style simulations to be applied to the typeface.
/// Bold and oblique simulations can be combined via bitwise OR operation.
/// </summary>
[Flags]
public enum FontSimulations : byte
{
/// <summary>
/// No simulations are performed.
/// </summary>
None = 0x0000,
/// <summary>
/// Algorithmic emboldening is performed.
/// </summary>
Bold = 0x0001,
/// <summary>
/// Algorithmic italicization is performed.
/// </summary>
Oblique = 0x0002
}
}

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();

24
src/Avalonia.Base/Media/GlyphMetrics.cs

@ -0,0 +1,24 @@
namespace Avalonia.Media;
public readonly struct GlyphMetrics
{
/// <summary>
/// Distance from the x-origin to the left extremum of the glyph.
/// </summary>
public int XBearing { get; init; }
/// <summary>
/// Distance from the top extremum of the glyph to the y-origin.
/// </summary>
public int YBearing{ get; init; }
/// <summary>
/// Distance from the left extremum of the glyph to the right extremum.
/// </summary>
public int Width{ get; init; }
/// <summary>
/// Distance from the top extremum of the glyph to the bottom extremum.
/// </summary>
public int Height{ get; init; }
}

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()

15
src/Avalonia.Base/Media/IGlyphTypeface.cs

@ -19,6 +19,21 @@ namespace Avalonia.Media
/// </returns>
FontMetrics Metrics { get; }
/// <summary>
/// Gets the algorithmic style simulations applied to this glyph typeface.
/// </summary>
FontSimulations FontSimulations { get; }
/// <summary>
/// Tries to get a glyph's metrics in em units.
/// </summary>
/// <param name="glyph">The glyph id.</param>
/// <param name="metrics">The glyph metrics.</param>
/// <returns>
/// <c>true</c> if an glyph's metrics was found, <c>false</c> otherwise.
/// </returns>
bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics);
/// <summary>
/// Returns an glyph index for the specified codepoint.
/// </summary>

18
src/Avalonia.Base/Media/Imaging/Bitmap.cs

@ -121,18 +121,28 @@ namespace Avalonia.Media.Imaging
/// Saves the bitmap to a file.
/// </summary>
/// <param name="fileName">The filename.</param>
public void Save(string fileName)
/// <param name="quality">
/// The optional quality for compression.
/// The quality value is interpreted from 0 - 100. If quality is null the default quality
/// setting is applied.
/// </param>
public void Save(string fileName, int? quality = null)
{
PlatformImpl.Item.Save(fileName);
PlatformImpl.Item.Save(fileName, quality);
}
/// <summary>
/// Saves the bitmap to a stream.
/// </summary>
/// <param name="stream">The stream.</param>
public void Save(Stream stream)
/// <param name="quality">
/// The optional quality for compression.
/// The quality value is interpreted from 0 - 100. If quality is null the default quality
/// setting is applied.
/// </param>
public void Save(Stream stream, int? quality = null)
{
PlatformImpl.Item.Save(stream);
PlatformImpl.Item.Save(stream, quality);
}
/// <inheritdoc/>

14
src/Avalonia.Base/Media/Imaging/IBitmap.cs

@ -35,12 +35,22 @@ namespace Avalonia.Media.Imaging
/// Saves the bitmap to a file.
/// </summary>
/// <param name="fileName">The filename.</param>
void Save(string fileName);
/// <param name="quality">
/// The optional quality for compression if supported by the specific backend.
/// The quality value is interpreted from 0 - 100. If quality is null the default quality
/// setting of the backend is applied.
/// </param>
void Save(string fileName, int? quality = null);
/// <summary>
/// Saves the bitmap to a stream in png format.
/// </summary>
/// <param name="stream">The stream.</param>
void Save(Stream stream);
/// <param name="quality">
/// The optional quality for compression if supported by the specific backend.
/// The quality value is interpreted from 0 - 100. If quality is null the default quality
/// setting of the backend is applied.
/// </param>
void Save(Stream stream, int? quality = null);
}
}

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>

36
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));
@ -477,32 +478,35 @@ namespace Avalonia.Media.TextFormatting
{
case ShapedTextCharacters shapedTextCharacters:
{
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
var lastCluster = firstCluster;
for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
if(shapedTextCharacters.ShapedBuffer.Length > 0)
{
var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
var lastCluster = firstCluster;
if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
{
measuredLength += Math.Max(0, lastCluster - firstCluster);
var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
goto found;
}
if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
{
measuredLength += Math.Max(0, lastCluster - firstCluster);
lastCluster = glyphInfo.GlyphCluster;
currentWidth += glyphInfo.GlyphAdvance;
}
goto found;
}
measuredLength += currentRun.TextSourceLength;
lastCluster = glyphInfo.GlyphCluster;
currentWidth += glyphInfo.GlyphAdvance;
}
measuredLength += currentRun.TextSourceLength;
}
break;
}
case { } drawableTextRun:
{
if (currentWidth + drawableTextRun.Size.Width > paragraphWidth)
if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
{
goto found;
}
@ -665,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; }
}
}

14
src/Avalonia.Base/Platform/IBitmapImpl.cs

@ -29,12 +29,22 @@ namespace Avalonia.Platform
/// Saves the bitmap to a file.
/// </summary>
/// <param name="fileName">The filename.</param>
void Save(string fileName);
/// <param name="quality">
/// The optional quality for compression if supported by the specific backend.
/// The quality value is interpreted from 0 - 100. If quality is null the default quality
/// setting of the backend is applied.
/// </param>
void Save(string fileName, int? quality = null);
/// <summary>
/// Saves the bitmap to a stream in png format.
/// </summary>
/// <param name="stream">The stream.</param>
void Save(Stream stream);
/// <param name="quality">
/// The optional quality for compression if supported by the specific backend.
/// The quality value is interpreted from 0 - 100. If quality is null the default quality
/// setting of the backend is applied.
/// </param>
void Save(Stream stream, int? quality = null);
}
}

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.

2
src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs

@ -12,7 +12,7 @@ namespace Avalonia.Rendering
/// </summary>
/// <param name="root">The render root that has been updated.</param>
/// <param name="dirtyRect">The updated area.</param>
public SceneInvalidatedEventArgs(
internal SceneInvalidatedEventArgs(
IRenderRoot root,
Rect dirtyRect)
{

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();
}

3
src/Avalonia.Controls/ListBox.cs

@ -139,7 +139,8 @@ namespace Avalonia.Controls
e.Source,
true,
e.KeyModifiers.HasAllFlags(KeyModifiers.Shift),
e.KeyModifiers.HasAllFlags(KeyModifiers.Control));
e.KeyModifiers.HasAllFlags(KeyModifiers.Control),
fromFocus: true);
}
}

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):

45
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -586,6 +586,14 @@ namespace Avalonia.Controls.Primitives
Selection.SelectAll();
e.Handled = true;
}
else if (e.Key == Key.Space || e.Key == Key.Enter)
{
e.Handled = UpdateSelectionFromEventSource(
e.Source,
true,
e.KeyModifiers.HasFlag(KeyModifiers.Shift),
e.KeyModifiers.HasFlag(KeyModifiers.Control));
}
}
}
@ -662,12 +670,14 @@ namespace Avalonia.Controls.Primitives
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
/// <param name="fromFocus">Wheter the event is a focus event</param>
protected void UpdateSelection(
int index,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false,
bool rightButton = false)
bool rightButton = false,
bool fromFocus = false)
{
if (index < 0 || index >= ItemCount)
{
@ -696,22 +706,25 @@ namespace Avalonia.Controls.Primitives
Selection.Clear();
Selection.SelectRange(Selection.AnchorIndex, index);
}
else if (multi && toggle)
else if (!fromFocus && toggle)
{
if (Selection.IsSelected(index) == true)
if (multi)
{
Selection.Deselect(index);
if (Selection.IsSelected(index) == true)
{
Selection.Deselect(index);
}
else
{
Selection.Select(index);
}
}
else
{
Selection.Select(index);
SelectedIndex = (SelectedIndex == index) ? -1 : index;
}
}
else if (toggle)
{
SelectedIndex = (SelectedIndex == index) ? -1 : index;
}
else
else if (!toggle)
{
using var operation = Selection.BatchUpdate();
Selection.Clear();
@ -735,18 +748,20 @@ namespace Avalonia.Controls.Primitives
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
/// <param name="fromFocus">Wheter the event is a focus event</param>
protected void UpdateSelection(
IControl container,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false,
bool rightButton = false)
bool rightButton = false,
bool fromFocus = false)
{
var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1;
if (index != -1)
{
UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton);
UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton, fromFocus);
}
}
@ -759,6 +774,7 @@ namespace Avalonia.Controls.Primitives
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
/// <param name="fromFocus">Wheter the event is a focus event</param>
/// <returns>
/// True if the event originated from a container that belongs to the control; otherwise
/// false.
@ -768,13 +784,14 @@ namespace Avalonia.Controls.Primitives
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false,
bool rightButton = false)
bool rightButton = false,
bool fromFocus = false)
{
var container = GetContainerFromEventSource(eventSource);
if (container != null)
{
UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton);
UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton, fromFocus);
return true;
}

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

@ -113,6 +113,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));
@ -377,6 +383,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>

30
src/Avalonia.Controls/TreeViewItem.cs

@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Threading;
namespace Avalonia.Controls
{
@ -45,6 +46,8 @@ namespace Avalonia.Controls
private IControl? _header;
private bool _isExpanded;
private int _level;
private bool _templateApplied;
private bool _deferredBringIntoViewFlag;
/// <summary>
/// Initializes static members of the <see cref="TreeViewItem"/> class.
@ -136,15 +139,24 @@ namespace Avalonia.Controls
protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)
{
if (e.TargetObject == this && _header != null)
if (e.TargetObject == this)
{
var m = _header.TransformToVisual(this);
if (!_templateApplied)
{
_deferredBringIntoViewFlag = true;
return;
}
if (m.HasValue)
if (_header != null)
{
var bounds = new Rect(_header.Bounds.Size);
var rect = bounds.TransformToAABB(m.Value);
e.TargetRect = rect;
var m = _header.TransformToVisual(this);
if (m.HasValue)
{
var bounds = new Rect(_header.Bounds.Size);
var rect = bounds.TransformToAABB(m.Value);
e.TargetRect = rect;
}
}
}
}
@ -187,6 +199,12 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_header = e.NameScope.Find<IControl>("PART_Header");
_templateApplied = true;
if (_deferredBringIntoViewFlag)
{
_deferredBringIntoViewFlag = false;
Dispatcher.UIThread.Post(this.BringIntoView); // must use the Dispatcher, otherwise the TreeView doesn't scroll
}
}
private static int CalculateDistanceFromLogicalParent<T>(ILogical? logical, int @default = -1) where T : class

46
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)
@ -353,12 +323,12 @@ namespace Avalonia.Headless
public Vector Dpi { get; }
public PixelSize PixelSize { get; }
public int Version { get; set; }
public void Save(string fileName)
public void Save(string fileName, int? quality = null)
{
}
public void Save(Stream stream)
public void Save(Stream stream, int? quality = null)
{
}

13
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -102,6 +102,8 @@ namespace Avalonia.Headless
public int GlyphCount => 1337;
public FontSimulations FontSimulations { get; }
public void Dispose()
{
}
@ -138,6 +140,17 @@ namespace Avalonia.Headless
table = null;
return false;
}
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{
metrics = new GlyphMetrics
{
Height = 10,
Width = 10
};
return true;
}
}
class HeadlessTextShaperStub : ITextShaperImpl

2
src/Avalonia.MicroCom/Avalonia.MicroCom.csproj

@ -4,7 +4,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MicroCom.Runtime" Version="0.10.5" />
<PackageReference Include="MicroCom.Runtime" Version="0.11.0" />
<ProjectReference Include="..\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<ExcludeAssets>all</ExcludeAssets>

2
src/Avalonia.Native/Avalonia.Native.csproj

@ -24,7 +24,7 @@
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<PackageReference Include="MicroCom.CodeGenerator.MSBuild" Version="0.10.4" PrivateAssets="all" />
<PackageReference Include="MicroCom.CodeGenerator.MSBuild" Version="0.11.0" PrivateAssets="all" />
<MicroComIdl Include="avn.idl" CSharpInteropPath="Interop.Generated.cs" />
</ItemGroup>

2
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -103,6 +103,8 @@ namespace Avalonia.Native
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>() ?? new MacOSPlatformOptions();
_factory.MacOptions.SetShowInDock(macOpts.ShowInDock ? 1 : 0);
_factory.MacOptions.SetDisableSetProcessName(macOpts.DisableSetProcessName ? 1 : 0);
_factory.MacOptions.SetDisableAppDelegate(macOpts.DisableAvaloniaAppDelegate ? 1 : 0);
}
AvaloniaLocator.CurrentMutable

8
src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs

@ -83,5 +83,13 @@ namespace Avalonia
/// Gets or sets a value indicating whether the native macOS menu bar will be enabled for the application.
/// </summary>
public bool DisableNativeMenus { get; set; }
public bool DisableSetProcessName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether Avalonia can install its own AppDelegate.
/// Disabling this can be useful in some scenarios like when running as a plugin inside an existing macOS application.
/// </summary>
public bool DisableAvaloniaAppDelegate { get; set; }
}
}

2
src/Avalonia.Native/avn.idl

@ -599,6 +599,8 @@ interface IAvnMacOptions : IUnknown
{
HRESULT SetShowInDock(int show);
HRESULT SetApplicationTitle(char* utf8string);
HRESULT SetDisableSetProcessName(int disable);
HRESULT SetDisableAppDelegate(int disable);
}
[uuid(04c1b049-1f43-418a-9159-cae627ec1367)]

35
src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml

@ -281,7 +281,6 @@
<StaticResource x:Key="CheckBoxCheckGlyphForegroundIndeterminatePointerOver" ResourceKey="SystemControlHighlightAltChromeWhiteBrush" />
<StaticResource x:Key="CheckBoxCheckGlyphForegroundIndeterminatePressed" ResourceKey="SystemControlHighlightAltChromeWhiteBrush" />
<StaticResource x:Key="CheckBoxCheckGlyphForegroundIndeterminateDisabled" ResourceKey="SystemControlHighlightAltChromeWhiteBrush" />
<!-- Resources for Calendar.xaml, CalendarButton.xaml, CalendarDayButton.xaml, CalendarItem.xaml -->
<StaticResource x:Key="CalendarViewSelectedHoverBorderBrush" ResourceKey="SystemControlHighlightListAccentMediumBrush" />
@ -305,7 +304,39 @@
<StaticResource x:Key="CalendarViewCalendarItemRevealBorderBrush" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="CalendarViewNavigationButtonBorderBrushPointerOver" ResourceKey="SystemControlHighlightTransparentBrush" />
<StaticResource x:Key="CalendarViewNavigationButtonBorderBrush" ResourceKey="SystemControlTransparentBrush" />
<!-- Resources for Expander.xaml -->
<!-- Expander:Header -->
<StaticResource x:Key="ExpanderHeaderBackground" ResourceKey="SystemAltMediumHighColor" />
<StaticResource x:Key="ExpanderHeaderBackgroundPointerOver" ResourceKey="SystemAltMediumHighColor" />
<StaticResource x:Key="ExpanderHeaderBackgroundPressed" ResourceKey="SystemAltMediumHighColor" />
<StaticResource x:Key="ExpanderHeaderBackgroundDisabled" ResourceKey="SystemAltMediumHighColor" />
<StaticResource x:Key="ExpanderHeaderForeground" ResourceKey="SystemBaseHighColor" />
<StaticResource x:Key="ExpanderHeaderForegroundPointerOver" ResourceKey="SystemBaseHighColor" />
<StaticResource x:Key="ExpanderHeaderForegroundPressed" ResourceKey="SystemBaseHighColor" />
<StaticResource x:Key="ExpanderHeaderForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ExpanderHeaderBorderBrush" ResourceKey="SystemBaseLowColor" />
<StaticResource x:Key="ExpanderHeaderBorderBrushPointerOver" ResourceKey="SystemBaseLowColor" />
<StaticResource x:Key="ExpanderHeaderBorderBrushPressed" ResourceKey="SystemBaseLowColor" />
<StaticResource x:Key="ExpanderHeaderBorderBrushDisabled" ResourceKey="SystemControlDisabledBaseLowBrush" />
<SolidColorBrush x:Key="ExpanderChevronBackground" Color="Transparent" />
<SolidColorBrush x:Key="ExpanderChevronBackgroundPointerOver" Color="{StaticResource SystemBaseHighColor}" Opacity="0.1" />
<SolidColorBrush x:Key="ExpanderChevronBackgroundPressed" Color="Transparent" />
<SolidColorBrush x:Key="ExpanderChevronBackgroundDisabled" Color="Transparent" />
<StaticResource x:Key="ExpanderChevronForeground" ResourceKey="SystemBaseHighColor" />
<StaticResource x:Key="ExpanderChevronForegroundPointerOver" ResourceKey="SystemBaseHighColor" />
<StaticResource x:Key="ExpanderChevronForegroundPressed" ResourceKey="SystemBaseHighColor" />
<StaticResource x:Key="ExpanderChevronForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<SolidColorBrush x:Key="ExpanderChevronBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="ExpanderChevronBorderBrushPointerOver" Color="Transparent" />
<SolidColorBrush x:Key="ExpanderChevronBorderBrushPressed" Color="Transparent" />
<SolidColorBrush x:Key="ExpanderChevronBorderBrushDisabled" Color="Transparent" />
<!-- Expander:Content -->
<StaticResource x:Key="ExpanderContentBackground" ResourceKey="SystemChromeMediumLowColor" />
<StaticResource x:Key="ExpanderContentBorderBrush" ResourceKey="SystemBaseLowColor" />
<!--Resources for NotificationCard.xaml -->
<SolidColorBrush x:Key="NotificationCardBackgroundBrush" Color="#444444" />
<StaticResource x:Key="NotificationCardForegroundBrush" ResourceKey="SystemControlForegroundBaseHighBrush" />

36
src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml

@ -278,7 +278,7 @@
<StaticResource x:Key="CheckBoxCheckGlyphForegroundIndeterminatePointerOver" ResourceKey="SystemControlForegroundChromeWhiteBrush" />
<StaticResource x:Key="CheckBoxCheckGlyphForegroundIndeterminatePressed" ResourceKey="SystemControlForegroundChromeWhiteBrush" />
<StaticResource x:Key="CheckBoxCheckGlyphForegroundIndeterminateDisabled" ResourceKey="SystemControlForegroundChromeWhiteBrush" />
<!-- Resources for Calendar.xaml, CalendarButton.xaml, CalendarDayButton.xaml, CalendarItem.xaml -->
<StaticResource x:Key="CalendarViewSelectedHoverBorderBrush" ResourceKey="SystemControlHighlightListAccentMediumBrush" />
<StaticResource x:Key="CalendarViewSelectedPressedBorderBrush" ResourceKey="SystemControlHighlightListAccentHighBrush" />
@ -301,7 +301,39 @@
<StaticResource x:Key="CalendarViewCalendarItemRevealBorderBrush" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="CalendarViewNavigationButtonBorderBrushPointerOver" ResourceKey="SystemControlHighlightTransparentBrush" />
<StaticResource x:Key="CalendarViewNavigationButtonBorderBrush" ResourceKey="SystemControlTransparentBrush" />
<!-- Resources for Expander.xaml -->
<!-- Expander:Header -->
<StaticResource x:Key="ExpanderHeaderBackground" ResourceKey="SystemAltMediumHighColor" />
<StaticResource x:Key="ExpanderHeaderBackgroundPointerOver" ResourceKey="SystemAltMediumHighColor" />
<StaticResource x:Key="ExpanderHeaderBackgroundPressed" ResourceKey="SystemAltMediumHighColor" />
<StaticResource x:Key="ExpanderHeaderBackgroundDisabled" ResourceKey="SystemAltMediumHighColor" />
<StaticResource x:Key="ExpanderHeaderForeground" ResourceKey="SystemBaseHighColor" />
<StaticResource x:Key="ExpanderHeaderForegroundPointerOver" ResourceKey="SystemBaseHighColor" />
<StaticResource x:Key="ExpanderHeaderForegroundPressed" ResourceKey="SystemBaseHighColor" />
<StaticResource x:Key="ExpanderHeaderForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ExpanderHeaderBorderBrush" ResourceKey="SystemBaseLowColor" />
<StaticResource x:Key="ExpanderHeaderBorderBrushPointerOver" ResourceKey="SystemBaseLowColor" />
<StaticResource x:Key="ExpanderHeaderBorderBrushPressed" ResourceKey="SystemBaseLowColor" />
<StaticResource x:Key="ExpanderHeaderBorderBrushDisabled" ResourceKey="SystemControlDisabledBaseLowBrush" />
<SolidColorBrush x:Key="ExpanderChevronBackground" Color="Transparent" />
<SolidColorBrush x:Key="ExpanderChevronBackgroundPointerOver" Color="{StaticResource SystemBaseHighColor}" Opacity="0.1" />
<SolidColorBrush x:Key="ExpanderChevronBackgroundPressed" Color="Transparent" />
<SolidColorBrush x:Key="ExpanderChevronBackgroundDisabled" Color="Transparent" />
<StaticResource x:Key="ExpanderChevronForeground" ResourceKey="SystemBaseHighColor" />
<StaticResource x:Key="ExpanderChevronForegroundPointerOver" ResourceKey="SystemBaseHighColor" />
<StaticResource x:Key="ExpanderChevronForegroundPressed" ResourceKey="SystemBaseHighColor" />
<StaticResource x:Key="ExpanderChevronForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<SolidColorBrush x:Key="ExpanderChevronBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="ExpanderChevronBorderBrushPointerOver" Color="Transparent" />
<SolidColorBrush x:Key="ExpanderChevronBorderBrushPressed" Color="Transparent" />
<SolidColorBrush x:Key="ExpanderChevronBorderBrushDisabled" Color="Transparent" />
<!-- Expander:Content -->
<StaticResource x:Key="ExpanderContentBackground" ResourceKey="SystemChromeMediumLowColor" />
<StaticResource x:Key="ExpanderContentBorderBrush" ResourceKey="SystemBaseLowColor" />
<!--Resources for NotificationCard.xaml -->
<SolidColorBrush x:Key="NotificationCardBackgroundBrush" Color="White" />
<StaticResource x:Key="NotificationCardForegroundBrush" ResourceKey="SystemControlForegroundBaseHighBrush" />

179
src/Avalonia.Themes.Fluent/Controls/Expander.xaml

@ -1,6 +1,7 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:CompileBindings="True">
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Orientation="Vertical" Spacing="20" Width="350" Height="600">
@ -44,45 +45,59 @@
</Border>
</Design.PreviewWith>
<Thickness x:Key="ExpanderHeaderPadding">16</Thickness>
<!-- Shared header/content -->
<x:Double x:Key="ExpanderMinHeight">48</x:Double>
<!-- Header -->
<HorizontalAlignment x:Key="ExpanderHeaderHorizontalContentAlignment">Stretch</HorizontalAlignment>
<VerticalAlignment x:Key="ExpanderHeaderVerticalContentAlignment">Center</VerticalAlignment>
<Thickness x:Key="ExpanderHeaderPadding">16,0,0,0</Thickness>
<Thickness x:Key="ExpanderHeaderBorderThickness">1</Thickness>
<Thickness x:Key="ExpanderChevronBorderThickness">0</Thickness>
<Thickness x:Key="ExpanderChevronMargin">20,0,8,0</Thickness>
<x:Double x:Key="ExpanderChevronButtonSize">32</x:Double>
<!-- Content -->
<Thickness x:Key="ExpanderContentPadding">16</Thickness>
<Thickness x:Key="ExpanderBorderThickness">1</Thickness>
<Thickness x:Key="ExpanderDropdownLeftBorderThickness">1,1,0,1</Thickness>
<Thickness x:Key="ExpanderDropdownUpBorderThickness">1,1,1,0</Thickness>
<Thickness x:Key="ExpanderDropdownRightBorderThickness">0,1,1,1</Thickness>
<Thickness x:Key="ExpanderDropdownDownBorderThickness">1,0,1,1</Thickness>
<SolidColorBrush x:Key="ExpanderBackground" Color="{DynamicResource SystemAltMediumHighColor}" />
<SolidColorBrush x:Key="ExpanderBorderBrush" Color="{DynamicResource SystemBaseLowColor}" />
<SolidColorBrush x:Key="ExpanderDropDownBackground" Color="{DynamicResource SystemChromeMediumLowColor}" />
<SolidColorBrush x:Key="ExpanderDropDownBorderBrush" Color="{DynamicResource SystemBaseLowColor}" />
<SolidColorBrush x:Key="ExpanderForeground" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="ExpanderChevronForeground" Color="{DynamicResource SystemBaseHighColor}" />
<Thickness x:Key="ExpanderContentLeftBorderThickness">1,1,0,1</Thickness>
<Thickness x:Key="ExpanderContentUpBorderThickness">1,1,1,0</Thickness>
<Thickness x:Key="ExpanderContentRightBorderThickness">0,1,1,1</Thickness>
<Thickness x:Key="ExpanderContentDownBorderThickness">1,0,1,1</Thickness>
<ControlTheme x:Key="FluentExpanderToggleButtonTheme" TargetType="ToggleButton">
<Setter Property="Background" Value="{DynamicResource ExpanderHeaderBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource ExpanderHeaderBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource ExpanderHeaderBorderThickness}" />
<Setter Property="Foreground" Value="{DynamicResource ExpanderHeaderForeground}" />
<Setter Property="Padding" Value="{StaticResource ExpanderHeaderPadding}" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="{StaticResource ExpanderHeaderHorizontalContentAlignment}" />
<Setter Property="VerticalContentAlignment" Value="{StaticResource ExpanderHeaderVerticalContentAlignment}" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="ToggleButtonBackground"
CornerRadius="{TemplateBinding CornerRadius}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid ColumnDefinitions="*,Auto">
<Grid x:Name="ToggleButtonGrid"
ColumnDefinitions="*,Auto">
<ContentPresenter x:Name="PART_ContentPresenter"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="Center"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Foreground="{DynamicResource ExpanderForeground}" />
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Foreground="{TemplateBinding Foreground}"
Margin="{TemplateBinding Padding}"/>
<Border x:Name="ExpandCollapseChevronBorder"
Grid.Column="1"
Width="32"
Height="32"
Margin="7"
RenderTransformOrigin="50%,50%">
Width="{DynamicResource ExpanderChevronButtonSize}"
Height="{DynamicResource ExpanderChevronButtonSize}"
Margin="{DynamicResource ExpanderChevronMargin}"
CornerRadius="{DynamicResource ControlCornerRadius}"
BorderBrush="{DynamicResource ExpanderChevronBorderBrush}"
BorderThickness="{DynamicResource ExpanderChevronBorderThickness}"
Background="{DynamicResource ExpanderChevronBackground}">
<Path x:Name="ExpandCollapseChevron"
HorizontalAlignment="Center"
VerticalAlignment="Center"
@ -98,6 +113,7 @@
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:checked /template/ Border#ExpandCollapseChevronBorder">
<Style.Animations>
<Animation FillMode="Both" Duration="0:0:0.0625">
@ -107,6 +123,7 @@
</Animation>
</Style.Animations>
</Style>
<Style Selector="^:not(:checked) /template/ Border#ExpandCollapseChevronBorder">
<Style.Animations>
<Animation FillMode="Both" Duration="0:0:0.0625">
@ -119,60 +136,118 @@
</Animation>
</Style.Animations>
</Style>
<!-- PointerOver -->
<Style Selector="^:pointerover /template/ Border#ToggleButtonBackground">
<Setter Property="Background" Value="{DynamicResource ExpanderHeaderBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource ExpanderHeaderBorderBrushPointerOver}" />
</Style>
<Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ExpanderHeaderForegroundPointerOver}" />
</Style>
<Style Selector="^:pointerover /template/ Border#ExpandCollapseChevronBorder">
<Setter Property="Background" Value="{DynamicResource ExpanderChevronBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource ExpanderChevronBorderBrushPointerOver}" />
</Style>
<Style Selector="^:pointerover /template/ Path#ExpandCollapseChevron">
<Setter Property="Stroke" Value="{DynamicResource ExpanderChevronForegroundPointerOver}" />
</Style>
<!-- Pressed -->
<Style Selector="^:pressed /template/ Border#ToggleButtonBackground">
<Setter Property="Background" Value="{DynamicResource ExpanderHeaderBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ExpanderHeaderBorderBrushPressed}" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ExpanderHeaderForegroundPressed}" />
</Style>
<Style Selector="^:pressed /template/ Border#ExpandCollapseChevronBorder">
<Setter Property="Background" Value="{DynamicResource ExpanderChevronBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ExpanderChevronBorderBrushPressed}" />
</Style>
<Style Selector="^:pressed /template/ Path#ExpandCollapseChevron">
<Setter Property="Stroke" Value="{DynamicResource ExpanderChevronForegroundPressed}" />
</Style>
<!-- Disabled -->
<Style Selector="^:disabled /template/ Border#ToggleButtonBackground">
<Setter Property="Background" Value="{DynamicResource ExpanderHeaderBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ExpanderHeaderBorderBrushDisabled}" />
</Style>
<Style Selector="^:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ExpanderHeaderForegroundDisabled}" />
</Style>
<Style Selector="^:disabled /template/ Border#ExpandCollapseChevronBorder">
<Setter Property="Background" Value="{DynamicResource ExpanderChevronBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ExpanderChevronBorderBrushDisabled}" />
</Style>
<Style Selector="^:disabled /template/ Path#ExpandCollapseChevron">
<Setter Property="Stroke" Value="{DynamicResource ExpanderChevronForegroundDisabled}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="FluentExpanderToggleButtonUpTheme" TargetType="ToggleButton" BasedOn="{StaticResource FluentExpanderToggleButtonTheme}">
<Style Selector="^ /template/ Path#ExpandCollapseChevron">
<Setter Property="Data" Value="M 0 7 L 7 0 L 14 7" />
</Style>
</ControlTheme>
<ControlTheme x:Key="FluentExpanderToggleButtonDownTheme" TargetType="ToggleButton" BasedOn="{StaticResource FluentExpanderToggleButtonTheme}">
<Style Selector="^ /template/ Path#ExpandCollapseChevron">
<Setter Property="Data" Value="M 0 0 L 7 7 L 14 0" />
</Style>
</ControlTheme>
<ControlTheme x:Key="FluentExpanderToggleButtonLeftTheme" TargetType="ToggleButton" BasedOn="{StaticResource FluentExpanderToggleButtonTheme}">
<Style Selector="^ /template/ Path#ExpandCollapseChevron">
<Setter Property="Data" Value="M 7 0 L 0 7 L 7 14" />
</Style>
</ControlTheme>
<ControlTheme x:Key="FluentExpanderToggleButtonRightTheme" TargetType="ToggleButton" BasedOn="{StaticResource FluentExpanderToggleButtonTheme}">
<Style Selector="^ /template/ Path#ExpandCollapseChevron">
<Setter Property="Data" Value="M 0 0 L 7 7 L 0 14" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type Expander}" TargetType="Expander">
<Setter Property="Background" Value="{DynamicResource ExpanderBackground}" />
<Setter Property="BorderThickness" Value="{DynamicResource ExpanderBorderThickness}" />
<Setter Property="BorderBrush" Value="{DynamicResource ExpanderBorderBrush}" />
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="MinWidth" Value="{DynamicResource FlyoutThemeMinWidth}" />
<Setter Property="MinHeight" Value="{StaticResource ExpanderMinHeight}" />
<Setter Property="Background" Value="{DynamicResource ExpanderContentBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource ExpanderContentBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource ExpanderContentDownBorderThickness}" />
<Setter Property="Padding" Value="{StaticResource ExpanderContentPadding}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="Padding" Value="{DynamicResource ExpanderHeaderPadding}" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<ControlTemplate>
<DockPanel>
<DockPanel MinWidth="{TemplateBinding MinWidth}"
MaxWidth="{TemplateBinding MaxWidth}">
<ToggleButton x:Name="ExpanderHeader"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
HorizontalContentAlignment="Stretch"
MinHeight="{TemplateBinding MinHeight}"
CornerRadius="{TemplateBinding CornerRadius}"
IsEnabled="{TemplateBinding IsEnabled}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}"
IsEnabled="{TemplateBinding IsEnabled}"/>
IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}" />
<Border x:Name="ExpanderContent"
Padding="{DynamicResource ExpanderContentPadding}"
Background="{DynamicResource ExpanderDropDownBackground}"
BorderBrush="{DynamicResource ExpanderDropDownBorderBrush}"
IsVisible="{TemplateBinding IsExpanded, Mode=TwoWay}">
IsVisible="{TemplateBinding IsExpanded, Mode=TwoWay}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
MinHeight="{TemplateBinding MinHeight}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Padding="{TemplateBinding Padding}">
<ContentPresenter x:Name="PART_ContentPresenter"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
ContentTemplate="{TemplateBinding ContentTemplate}"
Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
</DockPanel>
</ControlTemplate>
@ -241,16 +316,16 @@
</Style>
<Style Selector="^:left /template/ Border#ExpanderContent">
<Setter Property="BorderThickness" Value="{DynamicResource ExpanderDropdownLeftBorderThickness}" />
<Setter Property="BorderThickness" Value="{DynamicResource ExpanderContentLeftBorderThickness}" />
</Style>
<Style Selector="^:up /template/ Border#ExpanderContent">
<Setter Property="BorderThickness" Value="{DynamicResource ExpanderDropdownUpBorderThickness}" />
<Setter Property="BorderThickness" Value="{DynamicResource ExpanderContentUpBorderThickness}" />
</Style>
<Style Selector="^:right /template/ Border#ExpanderContent">
<Setter Property="BorderThickness" Value="{DynamicResource ExpanderDropdownRightBorderThickness}" />
<Setter Property="BorderThickness" Value="{DynamicResource ExpanderContentRightBorderThickness}" />
</Style>
<Style Selector="^:down /template/ Border#ExpanderContent">
<Setter Property="BorderThickness" Value="{DynamicResource ExpanderDropdownDownBorderThickness}" />
<Setter Property="BorderThickness" Value="{DynamicResource ExpanderContentDownBorderThickness}" />
</Style>
</ControlTheme>
</ResourceDictionary>

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>

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
using XamlX;
using XamlX.Ast;
@ -51,6 +52,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
new AvaloniaXamlIlBindingPathParser(),
new AvaloniaXamlIlPropertyPathTransformer(),
new AvaloniaXamlIlSetterTargetTypeMetadataTransformer(),
new AvaloniaXamlIlSetterTransformer(),
new AvaloniaXamlIlConstructorServiceProviderTransformer(),
new AvaloniaXamlIlTransitionsTypeMetadataTransformer(),

34
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTargetTypeMetadataTransformer.cs

@ -0,0 +1,34 @@
using System.Linq;
using XamlX;
using XamlX.Ast;
using XamlX.Transform;
using XamlX.Transform.Transformers;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
internal class AvaloniaXamlIlSetterTargetTypeMetadataTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (node is XamlAstObjectNode on
&& on.Children.FirstOrDefault(c => c is XamlAstXmlDirective
{
Namespace: XamlNamespaces.Xaml2006,
Name: "SetterTargetType"
}) is { } typeDirective)
{
var value = ((XamlAstXmlDirective)typeDirective).Values.Single();
var type = value is XamlTypeExtensionNode typeNode ? typeNode.Value
: value is XamlAstTextNode tn ? TypeReferenceResolver.ResolveType(context, tn.Text, false, tn, true)
: null;
on.Children.Remove(typeDirective);
if (type is null)
{
throw new XamlParseException("Unable to resolve SetterTargetType type", typeDirective);
}
return new AvaloniaXamlIlTargetTypeMetadataNode(on, type, AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style);
}
return node;
}
}

25
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
@ -8,7 +9,6 @@ using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
using XamlParseException = XamlX.XamlParseException;
class AvaloniaXamlIlSetterTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
@ -17,10 +17,24 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
&& on.Type.GetClrType().FullName == "Avalonia.Styling.Setter"))
return node;
var targetTypeNode = context.ParentNodes()
IXamlType targetType = null;
IXamlLineInfo lineInfo = null;
var styleParent = context.ParentNodes()
.OfType<AvaloniaXamlIlTargetTypeMetadataNode>()
.FirstOrDefault(x => x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) ??
throw new XamlParseException("Can not find parent Style Selector or ControlTemplate TargetType", node);
.FirstOrDefault(x => x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style);
if (styleParent != null)
{
targetType = styleParent.TargetType.GetClrType()
?? throw new XamlParseException("Can not find parent Style Selector or ControlTemplate TargetType. If setter is not part of the style, you can set x:SetterTargetType directive on its parent.", node);
lineInfo = on;
}
if (targetType == null)
{
throw new XamlParseException("Could not determine target type of Setter", node);
}
IXamlType propType = null;
var property = @on.Children.OfType<XamlAstXamlPropertyValueNode>()
@ -31,9 +45,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
if (propertyName == null)
throw new XamlParseException("Setter.Property must be a string", node);
var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName,
new XamlAstClrTypeReference(targetTypeNode, targetTypeNode.TargetType.GetClrType(), false), property.Values[0]);
new XamlAstClrTypeReference(lineInfo, targetType, false), property.Values[0]);
property.Values = new List<IXamlAstValueNode> {avaloniaPropertyNode};
propType = avaloniaPropertyNode.AvaloniaPropertyType;
}

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) =>

16
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -148,11 +148,19 @@ namespace Avalonia.Skia
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
}
var isFakeBold = (int)typeface.Weight >= 600 && !skTypeface.IsBold;
var fontSimulations = FontSimulations.None;
var isFakeItalic = typeface.Style == FontStyle.Italic && !skTypeface.IsItalic;
return new GlyphTypefaceImpl(skTypeface, isFakeBold, isFakeItalic);
if((int)typeface.Weight >= 600 && !skTypeface.IsBold)
{
fontSimulations |= FontSimulations.Bold;
}
if(typeface.Style == FontStyle.Italic && !skTypeface.IsItalic)
{
fontSimulations |= FontSimulations.Oblique;
}
return new GlyphTypefaceImpl(skTypeface, fontSimulations);
}
}
}

28
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -12,7 +12,7 @@ namespace Avalonia.Skia
{
private bool _isDisposed;
public GlyphTypefaceImpl(SKTypeface typeface, bool isFakeBold = false, bool isFakeItalic = false)
public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations)
{
Typeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
@ -52,9 +52,7 @@ namespace Avalonia.Skia
GlyphCount = Typeface.GlyphCount;
IsFakeBold = isFakeBold;
IsFakeItalic = isFakeItalic;
FontSimulations = fontSimulations;
}
public Face Face { get; }
@ -63,15 +61,33 @@ namespace Avalonia.Skia
public SKTypeface Typeface { get; }
public FontSimulations FontSimulations { get; }
public int ReplacementCodepoint { get; }
public FontMetrics Metrics { get; }
public int GlyphCount { get; }
public bool IsFakeBold { get; }
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{
metrics = default;
public bool IsFakeItalic { get; }
if (!Font.TryGetGlyphExtents(glyph, out var extents))
{
return false;
}
metrics = new GlyphMetrics
{
XBearing = extents.XBearing,
YBearing = extents.YBearing,
Width = extents.Width,
Height = extents.Height
};
return true;
}
/// <inheritdoc cref="IGlyphTypeface"/>
public ushort GetGlyph(uint codepoint)

4
src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs

@ -25,9 +25,9 @@ namespace Avalonia.Skia
public Vector Dpi { get; }
public PixelSize PixelSize { get; }
public int Version { get; private set; }
public void Save(string fileName) => throw new NotSupportedException();
public void Save(string fileName, int? quality = null) => throw new NotSupportedException();
public void Save(Stream stream) => throw new NotSupportedException();
public void Save(Stream stream, int? quality = null) => throw new NotSupportedException();
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
{

33
src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs

@ -14,14 +14,19 @@ namespace Avalonia.Skia.Helpers
/// </summary>
/// <param name="image">Image to save</param>
/// <param name="fileName">Target file.</param>
public static void SaveImage(SKImage image, string fileName)
/// <param name="quality">
/// The optional quality for PNG compression.
/// The quality value is interpreted from 0 - 100. If quality is null
/// the encoder applies the default quality value.
/// </param>
public static void SaveImage(SKImage image, string fileName, int? quality = null)
{
if (image == null) throw new ArgumentNullException(nameof(image));
if (fileName == null) throw new ArgumentNullException(nameof(fileName));
using (var stream = File.Create(fileName))
{
SaveImage(image, stream);
SaveImage(image, stream, quality);
}
}
@ -29,16 +34,30 @@ namespace Avalonia.Skia.Helpers
/// Save Skia image to a stream.
/// </summary>
/// <param name="image">Image to save</param>
/// <param name="stream">Target stream.</param>
public static void SaveImage(SKImage image, Stream stream)
/// <param name="quality">
/// The optional quality for PNG compression.
/// The quality value is interpreted from 0 - 100. If quality is null
/// the encoder applies the default quality value.
/// </param>
public static void SaveImage(SKImage image, Stream stream, int? quality = null)
{
if (image == null) throw new ArgumentNullException(nameof(image));
if (stream == null) throw new ArgumentNullException(nameof(stream));
using (var data = image.Encode())
if (quality == null)
{
data.SaveTo(stream);
using (var data = image.Encode())
{
data.SaveTo(stream);
}
}
else
{
using (var data = image.Encode(SKEncodedImageFormat.Png, (int)quality))
{
data.SaveTo(stream);
}
}
}
}
}
}

4
src/Skia/Avalonia.Skia/ImmutableBitmap.cs

@ -139,13 +139,13 @@ namespace Avalonia.Skia
}
/// <inheritdoc />
public void Save(string fileName)
public void Save(string fileName, int? quality = null)
{
ImageSavingHelper.SaveImage(_image, fileName);
}
/// <inheritdoc />
public void Save(Stream stream)
public void Save(Stream stream, int? quality = null)
{
ImageSavingHelper.SaveImage(_image, stream);
}

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());
}
}
}

8
src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs

@ -116,20 +116,20 @@ namespace Avalonia.Skia
public int Version { get; private set; } = 1;
/// <inheritdoc />
public void Save(string fileName)
public void Save(string fileName, int? quality = null)
{
using (var image = SnapshotImage())
{
ImageSavingHelper.SaveImage(image, fileName);
ImageSavingHelper.SaveImage(image, fileName, quality);
}
}
/// <inheritdoc />
public void Save(Stream stream)
public void Save(Stream stream, int? quality = null)
{
using (var image = SnapshotImage())
{
ImageSavingHelper.SaveImage(image, stream);
ImageSavingHelper.SaveImage(image, stream, quality);
}
}

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(' ');

8
src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs

@ -137,20 +137,20 @@ namespace Avalonia.Skia
}
/// <inheritdoc />
public void Save(Stream stream)
public void Save(Stream stream, int? quality = null)
{
using (var image = GetSnapshot())
{
ImageSavingHelper.SaveImage(image, stream);
ImageSavingHelper.SaveImage(image, stream, quality);
}
}
/// <inheritdoc />
public void Save(string fileName)
public void Save(string fileName, int? quality = null)
{
using (var image = GetSnapshot())
{
ImageSavingHelper.SaveImage(image, fileName);
ImageSavingHelper.SaveImage(image, fileName, quality);
}
}

1
src/Web/Avalonia.Web/Avalonia.Web.csproj

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Remove="@(SupportedPlatform)" />
<SupportedPlatform Include="browser" />
</ItemGroup>

4
src/Web/Avalonia.Web/Avalonia.Web.props

@ -1,5 +1,5 @@
<Project>
<Project>
<PropertyGroup>
<EmccExtraLDFlags>$(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js"</EmccExtraLDFlags>
<EmccInitialHeapSize>16384000</EmccInitialHeapSize> <!-- must be a multiple of 64KiB, 1024000 * num MB, number grows -->
</PropertyGroup>
</Project>

30
src/Web/Avalonia.Web/Avalonia.Web.targets

@ -4,4 +4,34 @@
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\3.1.7\libHarfBuzzSharp.a" />
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\3.1.7\libSkiaSharp.a" />
</ItemGroup>
<PropertyGroup>
<UseAvaloniaWasmDefaultOptimizations Condition="'$(UseAvaloniaWasmDefaultOptimizations)'==''">True</UseAvaloniaWasmDefaultOptimizations>
<EmccExtraLDFlags>$(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js"</EmccExtraLDFlags>
<EmccFlags>$(EmccExtraLDFlags) -sERROR_ON_UNDEFINED_SYMBOLS=0</EmccFlags>
<WasmBuildNative>true</WasmBuildNative>
</PropertyGroup>
<PropertyGroup Condition="'$(UseAvaloniaWasmDefaultOptimizations)'=='True'">
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<InvariantGlobalization>true</InvariantGlobalization>
<EmccCompileOptimizationFlag>-Oz</EmccCompileOptimizationFlag>
<EmccLinkOptimizationFlag>-Oz</EmccLinkOptimizationFlag>
<WasmEmitSymbolMap>false</WasmEmitSymbolMap>
<WasmNativeDebugSymbols>false</WasmNativeDebugSymbols>
<WasmDebugLevel>0</WasmDebugLevel>
<WasmEnableExceptionHandling>false</WasmEnableExceptionHandling>
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
<DebuggerSupport>false</DebuggerSupport>
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
<EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
<EventSourceSupport>false</EventSourceSupport>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
<UseNativeHttpHandler>true</UseNativeHttpHandler>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<IncludeSatelliteDllsProjectOutputGroup>false</IncludeSatelliteDllsProjectOutputGroup>
</PropertyGroup>
</Project>

76
src/Web/Avalonia.Web/AvaloniaView.cs

@ -1,5 +1,9 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Collections.Pooled;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
using Avalonia.Controls.Platform;
@ -18,6 +22,7 @@ namespace Avalonia.Web
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
public partial class AvaloniaView : ITextInputMethodImpl
{
private static readonly PooledList<RawPointerPoint> s_intermediatePointsPooledList = new(ClearMode.Never);
private readonly BrowserTopLevelImpl _topLevelImpl;
private EmbeddableControlRoot _topLevel;
@ -52,13 +57,13 @@ namespace Avalonia.Web
}
_containerElement = hostContent.GetPropertyAsJSObject("host")
?? throw new InvalidOperationException("Host cannot be null");
?? throw new InvalidOperationException("Host cannot be null");
_canvas = hostContent.GetPropertyAsJSObject("canvas")
?? throw new InvalidOperationException("Canvas cannot be null");
?? throw new InvalidOperationException("Canvas cannot be null");
_nativeControlsContainer = hostContent.GetPropertyAsJSObject("nativeHost")
?? throw new InvalidOperationException("NativeHost cannot be null");
?? throw new InvalidOperationException("NativeHost cannot be null");
_inputElement = hostContent.GetPropertyAsJSObject("inputElement")
?? throw new InvalidOperationException("InputElement cannot be null");
?? throw new InvalidOperationException("InputElement cannot be null");
_splash = DomHelper.GetElementById("avalonia-splash");
@ -96,7 +101,8 @@ namespace Avalonia.Web
OnCompositionUpdate,
OnCompositionEnd);
InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnWheel);
InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp,
OnPointerCancel, OnWheel);
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>();
@ -117,7 +123,12 @@ namespace Avalonia.Web
_context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024);
}
_topLevelImpl.Surfaces = new[] { new BrowserSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) };
_topLevelImpl.Surfaces = new[]
{
new BrowserSkiaSurface(_context, _jsGlInfo, ColorType,
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi,
GRSurfaceOrigin.BottomLeft)
};
}
else
{
@ -135,7 +146,7 @@ namespace Avalonia.Web
DomHelper.ObserveSize(host, null, OnSizeChanged);
CanvasHelper.RequestAnimationFrame(_canvas, true);
InputHelper.FocusElement(_containerElement);
}
@ -155,17 +166,36 @@ namespace Avalonia.Web
private bool OnPointerMove(JSObject args)
{
var type = args.GetPropertyAsString("pointertype");
var pointerType = args.GetPropertyAsString("pointerType");
var point = ExtractRawPointerFromJSArgs(args);
var type = pointerType switch
{
"touch" => RawPointerEventType.TouchUpdate,
_ => RawPointerEventType.Move
};
var coalescedEvents = new Lazy<IReadOnlyList<RawPointerPoint>?>(() =>
{
var points = InputHelper.GetCoalescedEvents(args);
s_intermediatePointsPooledList.Clear();
s_intermediatePointsPooledList.Capacity = points.Length - 1;
// Skip the last one, as it is already processed point.
for (var i = 0; i < points.Length - 1; i++)
{
var point = points[i];
s_intermediatePointsPooledList.Add(ExtractRawPointerFromJSArgs(point));
}
return s_intermediatePointsPooledList;
});
return _topLevelImpl.RawPointerEvent(RawPointerEventType.Move, type!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
return _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"), coalescedEvents);
}
private bool OnPointerDown(JSObject args)
{
var pointerType = args.GetPropertyAsString("pointerType");
var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
var type = pointerType switch
{
"touch" => RawPointerEventType.TouchBegin,
@ -176,20 +206,18 @@ namespace Avalonia.Web
2 => RawPointerEventType.RightButtonDown,
3 => RawPointerEventType.XButton1Down,
4 => RawPointerEventType.XButton2Down,
// 5 => Pen eraser button,
5 => RawPointerEventType.XButton1Down, // should be pen eraser button,
_ => RawPointerEventType.Move
}
};
var point = ExtractRawPointerFromJSArgs(args);
return _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
return _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
}
private bool OnPointerUp(JSObject args)
{
var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
var type = pointerType switch
{
"touch" => RawPointerEventType.TouchEnd,
@ -200,15 +228,27 @@ namespace Avalonia.Web
2 => RawPointerEventType.RightButtonUp,
3 => RawPointerEventType.XButton1Up,
4 => RawPointerEventType.XButton2Up,
// 5 => Pen eraser button,
5 => RawPointerEventType.XButton1Up, // should be pen eraser button,
_ => RawPointerEventType.Move
}
};
var point = ExtractRawPointerFromJSArgs(args);
return _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
}
private bool OnPointerCancel(JSObject args)
{
var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
if (pointerType == "touch")
{
var point = ExtractRawPointerFromJSArgs(args);
_topLevelImpl.RawPointerEvent(RawPointerEventType.TouchCancel, pointerType, point,
GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
}
return false;
}
private bool OnWheel(JSObject args)
{

11
src/Web/Avalonia.Web/BrowserTopLevelImpl.cs

@ -67,17 +67,22 @@ namespace Avalonia.Web
public bool RawPointerEvent(
RawPointerEventType eventType, string pointerType,
RawPointerPoint p, RawInputModifiers modifiers, long touchPointId)
RawPointerPoint p, RawInputModifiers modifiers, long touchPointId,
Lazy<IReadOnlyList<RawPointerPoint>?>? intermediatePoints = null)
{
if (_inputRoot is { }
&& Input is { } input)
{
var device = GetPointerDevice(pointerType);
var args = device is TouchDevice ?
new RawTouchEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers, touchPointId) :
new RawTouchEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers, touchPointId)
{
IntermediatePoints = intermediatePoints
} :
new RawPointerEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers)
{
RawPointerId = touchPointId
RawPointerId = touchPointId,
IntermediatePoints = intermediatePoints
};
input.Invoke(args);

2
src/Web/Avalonia.Web/Interop/CanvasHelper.cs

@ -33,7 +33,7 @@ internal static partial class CanvasHelper
public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop);
[JSImport("Canvas.setCanvasSize", AvaloniaModule.MainModuleName)]
public static partial void SetCanvasSize(JSObject canvas, int height, int width);
public static partial void SetCanvasSize(JSObject canvas, int width, int height);
[JSImport("Canvas.initGL", AvaloniaModule.MainModuleName)]
private static partial JSObject InitGL(

6
src/Web/Avalonia.Web/Interop/InputHelper.cs

@ -1,4 +1,5 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
@ -36,6 +37,8 @@ internal static partial class InputHelper
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> pointerUp,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> pointerCancel,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> wheel);
@ -45,6 +48,9 @@ internal static partial class InputHelper
[JSMarshalAs<JSType.Function<JSType.String, JSType.Boolean>>]
Func<string, bool> input);
[JSImport("InputHelper.getCoalescedEvents", AvaloniaModule.MainModuleName)]
[return: JSMarshalAs<JSType.Array<JSType.Object>>]
public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent);
[JSImport("InputHelper.clearInput", AvaloniaModule.MainModuleName)]
public static partial void ClearInputElement(JSObject htmlElement);

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save