Browse Source

Merge remote-tracking branch 'origin/master' into feature/load-writeable-bitmap-from-file

# Conflicts:
#	src/Avalonia.Controls/ApiCompatBaseline.txt
#	src/Avalonia.Visuals/ApiCompatBaseline.txt
pull/5800/head
Dan Walmsley 5 years ago
parent
commit
fd68976b6e
  1. 2
      .github/PULL_REQUEST_TEMPLATE.md
  2. 2
      Avalonia.sln
  3. 37
      nukebuild/Build.cs
  4. 1
      nukebuild/_build.csproj
  5. 1
      samples/ControlCatalog/Pages/ContextMenuPage.xaml
  6. 14
      src/Avalonia.Base/Data/Optional.cs
  7. 4
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  8. 4
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  9. 4
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  10. 4
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  11. 37
      src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
  12. 2
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  13. 16
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  14. 6
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  15. 5
      src/Avalonia.Controls/ApiCompatBaseline.txt
  16. 2
      src/Avalonia.Controls/AutoCompleteBox.cs
  17. 2
      src/Avalonia.Controls/Control.cs
  18. 6
      src/Avalonia.Controls/IMenuItem.cs
  19. 25
      src/Avalonia.Controls/ItemsControl.cs
  20. 16
      src/Avalonia.Controls/MenuItem.cs
  21. 2
      src/Avalonia.Controls/NativeControlHost.cs
  22. 6
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  23. 76
      src/Avalonia.Controls/Primitives/AccessText.cs
  24. 2
      src/Avalonia.Controls/ScrollViewer.cs
  25. 34
      src/Avalonia.Controls/Selection/InternalSelectionModel.cs
  26. 25
      src/Avalonia.Controls/TextBlock.cs
  27. 12
      src/Avalonia.Controls/TextBox.cs
  28. 3
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  29. 2
      src/Avalonia.Dialogs/ChildFitter.cs
  30. 2
      src/Avalonia.Dialogs/FileSizeStringConverter.cs
  31. 37
      src/Avalonia.Dialogs/ManagedFileChooser.cs
  32. 144
      src/Avalonia.Dialogs/ManagedFileChooser.xaml
  33. 2
      src/Avalonia.Dialogs/ResourceSelectorConverter.cs
  34. 3
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  35. 11
      src/Avalonia.Input/ApiCompatBaseline.txt
  36. 6
      src/Avalonia.Input/Gestures.cs
  37. 8
      src/Avalonia.Input/InputElement.cs
  38. 2
      src/Avalonia.Input/MouseDevice.cs
  39. 13
      src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj
  40. 1
      src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj
  41. 1
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  42. 153
      src/Avalonia.Themes.Default/ManagedFileChooser.xaml
  43. 1
      src/Avalonia.Themes.Default/TextBox.xaml
  44. 3
      src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
  45. 2
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  46. 324
      src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
  47. 1
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  48. 58
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  49. 32
      src/Avalonia.Visuals/Media/BaselineAlignment.cs
  50. 25
      src/Avalonia.Visuals/Media/FlowDirection.cs
  51. 212
      src/Avalonia.Visuals/Media/GlyphRun.cs
  52. 25
      src/Avalonia.Visuals/Media/GlyphRunMetrics.cs
  53. 2
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  54. 3
      src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
  55. 144
      src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs
  56. 11
      src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs
  57. 14
      src/Avalonia.Visuals/Media/TextFormatting/LogicalDirection.cs
  58. 63
      src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
  59. 8
      src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
  60. 6
      src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs
  61. 221
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  62. 302
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  63. 117
      src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
  64. 10
      src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
  65. 265
      src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
  66. 101
      src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
  67. 37
      src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
  68. 2
      src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
  69. 7
      src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
  70. 3
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  71. 12
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  72. 8
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  73. 49
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  74. 4
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  75. 123
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  76. 28
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  77. 2
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  78. 2
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  79. 4
      tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs
  80. 20
      tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
  81. 46
      tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
  82. 38
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs
  83. 121
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattedTextSource.cs
  84. 5
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
  85. 7
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
  86. 95
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  87. 71
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  88. 36
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  89. 3
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  90. 2
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
  91. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png
  92. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png
  93. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png
  94. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png
  95. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png
  96. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png
  97. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png
  98. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png
  99. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png
  100. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png

2
.github/PULL_REQUEST_TEMPLATE.md

@ -23,6 +23,8 @@
## Breaking changes
<!--- List any breaking changes here. When the PR is merged please add an entry to https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes -->
## Obsoletions / Deprecations
<!--- Obsolete and Deprecated attributes on APIs MUST only be included when discussed with Core team. @grokys, @kekekeks & @danwalmsley -->
## Fixed issues
<!--- If the pull request fixes issue(s) list them like this:

2
Avalonia.sln

@ -222,8 +222,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "sr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroComGenerator", "src\tools\MicroComGenerator\MicroComGenerator.csproj", "{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}"

37
nukebuild/Build.cs

@ -16,7 +16,6 @@ using Nuke.Common.Tools.MSBuild;
using Nuke.Common.Tools.Npm;
using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections;
using Pharmacist.Core;
using static Nuke.Common.EnvironmentInfo;
using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction;
@ -163,44 +162,8 @@ partial class Build : NukeBuild
.AddProperty("PackageVersion", Parameters.Version)
.SetConfiguration(Parameters.Configuration)
);
await CompileReactiveEvents();
});
async Task CompileReactiveEvents()
{
var avaloniaBuildOutput = Path.Combine(RootDirectory, "packages", "Avalonia", "bin", Parameters.Configuration);
var avaloniaAssemblies = GlobFiles(avaloniaBuildOutput, "**/Avalonia*.dll")
.Where(file => !file.Contains("Avalonia.Build.Tasks") &&
!file.Contains("Avalonia.Remote.Protocol"));
var eventsDirectory = GlobDirectories($"{RootDirectory}/src/**/Avalonia.ReactiveUI.Events").First();
var eventsBuildFile = Path.Combine(eventsDirectory, "Events_Avalonia.cs");
if (File.Exists(eventsBuildFile))
File.Delete(eventsBuildFile);
using (var stream = File.Create(eventsBuildFile))
using (var writer = new StreamWriter(stream))
{
await ObservablesForEventGenerator.ExtractEventsFromAssemblies(
writer, avaloniaAssemblies, new string[0], "netstandard2.0"
);
}
var eventsProject = Path.Combine(eventsDirectory, "Avalonia.ReactiveUI.Events.csproj");
if (Parameters.IsRunningOnWindows)
MsBuildCommon(eventsProject, c => c
.SetProcessArgumentConfigurator(a => a.Add("/r"))
.AddTargets("Build")
);
else
DotNetBuild(c => c
.SetProjectFile(eventsProject)
.AddProperty("PackageVersion", Parameters.Version)
.SetConfiguration(Parameters.Configuration)
);
}
void RunCoreTest(string projectName)
{
Information($"Running tests from {projectName}");

1
nukebuild/_build.csproj

@ -17,7 +17,6 @@
<PackageReference Include="ILRepack.NETStandard" Version="2.0.4" />
<!-- Keep in sync with Avalonia.Build.Tasks -->
<PackageReference Include="Mono.Cecil" Version="0.11.2" />
<PackageReference Include="Pharmacist.Core" Version="1.8.1" />
</ItemGroup>
<ItemGroup>

1
samples/ControlCatalog/Pages/ContextMenuPage.xaml

@ -31,6 +31,7 @@
<CheckBox BorderThickness="0" IsHitTestVisible="False" IsChecked="True"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Menu Item that won't close on click" StaysOpenOnClick="True" />
</ContextMenu>
</Border.ContextMenu>
<TextBlock Text="Defined in XAML"/>

14
src/Avalonia.Base/Data/Optional.cs

@ -153,4 +153,18 @@ namespace Avalonia.Data
/// </summary>
public static Optional<T> Empty => default;
}
public static class OptionalExtensions
{
/// <summary>
/// Casts the type of an <see cref="Optional{T}"/> using only the C# cast operator.
/// </summary>
/// <typeparam name="T">The target type.</typeparam>
/// <param name="value">The binding value.</param>
/// <returns>The cast value.</returns>
public static Optional<T> Cast<T>(this Optional<object> value)
{
return value.HasValue ? new Optional<T>((T)value.Value) : Optional<T>.Empty;
}
}
}

4
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@ -127,8 +127,8 @@ namespace Avalonia.PropertyStore
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
oldValue.Cast<T>(),
newValue.Cast<T>(),
Priority));
}

4
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@ -65,8 +65,8 @@ namespace Avalonia.PropertyStore
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
oldValue.Cast<T>(),
newValue.Cast<T>(),
Priority));
}
}

4
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@ -36,8 +36,8 @@ namespace Avalonia.PropertyStore
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
oldValue.Cast<T>(),
newValue.Cast<T>(),
BindingPriority.LocalValue));
}
}

4
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -197,8 +197,8 @@ namespace Avalonia.PropertyStore
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
oldValue.Cast<T>(),
newValue.Cast<T>(),
Priority));
}

37
src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs

@ -265,6 +265,43 @@ namespace Avalonia.Collections
{
return new DataGridPathSortDescription(propertyPath, direction, comparer, null);
}
public static DataGridSortDescription FromComparer(IComparer comparer, ListSortDirection direction = ListSortDirection.Ascending)
{
return new DataGridComparerSortDesctiption(comparer, direction);
}
}
public class DataGridComparerSortDesctiption : DataGridSortDescription
{
private readonly IComparer _innerComparer;
private readonly ListSortDirection _direction;
private readonly IComparer<object> _comparer;
public IComparer SourceComparer => _innerComparer;
public override IComparer<object> Comparer => _comparer;
public override ListSortDirection Direction => _direction;
public DataGridComparerSortDesctiption(IComparer comparer, ListSortDirection direction)
{
_innerComparer = comparer;
_direction = direction;
_comparer = Comparer<object>.Create((x, y) => Compare(x, y));
}
private int Compare(object x, object y)
{
int result = _innerComparer.Compare(x, y);
if (Direction == ListSortDirection.Descending)
return -result;
else
return result;
}
public override DataGridSortDescription SwitchSortDirection()
{
var newDirection = _direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
return new DataGridComparerSortDesctiption(_innerComparer, newDirection);
}
}
public class DataGridSortDescriptionCollection : AvaloniaList<DataGridSortDescription>

2
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -5357,7 +5357,7 @@ namespace Avalonia.Controls
_focusedRow = null;
}
private void SelectAll()
public void SelectAll()
{
SetRowsSelection(0, SlotCount - 1);
}

16
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -1009,6 +1009,14 @@ namespace Avalonia.Controls
get;
set;
}
/// <summary>
/// Holds a Comparer to use for sorting, if not using the default.
/// </summary>
public System.Collections.IComparer CustomSortComparer
{
get;
set;
}
/// <summary>
/// We get the sort description from the data source. We don't worry whether we can modify sort -- perhaps the sort description
@ -1020,6 +1028,14 @@ namespace Avalonia.Controls
&& OwningGrid.DataConnection != null
&& OwningGrid.DataConnection.SortDescriptions != null)
{
if(CustomSortComparer != null)
{
return
OwningGrid.DataConnection.SortDescriptions
.OfType<DataGridComparerSortDesctiption>()
.FirstOrDefault(s => s.SourceComparer == CustomSortComparer);
}
string propertyName = GetSortPropertyName();
return OwningGrid.DataConnection.SortDescriptions.FirstOrDefault(s => s.HasPropertyPath && s.PropertyPath == propertyName);

6
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -274,6 +274,12 @@ namespace Avalonia.Controls
owningGrid.DataConnection.SortDescriptions.Add(newSort);
}
}
else if (OwningColumn.CustomSortComparer != null)
{
newSort = DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer);
owningGrid.DataConnection.SortDescriptions.Add(newSort);
}
else
{
string propertyName = OwningColumn.GetSortPropertyName();

5
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -1,4 +1,7 @@
Compat issues with assembly Avalonia.Controls:
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick.get()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.IMenuItem.StaysOpenOnClick.set(System.Boolean)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
@ -7,4 +10,4 @@ EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
Total Issues: 8
Total Issues: 11

2
src/Avalonia.Controls/AutoCompleteBox.cs

@ -31,7 +31,6 @@ namespace Avalonia.Controls
/// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populated" />
/// event.
/// </summary>
[PseudoClasses(":dropdownopen")]
public class PopulatedEventArgs : EventArgs
{
/// <summary>
@ -253,6 +252,7 @@ namespace Avalonia.Controls
/// drop-down that contains possible matches based on the input in the text
/// box.
/// </summary>
[PseudoClasses(":dropdownopen")]
public class AutoCompleteBox : TemplatedControl
{
/// <summary>

2
src/Avalonia.Controls/Control.cs

@ -37,7 +37,6 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="ContextMenu"/> property.
/// </summary>
[Obsolete("Prefer ContextFlyout")]
public static readonly StyledProperty<ContextMenu?> ContextMenuProperty =
AvaloniaProperty.Register<Control, ContextMenu?>(nameof(ContextMenu));
@ -77,7 +76,6 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets a context menu to the control.
/// </summary>
[Obsolete("Prefer ContextFlyout")]
public ContextMenu? ContextMenu
{
get => GetValue(ContextMenuProperty);

6
src/Avalonia.Controls/IMenuItem.cs

@ -23,6 +23,12 @@ namespace Avalonia.Controls
/// </summary>
bool IsSubMenuOpen { get; set; }
/// <summary>
/// Gets or sets a value that indicates the submenu that this <see cref="MenuItem"/> is
/// within should not close when this item is clicked.
/// </summary>
bool StaysOpenOnClick { get; set; }
/// <summary>
/// Gets a value that indicates whether the <see cref="MenuItem"/> is a top-level main menu item.
/// </summary>

25
src/Avalonia.Controls/ItemsControl.cs

@ -70,7 +70,7 @@ namespace Avalonia.Controls
/// </summary>
public ItemsControl()
{
PseudoClasses.Add(":empty");
UpdatePseudoClasses(0);
SubscribeToItems(_items);
}
@ -323,6 +323,16 @@ namespace Avalonia.Controls
base.OnKeyDown(e);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == ItemCountProperty)
{
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<int>());
}
}
/// <summary>
/// Called when the <see cref="Items"/> property changes.
/// </summary>
@ -371,10 +381,6 @@ namespace Avalonia.Controls
}
Presenter?.ItemsChanged(e);
var collection = sender as ICollection;
PseudoClasses.Set(":empty", collection == null || collection.Count == 0);
PseudoClasses.Set(":singleitem", collection != null && collection.Count == 1);
}
/// <summary>
@ -431,9 +437,6 @@ namespace Avalonia.Controls
/// <param name="items">The items collection.</param>
private void SubscribeToItems(IEnumerable items)
{
PseudoClasses.Set(":empty", items == null || items.Count() == 0);
PseudoClasses.Set(":singleitem", items != null && items.Count() == 1);
if (items is INotifyCollectionChanged incc)
{
CollectionChangedEventManager.Instance.AddListener(incc, this);
@ -469,6 +472,12 @@ namespace Avalonia.Controls
}
}
private void UpdatePseudoClasses(int itemCount)
{
PseudoClasses.Set(":empty", itemCount == 0);
PseudoClasses.Set(":singleitem", itemCount == 1);
}
protected static IInputElement GetNextControl(
INavigableContainer container,
NavigationDirection direction,

16
src/Avalonia.Controls/MenuItem.cs

@ -69,6 +69,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> IsSubMenuOpenProperty =
AvaloniaProperty.Register<MenuItem, bool>(nameof(IsSubMenuOpen));
/// <summary>
/// Defines the <see cref="StaysOpenOnClick"/> property.
/// </summary>
public static readonly StyledProperty<bool> StaysOpenOnClickProperty =
AvaloniaProperty.Register<MenuItem, bool>(nameof(StaysOpenOnClick));
/// <summary>
/// Defines the <see cref="Click"/> event.
/// </summary>
@ -265,6 +271,16 @@ namespace Avalonia.Controls
set { SetValue(IsSubMenuOpenProperty, value); }
}
/// <summary>
/// Gets or sets a value that indicates the submenu that this <see cref="MenuItem"/> is
/// within should not close when this item is clicked.
/// </summary>
public bool StaysOpenOnClick
{
get { return GetValue(StaysOpenOnClickProperty); }
set { SetValue(StaysOpenOnClickProperty, value); }
}
/// <summary>
/// Gets or sets a value that indicates whether the <see cref="MenuItem"/> has a submenu.
/// </summary>

2
src/Avalonia.Controls/NativeControlHost.cs

@ -120,7 +120,7 @@ namespace Avalonia.Controls
private Rect? GetAbsoluteBounds()
{
var bounds = Bounds;
var position = this.TranslatePoint(bounds.Position, _currentRoot);
var position = this.TranslatePoint(default, _currentRoot);
if (position == null)
return null;
return new Rect(position.Value, bounds.Size);

6
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -449,7 +449,11 @@ namespace Avalonia.Controls.Platform
protected void Click(IMenuItem item)
{
item.RaiseClick();
CloseMenu(item);
if (!item.StaysOpenOnClick)
{
CloseMenu(item);
}
}
protected void CloseMenu(IMenuItem item)

76
src/Avalonia.Controls/Primitives/AccessText.cs

@ -67,7 +67,7 @@ namespace Avalonia.Controls.Primitives
if (underscore != -1 && ShowAccessKey)
{
var rect = HitTestTextPosition(underscore);
var rect = TextLayout.HitTestTextPosition(underscore);
var offset = new Vector(0, -0.5);
context.DrawLine(
new Pen(Foreground, 1),
@ -76,80 +76,6 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Get the pixel location relative to the top-left of the layout box given the text position.
/// </summary>
/// <param name="textPosition">The text position.</param>
/// <returns></returns>
private Rect HitTestTextPosition(int textPosition)
{
if (TextLayout == null)
{
return new Rect();
}
if (TextLayout.TextLines.Count == 0)
{
return new Rect();
}
if (textPosition < 0 || textPosition >= Text.Length)
{
var lastLine = TextLayout.TextLines[TextLayout.TextLines.Count - 1];
var lineX = lastLine.LineMetrics.Size.Width;
var lineY = Bounds.Height - lastLine.LineMetrics.Size.Height;
return new Rect(lineX, lineY, 0, lastLine.LineMetrics.Size.Height);
}
var currentY = 0.0;
foreach (var textLine in TextLayout.TextLines)
{
if (textLine.TextRange.End < textPosition)
{
currentY += textLine.LineMetrics.Size.Height;
continue;
}
var currentX = 0.0;
foreach (var textRun in textLine.TextRuns)
{
if (!(textRun is ShapedTextCharacters shapedTextCharacters))
{
continue;
}
if (shapedTextCharacters.GlyphRun.Characters.End < textPosition)
{
currentX += shapedTextCharacters.Size.Width;
continue;
}
var characterHit =
shapedTextCharacters.GlyphRun.FindNearestCharacterHit(textPosition, out var width);
var distance = shapedTextCharacters.GlyphRun.GetDistanceFromCharacterHit(characterHit);
currentX += distance - width;
if (characterHit.TrailingLength == 0)
{
width = 0.0;
}
return new Rect(currentX, currentY, width, shapedTextCharacters.Size.Height);
}
}
return new Rect();
}
/// <inheritdoc/>
protected override TextLayout CreateTextLayout(Size constraint, string text)
{

2
src/Avalonia.Controls/ScrollViewer.cs

@ -120,7 +120,7 @@ namespace Avalonia.Controls
public static readonly AttachedProperty<ScrollBarVisibility> HorizontalScrollBarVisibilityProperty =
AvaloniaProperty.RegisterAttached<ScrollViewer, Control, ScrollBarVisibility>(
nameof(HorizontalScrollBarVisibility),
ScrollBarVisibility.Hidden);
ScrollBarVisibility.Disabled);
/// <summary>
/// Defines the VerticalScrollBarMaximum property.

34
src/Avalonia.Controls/Selection/InternalSelectionModel.cs

@ -80,10 +80,12 @@ namespace Avalonia.Controls.Selection
try
{
_ignoreSelectedItemsChanges = true;
++_ignoreModelChanges;
base.SetSource(value);
}
finally
{
--_ignoreModelChanges;
_ignoreSelectedItemsChanges = false;
}
@ -93,17 +95,14 @@ namespace Avalonia.Controls.Selection
}
else
{
foreach (var i in oldSelection)
{
var index = ItemsView!.IndexOf(i);
Select(index);
}
SyncFromSelectedItems();
}
}
private void SyncToSelectedItems()
{
if (_writableSelectedItems is object)
if (_writableSelectedItems is object &&
!SequenceEqual(_writableSelectedItems, base.SelectedItems))
{
try
{
@ -224,6 +223,7 @@ namespace Avalonia.Controls.Selection
if (_isResetting)
{
--_ignoreModelChanges;
_isResetting = false;
}
}
@ -310,5 +310,27 @@ namespace Avalonia.Controls.Selection
return -1;
}
private static bool SequenceEqual(IList first, IReadOnlyList<object?> second)
{
if (first is IEnumerable<object?> e)
{
return e.SequenceEqual(second);
}
var comparer = EqualityComparer<object?>.Default;
var e1 = first.GetEnumerator();
using var e2 = second.GetEnumerator();
while (e1.MoveNext())
{
if (!(e2.MoveNext() && comparer.Equals(e1.Current, e2.Current)))
{
return false;
}
}
return !e2.MoveNext();
}
}
}

25
src/Avalonia.Controls/TextBlock.cs

@ -416,26 +416,8 @@ namespace Avalonia.Controls
{
return;
}
var textAlignment = TextAlignment;
var width = Bounds.Size.Width;
var offsetX = 0.0;
switch (textAlignment)
{
case TextAlignment.Center:
offsetX = (width - TextLayout.Size.Width) / 2;
break;
case TextAlignment.Right:
offsetX = width - TextLayout.Size.Width;
break;
}
var padding = Padding;
var top = padding.Top;
var textSize = TextLayout.Size;
@ -453,10 +435,7 @@ namespace Avalonia.Controls
}
}
using (context.PushPostTransform(Matrix.CreateTranslation(padding.Left + offsetX, top)))
{
TextLayout.Draw(context);
}
TextLayout.Draw(context, new Point(padding.Left, top));
}
/// <summary>

12
src/Avalonia.Controls/TextBox.cs

@ -175,16 +175,12 @@ namespace Avalonia.Controls
this.GetObservable(TextWrappingProperty),
(acceptsReturn, wrapping) =>
{
if (acceptsReturn)
if (wrapping != TextWrapping.NoWrap)
{
return wrapping != TextWrapping.Wrap ?
ScrollBarVisibility.Auto :
ScrollBarVisibility.Disabled;
}
else
{
return ScrollBarVisibility.Hidden;
return ScrollBarVisibility.Disabled;
}
return acceptsReturn ? ScrollBarVisibility.Auto : ScrollBarVisibility.Hidden;
});
this.Bind(
ScrollViewer.HorizontalScrollBarVisibilityProperty,

3
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -4,9 +4,6 @@
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="**\*.xaml">
<SubType>Designer</SubType>
</AvaloniaResource>
<AvaloniaResource Include="Assets\*" />
</ItemGroup>

2
src/Avalonia.Dialogs/ChildFitter.cs

@ -4,7 +4,7 @@ using Avalonia.Layout;
namespace Avalonia.Dialogs
{
internal class ChildFitter : Decorator
public class ChildFitter : Decorator
{
protected override Size MeasureOverride(Size availableSize)
{

2
src/Avalonia.Dialogs/FileSizeStringConverter.cs

@ -6,7 +6,7 @@ using System.Text;
namespace Avalonia.Dialogs
{
internal class FileSizeStringConverter : IValueConverter
public class FileSizeStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{

37
src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs → src/Avalonia.Dialogs/ManagedFileChooser.cs

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
@ -10,17 +11,14 @@ using Avalonia.Markup.Xaml;
namespace Avalonia.Dialogs
{
internal class ManagedFileChooser : UserControl
public class ManagedFileChooser : TemplatedControl
{
private Control _quickLinksRoot;
private ListBox _filesView;
public ManagedFileChooser()
{
AvaloniaXamlLoader.Load(this);
AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel);
_quickLinksRoot = this.FindControl<Control>("QuickLinks");
_filesView = this.FindControl<ListBox>("Files");
}
ManagedFileChooserViewModel Model => DataContext as ManagedFileChooserViewModel;
@ -34,19 +32,22 @@ namespace Avalonia.Dialogs
return;
}
var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
if (e.ClickCount == 2 || isQuickLink)
if (_quickLinksRoot != null)
{
if (model.ItemType == ManagedFileChooserItemType.File)
var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
if (e.ClickCount == 2 || isQuickLink)
{
Model?.SelectSingleFile(model);
if (model.ItemType == ManagedFileChooserItemType.File)
{
Model?.SelectSingleFile(model);
}
else
{
Model?.Navigate(model.Path);
}
e.Handled = true;
}
else
{
Model?.Navigate(model.Path);
}
e.Handled = true;
}
}
@ -79,10 +80,16 @@ namespace Avalonia.Dialogs
// Workaround for ListBox bug, scroll to the previous file
var indexOfPreselected = model.Items.IndexOf(preselected);
if (indexOfPreselected > 1)
if ((_filesView != null) && (indexOfPreselected > 1))
{
_filesView.ScrollIntoView(indexOfPreselected - 1);
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_quickLinksRoot = e.NameScope.Get<Control>("QuickLinks");
_filesView = e.NameScope.Get<ListBox>("Files");
}
}
}

144
src/Avalonia.Dialogs/ManagedFileChooser.xaml

@ -1,144 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dialogs="clr-namespace:Avalonia.Dialogs"
xmlns:internal="clr-namespace:Avalonia.Dialogs"
x:Class="Avalonia.Dialogs.ManagedFileChooser" Margin="10">
<UserControl.Resources>
<internal:FileSizeStringConverter x:Key="FileSizeConverter" />
<DrawingGroup x:Key="LevelUp">
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M14.5,0L6.39,0 5.39,2 2.504,2C1.677,2,1,2.673,1,3.5L1,10.582 1,10.586 1,15.414 3,13.414 3,16 7,16 7,13.414 9,15.414 9,13 14.5,13C15.327,13,16,12.327,16,11.5L16,1.5C16,0.673,15.327,0,14.5,0" />
<GeometryDrawing Brush="#FFDCB679" Geometry="F1M14,3L7.508,3 8.008,2 8.012,2 14,2z M14.5,1L7.008,1 6.008,3 2.504,3C2.227,3,2,3.224,2,3.5L2,9.582 4.998,6.586 9,10.586 9,12 14.5,12C14.775,12,15,11.776,15,11.5L15,1.5C15,1.224,14.775,1,14.5,1" />
<GeometryDrawing Brush="#FF00529C" Geometry="F1M8,11L5,8 2,11 2,13 4,11 4,15 6,15 6,11 8,13z" />
<GeometryDrawing Brush="#FFF0EFF1" Geometry="F1M8.0001,1.9996L7.5001,3.0006 14.0001,3.0006 14.0001,1.9996z" />
</DrawingGroup>
<dialogs:ResourceSelectorConverter x:Key="Icons">
<DrawingGroup x:Key="Icon_Folder">
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M0,0L16,0 16,16 0,16z" />
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M1.5,1L9.61,1 10.61,3 13.496,3C14.323,3,14.996,3.673,14.996,4.5L14.996,12.5C14.996,13.327,14.323,14,13.496,14L1.5,14C0.673,14,0,13.327,0,12.5L0,2.5C0,1.673,0.673,1,1.5,1" />
<GeometryDrawing Brush="#FFDCB67A" Geometry="F1M2,3L8.374,3 8.874,4 2,4z M13.496,4L10,4 9.992,4 8.992,2 1.5,2C1.225,2,1,2.224,1,2.5L1,12.5C1,12.776,1.225,13,1.5,13L13.496,13C13.773,13,13.996,12.776,13.996,12.5L13.996,4.5C13.996,4.224,13.773,4,13.496,4" />
<GeometryDrawing Brush="#FFEFEFF0" Geometry="F1M2,3L8.374,3 8.874,4 2,4z" />
</DrawingGroup>
<DrawingGroup x:Key="Icon_File">
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M4,15C3.03,15,2,14.299,2,13L2,3C2,1.701,3.03,1,4,1L10.061,1 14,4.556 14,13C14,13.97,13.299,15,12,15z" />
<GeometryDrawing Brush="#FF9B4E96" Geometry="F1M12,13L4,13 4,3 9,3 9,6 12,6z M9.641,2L3.964,2C3.964,2,3,2,3,3L3,13C3,14,3.964,14,3.964,14L11.965,14C12.965,14,13,13,13,13L13,5z" />
<GeometryDrawing Brush="#FFF0EFF1" Geometry="F1M4,3L9,3 9,6 12,6 12,13 4,13z" />
</DrawingGroup>
<DrawingGroup x:Key="Icon_Volume">
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M0,12L0,6.5C0,5.122,1.122,4,2.5,4L13.5,4C14.879,4,16,5.122,16,6.5L16,12z" />
<GeometryDrawing Brush="#FFEFEFF0" Geometry="F1M13,8L12,8 12,7 13,7z M11,8L10,8 10,7 11,7z M13.5,6L2.5,6C2.224,6,2,6.224,2,6.5L2,10 14,10 14,6.5C14,6.224,13.775,6,13.5,6" />
<GeometryDrawing Brush="#FF424242" Geometry="F1M13,7L12,7 12,8 13,8z M11,7L10,7 10,8 11,8z M2,10L14,10 14,6.5C14,6.224,13.775,6,13.5,6L2.5,6C2.224,6,2,6.224,2,6.5z M15,11L1,11 1,6.5C1,5.673,1.673,5,2.5,5L13.5,5C14.327,5,15,5.673,15,6.5z" />
</DrawingGroup>
</dialogs:ResourceSelectorConverter>
</UserControl.Resources>
<DockPanel>
<DockPanel DockPanel.Dock="Top" Margin="0 0 0 5">
<dialogs:ChildFitter DockPanel.Dock="Right" Width="{Binding ElementName=Location, Path=Bounds.Height}">
<Button Command="{Binding GoUp}" >
<DrawingPresenter Drawing="{StaticResource LevelUp}" Stretch="Fill"/>
</Button>
</dialogs:ChildFitter>
<TextBox x:Name="Location" Text="{Binding Location}" Margin="0 0 5 0">
<TextBox.KeyBindings>
<KeyBinding Command="{Binding EnterPressed}" Gesture="Enter"/>
</TextBox.KeyBindings>
</TextBox>
</DockPanel>
<DockPanel Margin="0 5 0 0" DockPanel.Dock="Bottom">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
<CheckBox IsChecked="{Binding ShowHiddenFiles}">
<TextBlock>Show hidden files</TextBlock>
</CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Margin" Value="4"/>
</Style>
</StackPanel.Styles>
<Button Command="{Binding Ok}">OK</Button>
<Button Command="{Binding Cancel}">Cancel</Button>
</StackPanel>
</DockPanel>
<ComboBox DockPanel.Dock="Bottom"
IsVisible="{Binding ShowFilters}"
Items="{Binding Filters}"
SelectedItem="{Binding SelectedFilter}"
Margin="0 5 0 0" />
<TextBox Text="{Binding FileName}" Watermark="File name" DockPanel.Dock="Bottom" IsVisible="{Binding !SelectingFolder}" />
<ListBox Margin="0 0 5 5" BorderBrush="Transparent" x:Name="QuickLinks" Items="{Binding QuickLinks}"
SelectedIndex="{Binding QuickLinksSelectedIndex}"
DockPanel.Dock="Left" Focusable="False">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Spacing="4" Orientation="Horizontal" Background="Transparent">
<DrawingPresenter Width="16" Height="16" Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/>
<TextBlock Text="{Binding DisplayName}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<DockPanel Grid.IsSharedSizeScope="True">
<Grid DockPanel.Dock="Top" Margin="15 5 0 0" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" SharedSizeGroup="Icon" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="400" SharedSizeGroup="Name" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Modified" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="150" SharedSizeGroup="Type" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Size" />
</Grid.ColumnDefinitions>
<GridSplitter Grid.Column="1" />
<TextBlock Grid.Column="2" Text="Name" />
<GridSplitter Grid.Column="3" />
<TextBlock Grid.Column="4" Text="Date Modified" />
<GridSplitter Grid.Column="5" />
<TextBlock Grid.Column="6" Text="Type" />
<GridSplitter Grid.Column="7" />
<TextBlock Grid.Column="8" Text="Size" />
</Grid>
<ListBox x:Name="Files"
VirtualizationMode="Simple"
Items="{Binding Items}"
Margin="0 5"
SelectionMode="{Binding SelectionMode}"
SelectedItems="{Binding SelectedItems}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Icon" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Name" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Modified" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Type" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Size" />
</Grid.ColumnDefinitions>
<DrawingPresenter Width="16" Height="16" Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/>
<TextBlock Grid.Column="2" Text="{Binding DisplayName}"/>
<TextBlock Grid.Column="4" Text="{Binding Modified}" />
<TextBlock Grid.Column="6" Text="{Binding Type}" />
<TextBlock Grid.Column="8" Text="{Binding Size, Converter={StaticResource FileSizeConverter}}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</DockPanel>
</UserControl>

2
src/Avalonia.Dialogs/ResourceSelectorConverter.cs

@ -5,7 +5,7 @@ using Avalonia.Data.Converters;
namespace Avalonia.Dialogs
{
internal class ResourceSelectorConverter : ResourceDictionary, IValueConverter
public class ResourceSelectorConverter : ResourceDictionary, IValueConverter
{
public object Convert(object key, Type targetType, object parameter, CultureInfo culture)
{

3
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -112,9 +112,8 @@ namespace Avalonia.Headless
return new HeadlessBitmapStub(destinationSize, new Vector(96, 96));
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
width = 100;
return new HeadlessGlyphRunStub();
}

11
src/Avalonia.Input/ApiCompatBaseline.txt

@ -1,4 +1,13 @@
Compat issues with assembly Avalonia.Input:
MembersMustExist : Member 'public Avalonia.Platform.IPlatformHandle Avalonia.Input.Cursor.PlatformCursor.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent<Avalonia.Interactivity.RoutedEventArgs> Avalonia.Interactivity.RoutedEvent<Avalonia.Interactivity.RoutedEventArgs> Avalonia.Input.Gestures.DoubleTappedEvent' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent<Avalonia.Interactivity.RoutedEventArgs> Avalonia.Interactivity.RoutedEvent<Avalonia.Interactivity.RoutedEventArgs> Avalonia.Input.Gestures.RightTappedEvent' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent<Avalonia.Interactivity.RoutedEventArgs> Avalonia.Interactivity.RoutedEvent<Avalonia.Interactivity.RoutedEventArgs> Avalonia.Input.Gestures.TappedEvent' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent<Avalonia.Interactivity.RoutedEventArgs> Avalonia.Interactivity.RoutedEvent<Avalonia.Interactivity.RoutedEventArgs> Avalonia.Input.InputElement.DoubleTappedEvent' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent<Avalonia.Interactivity.RoutedEventArgs> Avalonia.Interactivity.RoutedEvent<Avalonia.Interactivity.RoutedEventArgs> Avalonia.Input.InputElement.TappedEvent' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_DoubleTapped(System.EventHandler<Avalonia.Interactivity.RoutedEventArgs>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_Tapped(System.EventHandler<Avalonia.Interactivity.RoutedEventArgs>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_DoubleTapped(System.EventHandler<Avalonia.Interactivity.RoutedEventArgs>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_Tapped(System.EventHandler<Avalonia.Interactivity.RoutedEventArgs>)' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Platform.IStandardCursorFactory' does not exist in the implementation but it does exist in the contract.
Total Issues: 2
Total Issues: 11

6
src/Avalonia.Input/Gestures.cs

@ -6,17 +6,17 @@ namespace Avalonia.Input
{
public static class Gestures
{
public static readonly RoutedEvent<RoutedEventArgs> TappedEvent = RoutedEvent.Register<RoutedEventArgs>(
public static readonly RoutedEvent<TappedEventArgs> TappedEvent = RoutedEvent.Register<TappedEventArgs>(
"Tapped",
RoutingStrategies.Bubble,
typeof(Gestures));
public static readonly RoutedEvent<RoutedEventArgs> DoubleTappedEvent = RoutedEvent.Register<RoutedEventArgs>(
public static readonly RoutedEvent<TappedEventArgs> DoubleTappedEvent = RoutedEvent.Register<TappedEventArgs>(
"DoubleTapped",
RoutingStrategies.Bubble,
typeof(Gestures));
public static readonly RoutedEvent<RoutedEventArgs> RightTappedEvent = RoutedEvent.Register<RoutedEventArgs>(
public static readonly RoutedEvent<TappedEventArgs> RightTappedEvent = RoutedEvent.Register<TappedEventArgs>(
"RightTapped",
RoutingStrategies.Bubble,
typeof(Gestures));

8
src/Avalonia.Input/InputElement.cs

@ -176,12 +176,12 @@ namespace Avalonia.Input
/// <summary>
/// Defines the <see cref="Tapped"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> TappedEvent = Gestures.TappedEvent;
public static readonly RoutedEvent<TappedEventArgs> TappedEvent = Gestures.TappedEvent;
/// <summary>
/// Defines the <see cref="DoubleTapped"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> DoubleTappedEvent = Gestures.DoubleTappedEvent;
public static readonly RoutedEvent<TappedEventArgs> DoubleTappedEvent = Gestures.DoubleTappedEvent;
private bool _isEffectivelyEnabled = true;
private bool _isFocused;
@ -346,7 +346,7 @@ namespace Avalonia.Input
/// <summary>
/// Occurs when a tap gesture occurs on the control.
/// </summary>
public event EventHandler<RoutedEventArgs> Tapped
public event EventHandler<TappedEventArgs> Tapped
{
add { AddHandler(TappedEvent, value); }
remove { RemoveHandler(TappedEvent, value); }
@ -355,7 +355,7 @@ namespace Avalonia.Input
/// <summary>
/// Occurs when a double-tap gesture occurs on the control.
/// </summary>
public event EventHandler<RoutedEventArgs> DoubleTapped
public event EventHandler<TappedEventArgs> DoubleTapped
{
add { AddHandler(DoubleTappedEvent, value); }
remove { RemoveHandler(DoubleTappedEvent, value); }

2
src/Avalonia.Input/MouseDevice.cs

@ -435,7 +435,7 @@ namespace Avalonia.Input
IInputElement? branch = null;
var el = element;
IInputElement? el = element;
while (el != null)
{

13
src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj

@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Avalonia.ReactiveUI.Events</PackageId>
<SignAssembly>false</SignAssembly>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Pharmacist.Common" Version="1.8.1" />
</ItemGroup>
</Project>

1
src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj

@ -12,6 +12,7 @@
<ProjectReference Include="..\Avalonia.Layout\Avalonia.Layout.csproj" />
<ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
<ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<AvaloniaResource Include="DefaultTheme.xaml" />
<AvaloniaResource Include="Accents/*.xaml" />
<!-- Compatibility with old apps, probably need to replace with AvaloniaResource -->

1
src/Avalonia.Themes.Default/DefaultTheme.xaml

@ -62,4 +62,5 @@
<StyleInclude Source="resm:Avalonia.Themes.Default.TimePicker.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.FlyoutPresenter.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.MenuFlyoutPresenter.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ManagedFileChooser.xaml?assembly=Avalonia.Themes.Default"/>
</Styles>

153
src/Avalonia.Themes.Default/ManagedFileChooser.xaml

@ -0,0 +1,153 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dialogs="clr-namespace:Avalonia.Dialogs;assembly=Avalonia.Dialogs">
<Style Selector="dialogs|ManagedFileChooser">
<Style.Resources>
<ResourceDictionary>
<DrawingGroup x:Key="LevelUp">
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M14.5,0L6.39,0 5.39,2 2.504,2C1.677,2,1,2.673,1,3.5L1,10.582 1,10.586 1,15.414 3,13.414 3,16 7,16 7,13.414 9,15.414 9,13 14.5,13C15.327,13,16,12.327,16,11.5L16,1.5C16,0.673,15.327,0,14.5,0" />
<GeometryDrawing Brush="#FFDCB679" Geometry="F1M14,3L7.508,3 8.008,2 8.012,2 14,2z M14.5,1L7.008,1 6.008,3 2.504,3C2.227,3,2,3.224,2,3.5L2,9.582 4.998,6.586 9,10.586 9,12 14.5,12C14.775,12,15,11.776,15,11.5L15,1.5C15,1.224,14.775,1,14.5,1" />
<GeometryDrawing Brush="#FF00529C" Geometry="F1M8,11L5,8 2,11 2,13 4,11 4,15 6,15 6,11 8,13z" />
<GeometryDrawing Brush="#FFF0EFF1" Geometry="F1M8.0001,1.9996L7.5001,3.0006 14.0001,3.0006 14.0001,1.9996z" />
</DrawingGroup>
<dialogs:ResourceSelectorConverter x:Key="Icons">
<DrawingGroup x:Key="Icon_Folder">
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M0,0L16,0 16,16 0,16z" />
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M1.5,1L9.61,1 10.61,3 13.496,3C14.323,3,14.996,3.673,14.996,4.5L14.996,12.5C14.996,13.327,14.323,14,13.496,14L1.5,14C0.673,14,0,13.327,0,12.5L0,2.5C0,1.673,0.673,1,1.5,1" />
<GeometryDrawing Brush="#FFDCB67A" Geometry="F1M2,3L8.374,3 8.874,4 2,4z M13.496,4L10,4 9.992,4 8.992,2 1.5,2C1.225,2,1,2.224,1,2.5L1,12.5C1,12.776,1.225,13,1.5,13L13.496,13C13.773,13,13.996,12.776,13.996,12.5L13.996,4.5C13.996,4.224,13.773,4,13.496,4" />
<GeometryDrawing Brush="#FFEFEFF0" Geometry="F1M2,3L8.374,3 8.874,4 2,4z" />
</DrawingGroup>
<DrawingGroup x:Key="Icon_File">
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M4,15C3.03,15,2,14.299,2,13L2,3C2,1.701,3.03,1,4,1L10.061,1 14,4.556 14,13C14,13.97,13.299,15,12,15z" />
<GeometryDrawing Brush="#FF9B4E96" Geometry="F1M12,13L4,13 4,3 9,3 9,6 12,6z M9.641,2L3.964,2C3.964,2,3,2,3,3L3,13C3,14,3.964,14,3.964,14L11.965,14C12.965,14,13,13,13,13L13,5z" />
<GeometryDrawing Brush="#FFF0EFF1" Geometry="F1M4,3L9,3 9,6 12,6 12,13 4,13z" />
</DrawingGroup>
<DrawingGroup x:Key="Icon_Volume">
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M0,12L0,6.5C0,5.122,1.122,4,2.5,4L13.5,4C14.879,4,16,5.122,16,6.5L16,12z" />
<GeometryDrawing Brush="#FFEFEFF0" Geometry="F1M13,8L12,8 12,7 13,7z M11,8L10,8 10,7 11,7z M13.5,6L2.5,6C2.224,6,2,6.224,2,6.5L2,10 14,10 14,6.5C14,6.224,13.775,6,13.5,6" />
<GeometryDrawing Brush="#FF424242" Geometry="F1M13,7L12,7 12,8 13,8z M11,7L10,7 10,8 11,8z M2,10L14,10 14,6.5C14,6.224,13.775,6,13.5,6L2.5,6C2.224,6,2,6.224,2,6.5z M15,11L1,11 1,6.5C1,5.673,1.673,5,2.5,5L13.5,5C14.327,5,15,5.673,15,6.5z" />
</DrawingGroup>
</dialogs:ResourceSelectorConverter>
</ResourceDictionary>
</Style.Resources>
<Setter Property="Template">
<ControlTemplate>
<DockPanel>
<DockPanel DockPanel.Dock="Top" Margin="0 0 0 5">
<dialogs:ChildFitter DockPanel.Dock="Right" Width="{Binding ElementName=Location, Path=Bounds.Height}">
<Button Command="{Binding GoUp}">
<DrawingPresenter Drawing="{StaticResource LevelUp}" Stretch="Fill"/>
</Button>
</dialogs:ChildFitter>
<TextBox x:Name="Location" Text="{Binding Location}" Margin="0 0 5 0">
<TextBox.KeyBindings>
<KeyBinding Command="{Binding EnterPressed}" Gesture="Enter"/>
</TextBox.KeyBindings>
</TextBox>
</DockPanel>
<DockPanel Margin="0 5 0 0" DockPanel.Dock="Bottom">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
<CheckBox IsChecked="{Binding ShowHiddenFiles}">
<TextBlock>Show hidden files</TextBlock>
</CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Margin" Value="4"/>
</Style>
</StackPanel.Styles>
<Button Command="{Binding Ok}">OK</Button>
<Button Command="{Binding Cancel}">Cancel</Button>
</StackPanel>
</DockPanel>
<ComboBox DockPanel.Dock="Bottom"
IsVisible="{Binding ShowFilters}"
Items="{Binding Filters}"
SelectedItem="{Binding SelectedFilter}"
Margin="0 5 0 0" />
<TextBox Text="{Binding FileName}" Watermark="File name" DockPanel.Dock="Bottom" IsVisible="{Binding !SelectingFolder}" />
<ListBox Margin="0 0 5 5" BorderBrush="Transparent" x:Name="QuickLinks" Items="{Binding QuickLinks}"
SelectedIndex="{Binding QuickLinksSelectedIndex}"
DockPanel.Dock="Left" Focusable="False">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Spacing="4" Orientation="Horizontal" Background="Transparent">
<DrawingPresenter Width="16" Height="16" Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/>
<TextBlock Text="{Binding DisplayName}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<DockPanel Grid.IsSharedSizeScope="True">
<Grid DockPanel.Dock="Top" Margin="15 5 0 0" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" SharedSizeGroup="Icon" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="400" SharedSizeGroup="Name" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Modified" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="150" SharedSizeGroup="Type" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Size" />
</Grid.ColumnDefinitions>
<GridSplitter Grid.Column="1" />
<TextBlock Grid.Column="2" Text="Name" />
<GridSplitter Grid.Column="3" />
<TextBlock Grid.Column="4" Text="Date Modified" />
<GridSplitter Grid.Column="5" />
<TextBlock Grid.Column="6" Text="Type" />
<GridSplitter Grid.Column="7" />
<TextBlock Grid.Column="8" Text="Size" />
</Grid>
<ListBox x:Name="Files"
VirtualizationMode="Simple"
Items="{Binding Items}"
Margin="0 5"
SelectionMode="{Binding SelectionMode}"
SelectedItems="{Binding SelectedItems}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Icon" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Name" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Modified" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Type" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Size" />
</Grid.ColumnDefinitions>
<DrawingPresenter Width="16" Height="16" Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/>
<TextBlock Grid.Column="2" Text="{Binding DisplayName}"/>
<TextBlock Grid.Column="4" Text="{Binding Modified}" />
<TextBlock Grid.Column="6" Text="{Binding Type}" />
<TextBlock Grid.Column="8">
<TextBlock.Text>
<Binding Path="Size">
<Binding.Converter>
<dialogs:FileSizeStringConverter/>
</Binding.Converter>
</Binding>
</TextBlock.Text>
</TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</DockPanel>
</ControlTemplate>
</Setter>
</Style>
</Styles>

1
src/Avalonia.Themes.Default/TextBox.xaml

@ -10,7 +10,6 @@
<MenuItem x:Name="TextBoxContextFlyoutPasteItem" Header="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" InputGesture="{x:Static TextBox.PasteGesture}"/>
</MenuFlyout>
<!-- ContextMenu obsolete, prefer ContextFlyout -->
<ContextMenu x:Key="DefaultTextBoxContextMenu" x:Name="TextBoxContextMenu">
<MenuItem x:Name="TextBoxContextMenuCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
<MenuItem x:Name="TextBoxContextMenuCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>

3
src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj

@ -11,7 +11,8 @@
<ProjectReference Include="..\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
<ProjectReference Include="..\Avalonia.Layout\Avalonia.Layout.csproj" />
<ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
<ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<AvaloniaResource Include="**/*.xaml" />
<AvaloniaResource Include="Assets\*" />
</ItemGroup>

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

@ -61,4 +61,6 @@
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/TimePicker.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml"/>
<!-- ManagedFileChooser comes last because it uses (and overrides) styles for a multitude of other controls...the dialogs were originally UserControls, after all-->
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml"/>
</Styles>

324
src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml

@ -0,0 +1,324 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dialogs="clr-namespace:Avalonia.Dialogs;assembly=Avalonia.Dialogs">
<Style Selector="dialogs|ManagedFileChooser">
<Style.Resources>
<ResourceDictionary>
<GradientStops x:Key="IconRes.FolderBackGradientStops">
</GradientStops>
<GradientStops x:Key="IconRes.FolderFrontGradientStops">
<GradientStop Offset="0" Color="#FFFFDA6F"/>
<GradientStop Offset="1" Color="#FFFEC326"/>
</GradientStops>
<DrawingGroup x:Key="LevelUp">
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M14.5,0L6.39,0 5.39,2 2.504,2C1.677,2,1,2.673,1,3.5L1,10.582 1,10.586 1,15.414 3,13.414 3,16 7,16 7,13.414 9,15.414 9,13 14.5,13C15.327,13,16,12.327,16,11.5L16,1.5C16,0.673,15.327,0,14.5,0" />
<GeometryDrawing Brush="#FFDCB679" Geometry="F1M14,3L7.508,3 8.008,2 8.012,2 14,2z M14.5,1L7.008,1 6.008,3 2.504,3C2.227,3,2,3.224,2,3.5L2,9.582 4.998,6.586 9,10.586 9,12 14.5,12C14.775,12,15,11.776,15,11.5L15,1.5C15,1.224,14.775,1,14.5,1" />
<GeometryDrawing Brush="#FF00529C" Geometry="F1M8,11L5,8 2,11 2,13 4,11 4,15 6,15 6,11 8,13z" />
<GeometryDrawing Brush="#FFF0EFF1" Geometry="F1M8.0001,1.9996L7.5001,3.0006 14.0001,3.0006 14.0001,1.9996z" />
</DrawingGroup>
<dialogs:ResourceSelectorConverter x:Key="Icons">
<DrawingGroup x:Key="Icon_Folder">
<GeometryDrawing Geometry="M 0 0 L 16 16"/>
<GeometryDrawing Geometry="M 0 3 C 0,1 0,1 2,1 L 5 1 C 5.5,1 6,1 6.5,1.5 L 8 3 L 14 3 C 16,3 16,3 16,5
L 16,12 C 16,14 16,14 14,14
L 2,14 C 0,14 0,14 0,12 Z">
<GeometryDrawing.Brush>
<LinearGradientBrush StartPoint="1,4" EndPoint="23,20">
<GradientStop Offset="0" Color="#FFFFC018"/>
<GradientStop Offset="1" Color="#FFDFA32D"/>
</LinearGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Geometry="M 0 4.5 L 8 4.5 L 8 9 L 0 9 Z">
<GeometryDrawing.Brush>
<LinearGradientBrush StartPoint="0,4.5" EndPoint="0,5">
<GradientStop Offset="0" Color="#00D7A018"/>
<GradientStop Offset="1" Color="#7FD7A018"/>
</LinearGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Geometry="M 0 9 C 0,5 0,5 2,5 L 5 5 C 5.5,5 6,5 6.5,4.75 L 8 4 L 14 4 C 16,4 16,4 16,6
L 16,11 C 16,13 16,13 14,13
L 2,13 C 0,13 0,13 0,11 Z">
<GeometryDrawing.Brush>
<LinearGradientBrush StartPoint="1,6" EndPoint="23,19">
<GradientStop Offset="0" Color="#FFFFE69D"/>
<GradientStop Offset="1" Color="#FFFFC937"/>
</LinearGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Geometry="M 0 9 C 0,5 0,5 2,5 L 5 5 C 5.5,5 6,5 6.5,4.75 L 8 4 L 14 4
L 8 4.25 C 6,5.25 5.5,5.25 5.125,5.25 L 2 5.25 C 0,5.25 0,5.25 0,9.25 z" Brush="#7FFFFFFF"/>
</DrawingGroup>
<DrawingGroup x:Key="Icon_File">
<GeometryDrawing Geometry="M 0 0 L 16 16"/>
<GeometryDrawing Geometry="M 2 0 L 10 0 L 14 4 L 14 16 L 2 16 Z" Brush="#FF797774"/>
<GeometryDrawing Geometry="M 3 1 L 9.7 1 L 13 4.3 L 13 15 L 3 15 Z" Brush="#FFFAFAFA"/>
<GeometryDrawing Geometry="L 9 1 L 9 5 L 14 5 L 14 4 L 10 4 L 10 1 Z" Brush="#FF797774"/>
</DrawingGroup>
<DrawingGroup x:Key="Icon_Volume">
<GeometryDrawing Geometry="M 0 0 L 16 16"/>
<GeometryDrawing Geometry="M 4 5 L 12 5 L 14.5 7.5 C 15,8 15,8 15,9 L 1 9 C 1,8 1,8 1.5 7.5 Z" Brush="#FFE1E3E6"/>
<GeometryDrawing Geometry="M 12 5 L 14.5 7.5 C 15,8 15,8 15,9 L 10 9 L 10 5 Z">
<GeometryDrawing.Brush>
<LinearGradientBrush StartPoint="12,5" EndPoint="11.5,5.5">
<GradientStop Offset="0" Color="#FFCDCFD1"/>
<GradientStop Offset="1" Color="#00CDCFD1"/>
</LinearGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Geometry="M 4 5 L 1.5 7.5 C 1,8 1,8 1,9 L 4 9 L 6 9 L 6 5 Z">
<GeometryDrawing.Brush>
<LinearGradientBrush StartPoint="4,5" EndPoint="4.5,5.5">
<GradientStop Offset="0" Color="#FFCDCFD1"/>
<GradientStop Offset="1" Color="#00CDCFD1"/>
</LinearGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Geometry="M 1 9 C 1,8 1,8 2,8 L 14 8
C 15,8 15,8 15,9 L 15 11
C 15,12 15,12 14,12 L 2 12
C 1,12 1,12 1,11 Z">
<GeometryDrawing.Brush>
<LinearGradientBrush StartPoint="0,8" EndPoint="0,12">
<GradientStop Offset="0" Color="#FF737374"/>
<GradientStop Offset="1" Color="#FFA8A8A8"/>
</LinearGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Geometry="M 2 9 C 2,8 2,8 3,8 L 13 8
C 14,8 14,8 14,9 L 14 10
C 14,11 14,11 13,11 L 3 11
C 2,11 2,11 2,10 Z">
<GeometryDrawing.Brush>
<LinearGradientBrush StartPoint="0,8" EndPoint="0,11">
<GradientStop Offset="0" Color="#FF333333"/>
<GradientStop Offset="1" Color="#FF5A5A5A"/>
</LinearGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing>
<GeometryDrawing.Geometry>
<EllipseGeometry Rect="2.5,8.5,2,2"/>
</GeometryDrawing.Geometry>
<GeometryDrawing.Brush>
<RadialGradientBrush GradientOrigin="3.5,9.5" Center="3.5,9.5">
<GradientStop Offset="0.8" Color="#4001FF01"/>
<GradientStop Offset="1" Color="#0001FF01"/>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing>
<GeometryDrawing.Geometry>
<EllipseGeometry Rect="3,9,1,1"/>
</GeometryDrawing.Geometry>
<GeometryDrawing.Brush>
<RadialGradientBrush GradientOrigin="3.5,9.5" Center="3.25,9.75">
<GradientStop Offset="0" Color="#FFB6FFB6"/>
<GradientStop Offset="1" Color="#FF01FF01"/>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Geometry="M 3.23483495705 9.76516504295 A 0.375,0.375 180 1 0 3.76516504295,9.23483495705 A 0.4375,0.4375 135 0 1 3.23483495705,9.76516504295 Z" Brush="#FF00B300"/>
</DrawingGroup>
</dialogs:ResourceSelectorConverter>
</ResourceDictionary>
</Style.Resources>
<Setter Property="Template">
<ControlTemplate>
<DockPanel>
<ListBox x:Name="QuickLinks" DockPanel.Dock="Left" Items="{Binding QuickLinks}" SelectedIndex="{Binding QuickLinksSelectedIndex}" Focusable="False">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Spacing="4" Orientation="Horizontal" Background="Transparent">
<DrawingPresenter Width="16" Height="16" Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/>
<TextBlock Text="{Binding DisplayName}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<DockPanel x:Name="NavBar" DockPanel.Dock="Top" Margin="8,5,8,0" VerticalAlignment="Center">
<Rectangle Fill="{DynamicResource SystemControlHighlightAltBaseMediumLowBrush}" Height="1" Margin="0,5,0,0" DockPanel.Dock="Bottom"/>
<DockPanel Margin="4,0">
<Button Command="{Binding GoUp}" DockPanel.Dock="Left">
<Path Data="M 0 7 L 7 0 L 14 7 M 7 0 L 7 16" Stroke="{Binding $parent[Button].Foreground}" StrokeThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,1,0,-1"/>
</Button>
<TextBox x:Name="Location" Text="{Binding Location}">
<TextBox.KeyBindings>
<KeyBinding Command="{Binding EnterPressed}" Gesture="Enter"/>
</TextBox.KeyBindings>
</TextBox>
</DockPanel>
</DockPanel>
<DockPanel Margin="8,0,8,5" DockPanel.Dock="Bottom">
<Rectangle Fill="{DynamicResource SystemControlHighlightAltBaseMediumLowBrush}" Height="1" Margin="0,0,0,5" DockPanel.Dock="Top"/>
<DockPanel Margin="4,0">
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,4">
<ComboBox DockPanel.Dock="Right"
IsVisible="{Binding ShowFilters}"
Items="{Binding Filters}"
SelectedItem="{Binding SelectedFilter}" />
<TextBox Text="{Binding FileName}" Watermark="File name" IsVisible="{Binding !SelectingFolder}" />
</DockPanel>
<CheckBox IsChecked="{Binding ShowHiddenFiles}" Content="Show hidden files" DockPanel.Dock="Left"/>
<UniformGrid x:Name="Finalize" HorizontalAlignment="Right" Rows="1">
<Button Command="{Binding Ok}">OK</Button>
<Button Command="{Binding Cancel}">Cancel</Button>
</UniformGrid>
</DockPanel>
</DockPanel>
<DockPanel Grid.IsSharedSizeScope="True">
<Grid DockPanel.Dock="Top" Margin="15 5 0 0" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" SharedSizeGroup="Icon" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="275" SharedSizeGroup="Name" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Modified" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="150" SharedSizeGroup="Type" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Size" />
</Grid.ColumnDefinitions>
<Grid.Styles>
<Style Selector="GridSplitter">
<Setter Property="Background" Value="{DynamicResource SystemControlHighlightAltBaseMediumLowBrush}"/>
<Setter Property="Template">
<ControlTemplate>
<Border VerticalAlignment="Stretch" BorderThickness="0" Background="#01000000">
<Rectangle Width="1" VerticalAlignment="Stretch" Fill="{TemplateBinding Background}"/>
</Border>
</ControlTemplate>
</Setter>
</Style>
</Grid.Styles>
<GridSplitter Grid.Column="1" />
<TextBlock Grid.Column="2" Text="Name" />
<GridSplitter Grid.Column="3" />
<TextBlock Grid.Column="4" Text="Date Modified" />
<GridSplitter Grid.Column="5" />
<TextBlock Grid.Column="6" Text="Type" />
<GridSplitter Grid.Column="7" />
<TextBlock Grid.Column="8" Text="Size" />
</Grid>
<ListBox x:Name="Files"
VirtualizationMode="Simple"
Items="{Binding Items}"
Margin="0 5"
SelectionMode="{Binding SelectionMode}"
SelectedItems="{Binding SelectedItems}"
Background="Transparent"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Icon" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Name" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Modified" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Type" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Size" />
</Grid.ColumnDefinitions>
<DrawingPresenter Width="16" Height="16" Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/>
<TextBlock Grid.Column="2" Text="{Binding DisplayName}"/>
<TextBlock Grid.Column="4" Text="{Binding Modified}" />
<TextBlock Grid.Column="6" Text="{Binding Type}" />
<TextBlock Grid.Column="8">
<TextBlock.Text>
<Binding Path="Size">
<Binding.Converter>
<dialogs:FileSizeStringConverter/>
</Binding.Converter>
</Binding>
</TextBlock.Text>
</TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</DockPanel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="dialogs|ManagedFileChooser /template/ ListBox#QuickLinks">
<Setter Property="Margin" Value="0"/>
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundChromeMediumBrush}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Width" Value="240"/>
<Setter Property="Padding" Value="0,20"/>
<Setter Property="Template">
<ControlTemplate>
<Border Name="border" BoxShadow="inset -6 0 3 -3 #20000000" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer Name="PART_ScrollViewer" HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{TemplateBinding Padding}"
VirtualizationMode="{TemplateBinding VirtualizationMode}"/>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="dialogs|ManagedFileChooser /template/ ListBox#QuickLinks ListBoxItem">
<Setter Property="Height" Value="32"/>
<Setter Property="Padding" Value="30,6"/>
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="LayoutRoot" CornerRadius="2" Margin="10,0">
<Panel>
<Border x:Name="SelectedLine" HorizontalAlignment="Left" Margin="2,6" CornerRadius="0.5" Width="3" Background="{DynamicResource SystemControlHighlightAccentBrush}" IsVisible="{TemplateBinding IsSelected}"/>
<ContentPresenter Name="PART_ContentPresenter"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="0"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"/>
</Panel>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="dialogs|ManagedFileChooser /template/ ListBox#QuickLinks ListBoxItem:pointerover /template/ Border#LayoutRoot">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAltMediumBrush}"/>
</Style>
<Style Selector="dialogs|ManagedFileChooser /template/ ListBox#QuickLinks ListBoxItem:selected /template/ Border#LayoutRoot">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAltMediumHighBrush}"/>
</Style>
<Style Selector="dialogs|ManagedFileChooser /template/ DockPanel#NavBar Button,
dialogs|ManagedFileChooser /template/ DockPanel#NavBar TextBox">
<Setter Property="Height" Value="30"/>
</Style>
<Style Selector="dialogs|ManagedFileChooser /template/ DockPanel#NavBar Button">
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="Width" Value="40"/>
<Setter Property="Margin" Value="0,0,8,0"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="dialogs|ManagedFileChooser /template/ DockPanel#NavBar Button:not(:pointerover):not(:pressed)">
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style Selector="dialogs|ManagedFileChooser /template/ UniformGrid#Finalize > Button">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="4,0,0,0"/>
</Style>
<Style Selector="dialogs|ManagedFileChooser /template/ UniformGrid#Finalize > Button /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</Style>
</Styles>

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

@ -21,7 +21,6 @@
<MenuItem x:Name="TextBoxContextFlyoutPasteItem" Header="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" InputGesture="{x:Static TextBox.PasteGesture}"/>
</MenuFlyout>
<!-- ContextMenu obsolete, prefer ContextFlyout -->
<ContextMenu x:Key="DefaultTextBoxContextMenu" x:Name="TextBoxContextMenu">
<MenuItem x:Name="TextBoxContextMenuCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
<MenuItem x:Name="TextBoxContextMenuCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>

58
src/Avalonia.Visuals/ApiCompatBaseline.txt

@ -1,4 +1,58 @@
Compat issues with assembly Avalonia.Visuals:
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract.
CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextFormatting.TextRunProperties Avalonia.Media.TextFormatting.GenericTextParagraphProperties.DefaultTextRunProperties' is non-virtual in the implementation but is virtual in the contract.
CannotMakeMemberNonVirtual : Member 'public System.Double Avalonia.Media.TextFormatting.GenericTextParagraphProperties.LineHeight' is non-virtual in the implementation but is virtual in the contract.
CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextAlignment Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextAlignment' is non-virtual in the implementation but is virtual in the contract.
CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextWrapping Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextWrapping' is non-virtual in the implementation but is virtual in the contract.
CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextFormatting.TextRunProperties Avalonia.Media.TextFormatting.GenericTextParagraphProperties.DefaultTextRunProperties.get()' is non-virtual in the implementation but is virtual in the contract.
CannotMakeMemberNonVirtual : Member 'public System.Double Avalonia.Media.TextFormatting.GenericTextParagraphProperties.LineHeight.get()' is non-virtual in the implementation but is virtual in the contract.
CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextAlignment Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextAlignment.get()' is non-virtual in the implementation but is virtual in the contract.
CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextWrapping Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextWrapping.get()' is non-virtual in the implementation but is virtual in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.GenericTextRunProperties..ctor(Avalonia.Media.Typeface, System.Double, Avalonia.Media.TextDecorationCollection, Avalonia.Media.IBrush, Avalonia.Media.IBrush, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextEndOfLine..ctor()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Baseline' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Height' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.NewLineLength' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangAfter' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangLeading' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangTrailing' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Start' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.TrailingWhitespaceLength' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Width' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.WidthIncludingTrailingWhitespace' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Baseline.get()' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Height.get()' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineMetrics Avalonia.Media.TextFormatting.TextLine.LineMetrics.get()' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.NewLineLength.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangAfter.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangLeading.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangTrailing.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Start.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.TrailingWhitespaceLength.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Width.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.WidthIncludingTrailingWhitespace.get()' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLineMetrics..ctor(Avalonia.Size, System.Double, Avalonia.Media.TextFormatting.TextRange, System.Boolean)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineMetrics Avalonia.Media.TextFormatting.TextLineMetrics.Create(System.Collections.Generic.IEnumerable<Avalonia.Media.TextFormatting.TextRun>, Avalonia.Media.TextFormatting.TextRange, System.Double, Avalonia.Media.TextFormatting.TextParagraphProperties)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.TextLineMetrics.Size.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextRange Avalonia.Media.TextFormatting.TextLineMetrics.TextRange.get()' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextParagraphProperties.FirstLineInParagraph' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.FlowDirection Avalonia.Media.TextFormatting.TextParagraphProperties.FlowDirection' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextParagraphProperties.Indent' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextParagraphProperties.FirstLineInParagraph.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.FlowDirection Avalonia.Media.TextFormatting.TextParagraphProperties.FlowDirection.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextParagraphProperties.Indent.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment.get()' is abstract in the implementation but is missing in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PopBitmapBlendMode()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PushBitmapBlendMode(Avalonia.Visuals.Media.Imaging.BitmapBlendingMode)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength' is present in the implementation but not in the contract.
@ -6,6 +60,10 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avaloni
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAndTangentAtDistance(System.Double, Avalonia.Point, Avalonia.Point)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAtDistance(System.Double, Avalonia.Point)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetSegment(System.Double, System.Double, System.Boolean, Avalonia.Platform.IGeometryImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' does not exist in the implementation but it does exist in the contract.
Total Issues: 64
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.IO.Stream)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract.

32
src/Avalonia.Visuals/Media/BaselineAlignment.cs

@ -0,0 +1,32 @@
namespace Avalonia.Media
{
/// <summary>
/// Enum specifying where a box should be positioned Vertically
/// </summary>
public enum BaselineAlignment
{
/// <summary>Align top toward top of container</summary>
Top,
/// <summary>Center vertically</summary>
Center,
/// <summary>Align bottom toward bottom of container</summary>
Bottom,
/// <summary>Align at baseline</summary>
Baseline,
/// <summary>Align toward text's top of container</summary>
TextTop,
/// <summary>Align toward text's bottom of container</summary>
TextBottom,
/// <summary>Align baseline to subscript position of container</summary>
Subscript,
/// <summary>Align baseline to superscript position of container</summary>
Superscript,
}
}

25
src/Avalonia.Visuals/Media/FlowDirection.cs

@ -0,0 +1,25 @@
namespace Avalonia.Media
{
/// <summary>
/// The 'flow-direction' property specifies whether the primary text advance
/// direction shall be left-to-right or right-to-left.
/// </summary>
public enum FlowDirection
{
/// <internalonly>
/// Sets the primary text advance direction to left-to-right, and the line
/// progression direction to top-to-bottom as is common in most Roman-based
/// documents. For most characters, the current text position is advanced
/// from left to right after each glyph is rendered. The 'direction' property
/// is set to 'ltr'.
/// </internalonly>
LeftToRight,
/// <internalonly>
/// Sets the primary text advance direction to right-to-left, and the line
/// progression direction to top-to-bottom as is common in Arabic or Hebrew
/// scripts. The direction property is set to 'rtl'.
/// </internalonly>
RightToLeft
}
}

212
src/Avalonia.Visuals/Media/GlyphRun.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -16,9 +17,9 @@ namespace Avalonia.Media
private IGlyphRunImpl _glyphRunImpl;
private GlyphTypeface _glyphTypeface;
private double _fontRenderingEmSize;
private Size? _size;
private int _biDiLevel;
private Point? _baselineOrigin;
private GlyphRunMetrics? _glyphRunMetrics;
private ReadOnlySlice<ushort> _glyphIndices;
private ReadOnlySlice<double> _glyphAdvances;
@ -90,6 +91,24 @@ namespace Avalonia.Media
set => Set(ref _fontRenderingEmSize, value);
}
/// <summary>
/// Gets or sets the conservative bounding box of the <see cref="GlyphRun"/>.
/// </summary>
public Size Size => new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height);
/// <summary>
///
/// </summary>
public GlyphRunMetrics Metrics
{
get
{
_glyphRunMetrics ??= CreateGlyphRunMetrics();
return _glyphRunMetrics.Value;
}
}
/// <summary>
/// Gets or sets the baseline origin of the<see cref="GlyphRun"/>.
/// </summary>
@ -168,19 +187,6 @@ namespace Avalonia.Media
/// </summary>
public bool IsLeftToRight => ((BiDiLevel & 1) == 0);
/// <summary>
/// Gets or sets the conservative bounding box of the <see cref="GlyphRun"/>.
/// </summary>
public Size Size
{
get
{
_size ??= CalculateSize();
return _size.Value;
}
}
/// <summary>
/// The platform implementation of the <see cref="GlyphRun"/>.
/// </summary>
@ -232,16 +238,7 @@ namespace Avalonia.Media
for (var i = 0; i < glyphIndex; i++)
{
if (GlyphAdvances.IsEmpty)
{
var glyph = GlyphIndices[i];
distance += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
else
{
distance += GlyphAdvances[i];
}
distance += GetGlyphAdvance(i);
}
return distance;
@ -282,42 +279,20 @@ namespace Avalonia.Media
var currentX = 0.0;
var index = 0;
if (GlyphTypeface.IsFixedPitch)
for (; index < GlyphIndices.Length - Metrics.NewlineLength; index++)
{
var glyph = GlyphIndices[index];
var advance = GetGlyphAdvance(index);
var advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
index = Math.Min(GlyphIndices.Length - 1,
(int)Math.Round(distance / advance, MidpointRounding.AwayFromZero));
}
else
{
for (; index < GlyphIndices.Length; index++)
if (currentX + advance >= distance)
{
double advance;
if (GlyphAdvances.IsEmpty)
{
var glyph = GlyphIndices[index];
advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
else
{
advance = GlyphAdvances[index];
}
if (currentX + advance >= distance)
{
break;
}
currentX += advance;
break;
}
currentX += advance;
}
var characterHit = FindNearestCharacterHit(GlyphClusters.IsEmpty ? index : GlyphClusters[index], out var width);
var characterHit =
FindNearestCharacterHit(GlyphClusters.IsEmpty ? index : GlyphClusters[index], out var width);
var offset = GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex));
@ -343,7 +318,8 @@ namespace Avalonia.Media
return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
}
var nextCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
var nextCharacterHit =
FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
return new CharacterHit(nextCharacterHit.FirstCharacterIndex);
}
@ -368,14 +344,6 @@ namespace Avalonia.Media
FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
}
private class ReverseComparer<T> : IComparer<T>
{
public int Compare(T x, T y)
{
return Comparer<T>.Default.Compare(y, x);
}
}
/// <summary>
/// Finds a glyph index for given character index.
/// </summary>
@ -472,7 +440,7 @@ namespace Avalonia.Media
if (GlyphClusters.IsEmpty)
{
width = GetGlyphWidth(index);
width = GetGlyphAdvance(index);
return new CharacterHit(start, 1);
}
@ -485,7 +453,7 @@ namespace Avalonia.Media
while (nextCluster == cluster)
{
width += GetGlyphWidth(currentIndex);
width += GetGlyphAdvance(currentIndex);
if (IsLeftToRight)
{
@ -528,16 +496,16 @@ namespace Avalonia.Media
/// </summary>
/// <param name="index">The glyph index.</param>
/// <returns>The glyph's width.</returns>
private double GetGlyphWidth(int index)
private double GetGlyphAdvance(int index)
{
if (GlyphAdvances.IsEmpty)
if (!GlyphAdvances.IsEmpty)
{
var glyph = GlyphIndices[index];
return GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
return GlyphAdvances[index];
}
return GlyphAdvances[index];
var glyph = GlyphIndices[index];
return GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
/// <summary>
@ -549,34 +517,90 @@ namespace Avalonia.Media
return new Point(0, -GlyphTypeface.Ascent * Scale);
}
/// <summary>
/// Calculates the size of the <see cref="GlyphRun"/>.
/// </summary>
/// <returns>
/// The calculated bounds.
/// </returns>
private Size CalculateSize()
private GlyphRunMetrics CreateGlyphRunMetrics()
{
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
var width = 0.0;
var widthIncludingTrailingWhitespace = 0d;
var width = 0d;
var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength);
if (GlyphAdvances.IsEmpty)
for (var index = 0; index < _glyphIndices.Length; index++)
{
foreach (var glyph in GlyphIndices)
var advance = GetGlyphAdvance(index);
widthIncludingTrailingWhitespace += advance;
if (index > _glyphIndices.Length - 1 - trailingWhitespaceLength)
{
width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
continue;
}
width += advance;
}
return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength,
height);
}
private int GetTrailingWhitespaceLength(out int newLineLength)
{
newLineLength = 0;
if (_characters.IsEmpty)
{
return 0;
}
var trailingWhitespaceLength = 0;
if (_glyphClusters.IsEmpty)
{
for (var i = _characters.Length - 1; i >= 0;)
{
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (!codepoint.IsWhiteSpace)
{
break;
}
if (codepoint.IsBreakChar)
{
newLineLength++;
}
trailingWhitespaceLength++;
i -= count;
}
}
else
{
foreach (var advance in GlyphAdvances)
for (var i = _glyphClusters.Length - 1; i >= 0; i--)
{
width += advance;
var cluster = _glyphClusters[i];
var codepointIndex = cluster - _characters.Start;
var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);
if (!codepoint.IsWhiteSpace)
{
break;
}
if (codepoint.IsBreakChar)
{
newLineLength++;
}
trailingWhitespaceLength++;
}
}
return new Size(width, height);
return trailingWhitespaceLength;
}
private void Set<T>(ref T field, T value)
@ -586,6 +610,10 @@ namespace Avalonia.Media
throw new InvalidOperationException("GlyphRun can't be changed after it has been initialized.'");
}
_glyphRunMetrics = null;
_baselineOrigin = null;
field = value;
}
@ -613,16 +641,20 @@ namespace Avalonia.Media
var platformRenderInterface = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width);
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
_size = new Size(width, height);
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(this);
}
void IDisposable.Dispose()
{
_glyphRunImpl?.Dispose();
}
private class ReverseComparer<T> : IComparer<T>
{
public int Compare(T x, T y)
{
return Comparer<T>.Default.Compare(y, x);
}
}
}
}

25
src/Avalonia.Visuals/Media/GlyphRunMetrics.cs

@ -0,0 +1,25 @@
namespace Avalonia.Media
{
public readonly struct GlyphRunMetrics
{
public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, int trailingWhitespaceLength,
int newlineLength, double height)
{
Width = width;
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
TrailingWhitespaceLength = trailingWhitespaceLength;
NewlineLength = newlineLength;
Height = height;
}
public double Width { get; }
public double WidthIncludingTrailingWhitespace { get; }
public int TrailingWhitespaceLength { get; }
public int NewlineLength { get; }
public double Height { get; }
}
}

2
src/Avalonia.Visuals/Media/GlyphTypeface.cs

@ -5,6 +5,8 @@ namespace Avalonia.Media
{
public sealed class GlyphTypeface : IDisposable
{
public const int InvisibleGlyph = 3;
public GlyphTypeface(Typeface typeface)
: this(FontManager.Current?.PlatformImpl.CreateGlyphTypeface(typeface))
{

3
src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs

@ -14,6 +14,7 @@
/// Draws the <see cref="DrawableTextRun"/> at the given origin.
/// </summary>
/// <param name="drawingContext">The drawing context.</param>
public abstract void Draw(DrawingContext drawingContext);
/// <param name="origin">The origin.</param>
public abstract void Draw(DrawingContext drawingContext, Point origin);
}
}

144
src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs

@ -1,33 +1,144 @@
namespace Avalonia.Media.TextFormatting
{
public class GenericTextParagraphProperties : TextParagraphProperties
/// <summary>
/// Generic implementation of TextParagraphProperties
/// </summary>
public sealed class GenericTextParagraphProperties : TextParagraphProperties
{
private FlowDirection _flowDirection;
private TextAlignment _textAlignment;
private TextWrapping _textWrapping;
private TextWrapping _textWrap;
private double _lineHeight;
public GenericTextParagraphProperties(
TextRunProperties defaultTextRunProperties,
/// <summary>
/// Constructing TextParagraphProperties
/// </summary>
/// <param name="defaultTextRunProperties">default paragraph's default run properties</param>
/// <param name="textAlignment">logical horizontal alignment</param>
/// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param>
public GenericTextParagraphProperties(TextRunProperties defaultTextRunProperties,
TextAlignment textAlignment = TextAlignment.Left,
TextWrapping textWrapping = TextWrapping.NoWrap,
TextWrapping textWrap = TextWrapping.NoWrap,
double lineHeight = 0)
{
DefaultTextRunProperties = defaultTextRunProperties;
_textAlignment = textAlignment;
_textWrap = textWrap;
_lineHeight = lineHeight;
}
/// <summary>
/// Constructing TextParagraphProperties
/// </summary>
/// <param name="flowDirection">text flow direction</param>
/// <param name="textAlignment">logical horizontal alignment</param>
/// <param name="firstLineInParagraph">true if the paragraph is the first line in the paragraph</param>
/// <param name="alwaysCollapsible">true if the line is always collapsible</param>
/// <param name="defaultTextRunProperties">default paragraph's default run properties</param>
/// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param>
/// <param name="indent">line indentation</param>
public GenericTextParagraphProperties(
FlowDirection flowDirection,
TextAlignment textAlignment,
bool firstLineInParagraph,
bool alwaysCollapsible,
TextRunProperties defaultTextRunProperties,
TextWrapping textWrap,
double lineHeight,
double indent
)
{
_flowDirection = flowDirection;
_textAlignment = textAlignment;
FirstLineInParagraph = firstLineInParagraph;
AlwaysCollapsible = alwaysCollapsible;
DefaultTextRunProperties = defaultTextRunProperties;
_textWrap = textWrap;
_lineHeight = lineHeight;
Indent = indent;
}
_textWrapping = textWrapping;
/// <summary>
/// Constructing TextParagraphProperties from another one
/// </summary>
/// <param name="textParagraphProperties">source line props</param>
public GenericTextParagraphProperties(TextParagraphProperties textParagraphProperties)
: this(textParagraphProperties.FlowDirection,
textParagraphProperties.TextAlignment,
textParagraphProperties.FirstLineInParagraph,
textParagraphProperties.AlwaysCollapsible,
textParagraphProperties.DefaultTextRunProperties,
textParagraphProperties.TextWrapping,
textParagraphProperties.LineHeight,
textParagraphProperties.Indent)
{
}
_lineHeight = lineHeight;
/// <summary>
/// This property specifies whether the primary text advance
/// direction shall be left-to-right, right-to-left, or top-to-bottom.
/// </summary>
public override FlowDirection FlowDirection
{
get { return _flowDirection; }
}
/// <summary>
/// This property describes how inline content of a block is aligned.
/// </summary>
public override TextAlignment TextAlignment
{
get { return _textAlignment; }
}
/// <summary>
/// Paragraph's line height
/// </summary>
public override double LineHeight
{
get { return _lineHeight; }
}
/// <summary>
/// Indicates the first line of the paragraph.
/// </summary>
public override bool FirstLineInParagraph { get; }
/// <summary>
/// If true, the formatted line may always be collapsed. If false (the default),
/// only lines that overflow the paragraph width are collapsed.
/// </summary>
public override bool AlwaysCollapsible { get; }
/// <summary>
/// Paragraph's default run properties
/// </summary>
public override TextRunProperties DefaultTextRunProperties { get; }
public override TextAlignment TextAlignment => _textAlignment;
/// <summary>
/// This property controls whether or not text wraps when it reaches the flow edge
/// of its containing block box
/// </summary>
public override TextWrapping TextWrapping
{
get { return _textWrap; }
}
/// <summary>
/// Line indentation
/// </summary>
public override double Indent { get; }
public override TextWrapping TextWrapping => _textWrapping;
/// <summary>
/// Set text flow direction
/// </summary>
internal void SetFlowDirection(FlowDirection flowDirection)
{
_flowDirection = flowDirection;
}
public override double LineHeight => _lineHeight;
/// <summary>
/// Set text alignment
@ -37,20 +148,21 @@
_textAlignment = textAlignment;
}
/// <summary>
/// Set text wrap
/// Set line height
/// </summary>
internal void SetTextWrapping(TextWrapping textWrapping)
internal void SetLineHeight(double lineHeight)
{
_textWrapping = textWrapping;
_lineHeight = lineHeight;
}
/// <summary>
/// Set line height
/// Set text wrap
/// </summary>
internal void SetLineHeight(double lineHeight)
internal void SetTextWrapping(TextWrapping textWrap)
{
_lineHeight = lineHeight;
_textWrap = textWrap;
}
}
}

11
src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs

@ -7,8 +7,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public class GenericTextRunProperties : TextRunProperties
{
public GenericTextRunProperties(Typeface typeface, double fontRenderingEmSize = 12,
TextDecorationCollection textDecorations = null, IBrush foregroundBrush = null, IBrush backgroundBrush = null,
private const double DefaultFontRenderingEmSize = 12;
public GenericTextRunProperties(Typeface typeface, double fontRenderingEmSize = DefaultFontRenderingEmSize,
TextDecorationCollection textDecorations = null, IBrush foregroundBrush = null,
IBrush backgroundBrush = null, BaselineAlignment baselineAlignment = BaselineAlignment.Baseline,
CultureInfo cultureInfo = null)
{
Typeface = typeface;
@ -16,6 +19,7 @@ namespace Avalonia.Media.TextFormatting
TextDecorations = textDecorations;
ForegroundBrush = foregroundBrush;
BackgroundBrush = backgroundBrush;
BaselineAlignment = baselineAlignment;
CultureInfo = cultureInfo;
}
@ -34,6 +38,9 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc />
public override IBrush BackgroundBrush { get; }
/// <inheritdoc />
public override BaselineAlignment BaselineAlignment { get; }
/// <inheritdoc />
public override CultureInfo CultureInfo { get; }
}

14
src/Avalonia.Visuals/Media/TextFormatting/LogicalDirection.cs

@ -0,0 +1,14 @@
namespace Avalonia.Media.TextFormatting
{
public enum LogicalDirection
{
/// <summary>
/// Backward, or from right to left.
/// </summary>
Backward,
/// <summary>
/// Forward, or from left to right.
/// </summary>
Forward
}
}

63
src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs

@ -45,38 +45,41 @@ namespace Avalonia.Media.TextFormatting
public GlyphRun GlyphRun { get; }
/// <inheritdoc/>
public override void Draw(DrawingContext drawingContext)
public override void Draw(DrawingContext drawingContext, Point origin)
{
if (GlyphRun.GlyphIndices.Length == 0)
using (drawingContext.PushPostTransform(Matrix.CreateTranslation(origin)))
{
return;
}
if (Properties.Typeface == default)
{
return;
}
if (Properties.ForegroundBrush == null)
{
return;
}
if (Properties.BackgroundBrush != null)
{
drawingContext.DrawRectangle(Properties.BackgroundBrush, null, new Rect(Size));
}
drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun);
if (Properties.TextDecorations == null)
{
return;
}
foreach (var textDecoration in Properties.TextDecorations)
{
textDecoration.Draw(drawingContext, this);
if (GlyphRun.GlyphIndices.Length == 0)
{
return;
}
if (Properties.Typeface == default)
{
return;
}
if (Properties.ForegroundBrush == null)
{
return;
}
if (Properties.BackgroundBrush != null)
{
drawingContext.DrawRectangle(Properties.BackgroundBrush, null, new Rect(Size));
}
drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun);
if (Properties.TextDecorations == null)
{
return;
}
foreach (var textDecoration in Properties.TextDecorations)
{
textDecoration.Draw(drawingContext, this);
}
}
}

8
src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs

@ -16,6 +16,14 @@ namespace Avalonia.Media.TextFormatting
Properties = properties;
}
public TextCharacters(ReadOnlySlice<char> text, int offsetToFirstCharacter, int length,
TextRunProperties properties)
{
Text = text.Skip(offsetToFirstCharacter).Take(length);
TextSourceLength = length;
Properties = properties;
}
/// <inheritdoc />
public override int TextSourceLength { get; }

6
src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs

@ -5,5 +5,11 @@
/// </summary>
public class TextEndOfLine : TextRun
{
public TextEndOfLine(int textSourceLength = DefaultTextSourceLength)
{
TextSourceLength = textSourceLength;
}
public override int TextSourceLength { get; }
}
}

221
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@ -12,7 +12,8 @@ namespace Avalonia.Media.TextFormatting
{
var textWrapping = paragraphProperties.TextWrapping;
var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, out var nextLineBreak);
var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak,
out var nextLineBreak);
var textRange = GetTextRange(textRuns);
@ -21,20 +22,18 @@ namespace Avalonia.Media.TextFormatting
switch (textWrapping)
{
case TextWrapping.NoWrap:
{
var textLineMetrics =
TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties);
textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak);
break;
}
{
textLine = new TextLineImpl(textRuns, textRange, paragraphWidth, paragraphProperties,
nextLineBreak);
break;
}
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties,
nextLineBreak);
break;
}
{
textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties,
nextLineBreak);
break;
}
default:
throw new ArgumentOutOfRangeException();
}
@ -51,7 +50,8 @@ namespace Avalonia.Media.TextFormatting
/// <returns>
/// <c>true</c> if characters fit into the available width; otherwise, <c>false</c>.
/// </returns>
internal static bool TryMeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth, out int count)
internal static bool TryMeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth,
out int count)
{
var glyphRun = textCharacters.GlyphRun;
@ -282,21 +282,24 @@ namespace Avalonia.Media.TextFormatting
switch (textRun)
{
case TextCharacters textCharacters:
{
var shapeableRuns = textCharacters.GetShapeableCharacters();
foreach (var run in shapeableRuns)
{
var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface,
run.Properties.FontRenderingEmSize, run.Properties.CultureInfo);
{
var shapeableRuns = textCharacters.GetShapeableCharacters();
var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties);
foreach (var run in shapeableRuns)
{
var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface,
run.Properties.FontRenderingEmSize, run.Properties.CultureInfo);
textRuns.Add(shapedCharacters);
}
var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties);
break;
textRuns.Add(shapedCharacters);
}
break;
}
case TextEndOfLine textEndOfLine:
nextLineBreak = new TextLineBreak(textEndOfLine);
break;
}
if (TryGetLineBreak(textRun, out var runLineBreak))
@ -359,107 +362,137 @@ namespace Avalonia.Media.TextFormatting
{
var availableWidth = paragraphWidth;
var currentWidth = 0.0;
var runIndex = 0;
var currentLength = 0;
var measuredLength = 0;
while (runIndex < textRuns.Count)
foreach (var currentRun in textRuns)
{
var currentRun = textRuns[runIndex];
if (currentWidth + currentRun.Size.Width > availableWidth)
{
var breakFound = false;
if (TryMeasureCharacters(currentRun, paragraphWidth - currentWidth, out var count))
{
measuredLength += count;
}
var currentBreakPosition = 0;
break;
}
if (TryMeasureCharacters(currentRun, paragraphWidth - currentWidth, out var measuredLength))
{
if (measuredLength < currentRun.Text.Length)
{
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
currentWidth += currentRun.Size.Width;
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
var nextBreakPosition = lineBreaker.Current.PositionWrap;
measuredLength += currentRun.Text.Length;
}
if (nextBreakPosition == 0 || nextBreakPosition > measuredLength)
{
break;
}
var currentLength = 0;
breakFound = lineBreaker.Current.Required ||
lineBreaker.Current.PositionWrap != currentRun.Text.Length;
var lastWrapPosition = 0;
currentBreakPosition = nextBreakPosition;
}
}
}
else
var currentPosition = 0;
if (measuredLength == 0 && paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow)
{
measuredLength = 1;
}
else
{
for (var index = 0; index < textRuns.Count; index++)
{
var currentRun = textRuns[index];
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
var breakFound = false;
while (lineBreaker.MoveNext())
{
// Make sure we wrap at least one character.
if (currentLength == 0)
if (lineBreaker.Current.Required &&
currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
{
measuredLength = 1;
breakFound = true;
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
break;
}
}
if (breakFound)
{
measuredLength = currentBreakPosition;
}
else
{
if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
if ((paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow || lastWrapPosition != 0) &&
currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
{
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(currentBreakPosition));
if (lineBreaker.MoveNext())
if (lastWrapPosition > 0)
{
measuredLength = currentBreakPosition + lineBreaker.Current.PositionWrap;
currentPosition = lastWrapPosition;
}
else
{
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
}
breakFound = true;
break;
}
}
currentLength += measuredLength;
if (currentLength + lineBreaker.Current.PositionWrap >= measuredLength)
{
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
var splitResult = SplitTextRuns(textRuns, currentLength);
if (index < textRuns.Count - 1 &&
lineBreaker.Current.PositionWrap == currentRun.Text.Length)
{
var nextRun = textRuns[index + 1];
var textLineMetrics = TextLineMetrics.Create(splitResult.First,
new TextRange(textRange.Start, currentLength), paragraphWidth, paragraphProperties);
lineBreaker = new LineBreakEnumerator(nextRun.Text);
var remainingCharacters = splitResult.Second;
if (lineBreaker.MoveNext() &&
lineBreaker.Current.PositionMeasure == 0)
{
currentPosition += lineBreaker.Current.PositionWrap;
}
}
if (currentLineBreak?.RemainingCharacters != null)
{
if (remainingCharacters != null)
{
remainingCharacters.AddRange(currentLineBreak.RemainingCharacters);
}
else
{
remainingCharacters = new List<ShapedTextCharacters>(currentLineBreak.RemainingCharacters);
breakFound = true;
break;
}
lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
}
if (!breakFound)
{
currentLength += currentRun.Text.Length;
continue;
}
var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ?
new TextLineBreak(remainingCharacters) :
null;
measuredLength = currentPosition;
return new TextLineImpl(splitResult.First, textLineMetrics, lineBreak);
break;
}
}
var splitResult = SplitTextRuns(textRuns, measuredLength);
currentWidth += currentRun.Size.Width;
textRange = new TextRange(textRange.Start, measuredLength);
currentLength += currentRun.GlyphRun.Characters.Length;
var remainingCharacters = splitResult.Second;
runIndex++;
if (currentLineBreak?.RemainingCharacters != null)
{
if (remainingCharacters != null)
{
remainingCharacters.AddRange(currentLineBreak.RemainingCharacters);
}
else
{
remainingCharacters = new List<ShapedTextCharacters>(currentLineBreak.RemainingCharacters);
}
}
return new TextLineImpl(textRuns,
TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties),
currentLineBreak?.RemainingCharacters != null ?
new TextLineBreak(currentLineBreak.RemainingCharacters) :
null);
var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ?
new TextLineBreak(remainingCharacters) :
null;
return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties,
lineBreak);
}
/// <summary>
@ -545,7 +578,7 @@ namespace Avalonia.Media.TextFormatting
_pos += Current.TextSourceLength;
return !(Current is TextEndOfLine);
return true;
}
}
}

302
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@ -115,27 +115,165 @@ namespace Avalonia.Media.TextFormatting
/// Draws the text layout.
/// </summary>
/// <param name="context">The drawing context.</param>
public void Draw(DrawingContext context)
/// <param name="origin">The origin.</param>
public void Draw(DrawingContext context, Point origin)
{
if (!TextLines.Any())
{
return;
}
var (currentX, currentY) = origin;
foreach (var textLine in TextLines)
{
textLine.Draw(context, new Point(currentX + textLine.Start, currentY));
currentY += textLine.Height;
}
}
/// <summary>
/// Get the pixel location relative to the top-left of the layout box given the text position.
/// </summary>
/// <param name="textPosition">The text position.</param>
/// <returns></returns>
public Rect HitTestTextPosition(int textPosition)
{
if (TextLines.Count == 0)
{
return new Rect();
}
if (textPosition < 0 || textPosition >= _text.Length)
{
var lastLine = TextLines[TextLines.Count - 1];
var lineX = lastLine.Width;
var lineY = Size.Height - lastLine.Height;
return new Rect(lineX, lineY, 0, lastLine.Height);
}
var currentY = 0.0;
foreach (var textLine in TextLines)
{
var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width,
_paragraphProperties.TextAlignment);
if (textLine.TextRange.End < textPosition)
{
currentY += textLine.Height;
continue;
}
var characterHit = new CharacterHit(textPosition);
var startX = textLine.GetDistanceFromCharacterHit(characterHit);
var nextCharacterHit = textLine.GetNextCaretCharacterHit(characterHit);
var endX = textLine.GetDistanceFromCharacterHit(nextCharacterHit);
return new Rect(startX, currentY, endX - startX, textLine.Height);
}
return new Rect();
}
public IEnumerable<Rect> HitTestTextRange(int start, int length)
{
if (start + length <= 0)
{
return Array.Empty<Rect>();
}
var result = new List<Rect>(TextLines.Count);
var currentY = 0d;
foreach (var textLine in TextLines)
{
var currentX = textLine.Start;
if (textLine.TextRange.End < start)
{
currentY += textLine.Height;
continue;
}
if (start > textLine.TextRange.Start)
{
currentX += textLine.GetDistanceFromCharacterHit(new CharacterHit(start));
}
var endX = textLine.GetDistanceFromCharacterHit(new CharacterHit(start + length));
result.Add(new Rect(currentX, currentY, endX - currentX, textLine.Height));
if (textLine.TextRange.Start + textLine.TextRange.Length >= start + length)
{
break;
}
currentY += textLine.Height;
}
return result;
}
public TextHitTestResult HitTestPoint(in Point point)
{
var currentY = 0d;
var lineIndex = 0;
TextLine currentLine = null;
CharacterHit characterHit;
for (; lineIndex < TextLines.Count; lineIndex++)
{
currentLine = TextLines[lineIndex];
using (context.PushPostTransform(Matrix.CreateTranslation(offsetX, currentY)))
if (currentY + currentLine.Height > point.Y)
{
textLine.Draw(context);
characterHit = currentLine.GetCharacterHitFromDistance(point.X);
return GetHitTestResult(currentLine, characterHit, point);
}
currentY += textLine.LineMetrics.Size.Height;
currentY += currentLine.Height;
}
if (currentLine is null)
{
return new TextHitTestResult();
}
characterHit = currentLine.GetNextCaretCharacterHit(new CharacterHit(currentLine.TextRange.End));
return GetHitTestResult(currentLine, characterHit, point);
}
private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point)
{
var (x, y) = point;
var lastTrailingIndex = textLine.TextRange.Start + textLine.TextRange.Length;
var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height;
if (x >= textLine.Width && textLine.TextRange.Length > 0 && textLine.NewLineLength > 0)
{
lastTrailingIndex -= textLine.NewLineLength;
}
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
y > Size.Height;
return new TextHitTestResult { IsInside = isInside, IsTrailing = isTrailing, TextPosition = textPosition };
}
/// <summary>
@ -155,7 +293,8 @@ namespace Avalonia.Media.TextFormatting
{
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
return new GenericTextParagraphProperties(textRunStyle, textAlignment, textWrapping, lineHeight);
return new GenericTextParagraphProperties(FlowDirection.LeftToRight, textAlignment, true, false,
textRunStyle, textWrapping, lineHeight, 0);
}
/// <summary>
@ -166,12 +305,14 @@ namespace Avalonia.Media.TextFormatting
/// <param name="height">The current height.</param>
private static void UpdateBounds(TextLine textLine, ref double width, ref double height)
{
if (width < textLine.LineMetrics.Size.Width)
var lineWidth = textLine.Width + textLine.Start * 2;
if (width < lineWidth)
{
width = textLine.LineMetrics.Size.Width;
width = lineWidth;
}
height += textLine.LineMetrics.Size.Height;
height += textLine.Height;
}
/// <summary>
@ -190,8 +331,9 @@ namespace Avalonia.Media.TextFormatting
new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties)
};
return new TextLineImpl(textRuns,
TextLineMetrics.Create(textRuns, new TextRange(startingIndex, 1), MaxWidth, _paragraphProperties));
var textRange = new TextRange(startingIndex, 1);
return new TextLineImpl(textRuns, textRange, MaxWidth, _paragraphProperties);
}
/// <summary>
@ -205,7 +347,7 @@ namespace Avalonia.Media.TextFormatting
TextLines = new List<TextLine> { textLine };
Size = new Size(0, textLine.LineMetrics.Size.Height);
Size = new Size(0, textLine.Height);
}
else
{
@ -230,7 +372,7 @@ namespace Avalonia.Media.TextFormatting
if (textLines.Count > 0)
{
if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) &&
height + textLine.LineMetrics.Size.Height > MaxHeight)
height + textLine.Height > MaxHeight)
{
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{
@ -244,7 +386,7 @@ namespace Avalonia.Media.TextFormatting
}
}
var hasOverflowed = textLine.LineMetrics.HasOverflowed;
var hasOverflowed = textLine.HasOverflowed;
if (hasOverflowed && _textTrimming != TextTrimming.None)
{
@ -257,7 +399,7 @@ namespace Avalonia.Media.TextFormatting
previousLine = textLine;
if (currentPosition != _text.Length || textLine.TextLineBreak == null)
if (currentPosition != _text.Length || textLine.TextLineBreak?.RemainingCharacters == null)
{
continue;
}
@ -290,6 +432,128 @@ namespace Avalonia.Media.TextFormatting
};
}
public int GetLineIndexFromCharacterIndex(int charIndex)
{
if (TextLines is null)
{
return -1;
}
if (charIndex < 0)
{
return -1;
}
if (charIndex > _text.Length - 1)
{
return TextLines.Count - 1;
}
for (var index = 0; index < TextLines.Count; index++)
{
var textLine = TextLines[index];
if (textLine.TextRange.End < charIndex)
{
continue;
}
if (charIndex >= textLine.Start && charIndex <= textLine.TextRange.End)
{
return index;
}
}
return TextLines.Count - 1;
}
public int GetCharacterIndexFromPoint(Point point, bool snapToText)
{
if (TextLines is null)
{
return -1;
}
var (x, y) = point;
if (!snapToText && y > Size.Height)
{
return -1;
}
var currentY = 0d;
foreach (var textLine in TextLines)
{
if (currentY + textLine.Height <= y)
{
currentY += textLine.Height;
continue;
}
if (x > textLine.WidthIncludingTrailingWhitespace)
{
if (snapToText)
{
return textLine.TextRange.End;
}
return -1;
}
var characterHit = textLine.GetCharacterHitFromDistance(x);
return characterHit.FirstCharacterIndex + characterHit.TrailingLength;
}
return _text.Length;
}
public Rect GetRectFromCharacterIndex(int characterIndex, bool trailingEdge)
{
if (TextLines is null)
{
return Rect.Empty;
}
var distanceY = 0d;
var currentIndex = 0;
foreach (var textLine in TextLines)
{
if (currentIndex + textLine.TextRange.Length < characterIndex)
{
distanceY += textLine.Height;
currentIndex += textLine.TextRange.Length;
continue;
}
var characterHit = new CharacterHit(characterIndex);
while (characterHit.FirstCharacterIndex < characterIndex)
{
characterHit = textLine.GetNextCaretCharacterHit(characterHit);
}
var distanceX = textLine.GetDistanceFromCharacterHit(trailingEdge ?
characterHit :
new CharacterHit(characterHit.FirstCharacterIndex));
if (characterHit.TrailingLength > 0)
{
distanceX += 1;
}
return new Rect(distanceX, distanceY, 0, textLine.Height);
}
return Rect.Empty;
}
private readonly struct FormattedTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
@ -306,16 +570,16 @@ namespace Avalonia.Media.TextFormatting
public TextRun GetTextRun(int textSourceIndex)
{
if (textSourceIndex > _text.End)
if (textSourceIndex > _text.Length)
{
return new TextEndOfLine();
return null;
}
var runText = _text.Skip(textSourceIndex);
if (runText.IsEmpty)
{
return new TextEndOfLine();
return new TextEndOfParagraph();
}
var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier);

117
src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs

@ -7,6 +7,14 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public abstract class TextLine
{
/// <summary>
/// Gets the text runs that are contained within a line.
/// </summary>
/// <value>
/// The contained text runs.
/// </value>
public abstract IReadOnlyList<TextRun> TextRuns { get; }
/// <summary>
/// Gets the text range that is covered by the line.
/// </summary>
@ -16,28 +24,28 @@ namespace Avalonia.Media.TextFormatting
public abstract TextRange TextRange { get; }
/// <summary>
/// Gets the text runs.
/// Gets the state of the line when broken by line breaking process.
/// </summary>
/// <value>
/// The text runs.
/// </value>
public abstract IReadOnlyList<TextRun> TextRuns { get; }
/// <returns>
/// A <see cref="TextLineBreak"/> value that represents the line break.
/// </returns>
public abstract TextLineBreak TextLineBreak { get; }
/// <summary>
/// Gets the line metrics.
/// Gets the distance from the top to the baseline of the current TextLine object.
/// </summary>
/// <value>
/// The line metrics.
/// </value>
public abstract TextLineMetrics LineMetrics { get; }
/// <returns>
/// A <see cref="double"/> that represents the baseline distance.
/// </returns>
public abstract double Baseline { get; }
/// <summary>
/// Gets the state of the line when broken by line breaking process.
/// Gets the distance from the top-most to bottom-most black pixel in a line.
/// </summary>
/// <returns>
/// A <see cref="TextLineBreak"/> value that represents the line break.
/// A value that represents the extent distance.
/// </returns>
public abstract TextLineBreak TextLineBreak { get; }
public abstract double Extent { get; }
/// <summary>
/// Gets a value that indicates whether the line is collapsed.
@ -47,11 +55,92 @@ namespace Avalonia.Media.TextFormatting
/// </returns>
public abstract bool HasCollapsed { get; }
/// <summary>
/// Gets a value that indicates whether content of the line overflows the specified paragraph width.
/// </summary>
/// <returns>
/// <c>true</c>, it the line overflows the specified paragraph width; otherwise, <c>false</c>.
/// </returns>
public abstract bool HasOverflowed { get; }
/// <summary>
/// Gets the height of a line of text.
/// </summary>
/// <returns>
/// The text line height.
/// </returns>
public abstract double Height { get; }
/// <summary>
/// Gets the number of newline characters at the end of a line.
/// </summary>
/// <returns>
/// The number of newline characters.
/// </returns>
public abstract int NewLineLength { get; }
/// <summary>
/// Gets the distance that black pixels extend beyond the bottom alignment edge of a line.
/// </summary>
/// <returns>
/// The overhang after distance.
/// </returns>
public abstract double OverhangAfter { get; }
/// <summary>
/// Gets the distance that black pixels extend prior to the left leading alignment edge of the line.
/// </summary>
/// <returns>
/// The overhang leading distance.
/// </returns>
public abstract double OverhangLeading { get; }
/// <summary>
/// Gets the distance that black pixels extend following the right trailing alignment edge of the line.
/// </summary>
/// <returns>
/// The overhang trailing distance.
/// </returns>
public abstract double OverhangTrailing { get; }
/// <summary>
/// Gets the distance from the start of a paragraph to the starting point of a line.
/// </summary>
/// <returns>
/// The distance from the start of a paragraph to the starting point of a line.
/// </returns>
public abstract double Start { get; }
/// <summary>
/// Gets the number of whitespace code points beyond the last non-blank character in a line.
/// </summary>
/// <returns>
/// The number of whitespace code points beyond the last non-blank character in a line.
/// </returns>
public abstract int TrailingWhitespaceLength { get; }
/// <summary>
/// Gets the width of a line of text, excluding trailing whitespace characters.
/// </summary>
/// <returns>
/// The text line width, excluding trailing whitespace characters.
/// </returns>
public abstract double Width { get; }
/// <summary>
/// Gets the width of a line of text, including trailing whitespace characters.
/// </summary>
/// <returns>
/// The text line width, including trailing whitespace characters.
/// </returns>
public abstract double WidthIncludingTrailingWhitespace { get; }
/// <summary>
/// Draws the <see cref="TextLine"/> at the given origin.
/// </summary>
/// <param name="drawingContext">The drawing context.</param>
public abstract void Draw(DrawingContext drawingContext);
/// <param name="lineOrigin"></param>
public abstract void Draw(DrawingContext drawingContext, Point lineOrigin);
/// <summary>
/// Create a collapsed line based on collapsed text properties.

10
src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs

@ -4,10 +4,20 @@ namespace Avalonia.Media.TextFormatting
{
public class TextLineBreak
{
public TextLineBreak(TextEndOfLine textEndOfLine)
{
TextEndOfLine = textEndOfLine;
}
public TextLineBreak(IReadOnlyList<ShapedTextCharacters> remainingCharacters)
{
RemainingCharacters = remainingCharacters;
}
/// <summary>
/// Get the
/// </summary>
public TextEndOfLine TextEndOfLine { get; }
/// <summary>
/// Get the remaining shaped characters that were split up by the <see cref="TextFormatter"/> during the formatting process.

265
src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs

@ -1,30 +1,36 @@
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal class TextLineImpl : TextLine
{
private readonly List<ShapedTextCharacters> _textRuns;
private readonly List<ShapedTextCharacters> _shapedTextRuns;
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
private readonly TextLineMetrics _textLineMetrics;
public TextLineImpl(List<ShapedTextCharacters> textRuns, TextLineMetrics lineMetrics,
TextLineBreak lineBreak = null, bool hasCollapsed = false)
public TextLineImpl(List<ShapedTextCharacters> textRuns, TextRange textRange, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak lineBreak = null, bool hasCollapsed = false)
{
_textRuns = textRuns;
LineMetrics = lineMetrics;
TextRange = textRange;
TextLineBreak = lineBreak;
HasCollapsed = hasCollapsed;
}
/// <inheritdoc/>
public override TextRange TextRange => LineMetrics.TextRange;
_shapedTextRuns = textRuns;
_paragraphWidth = paragraphWidth;
_paragraphProperties = paragraphProperties;
_textLineMetrics = CreateLineMetrics();
}
/// <inheritdoc/>
public override IReadOnlyList<TextRun> TextRuns => _textRuns;
public override IReadOnlyList<TextRun> TextRuns => _shapedTextRuns;
/// <inheritdoc/>
public override TextLineMetrics LineMetrics { get; }
public override TextRange TextRange { get; }
/// <inheritdoc/>
public override TextLineBreak TextLineBreak { get; }
@ -33,19 +39,52 @@ namespace Avalonia.Media.TextFormatting
public override bool HasCollapsed { get; }
/// <inheritdoc/>
public override void Draw(DrawingContext drawingContext)
public override bool HasOverflowed => _textLineMetrics.HasOverflowed;
/// <inheritdoc/>
public override double Baseline => _textLineMetrics.TextBaseline;
/// <inheritdoc/>
public override double Extent => _textLineMetrics.Height;
/// <inheritdoc/>
public override double Height => _textLineMetrics.Height;
/// <inheritdoc/>
public override int NewLineLength => _textLineMetrics.NewLineLength;
/// <inheritdoc/>
public override double OverhangAfter => 0;
/// <inheritdoc/>
public override double OverhangLeading => 0;
/// <inheritdoc/>
public override double OverhangTrailing => 0;
/// <inheritdoc/>
public override int TrailingWhitespaceLength => _textLineMetrics.TrailingWhitespaceLength;
/// <inheritdoc/>
public override double Start => _textLineMetrics.Start;
/// <inheritdoc/>
public override double Width => _textLineMetrics.Width;
/// <inheritdoc/>
public override double WidthIncludingTrailingWhitespace => _textLineMetrics.WidthIncludingTrailingWhitespace;
/// <inheritdoc/>
public override void Draw(DrawingContext drawingContext, Point lineOrigin)
{
var currentX = 0.0;
var (currentX, currentY) = lineOrigin;
foreach (var textRun in _textRuns)
foreach (var textRun in _shapedTextRuns)
{
var offsetY = LineMetrics.TextBaseline - textRun.GlyphRun.BaselineOrigin.Y;
using (drawingContext.PushPostTransform(Matrix.CreateTranslation(currentX, offsetY)))
{
textRun.Draw(drawingContext);
}
var offsetY = Baseline - textRun.GlyphRun.BaselineOrigin.Y;
textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
currentX += textRun.Size.Width;
}
}
@ -59,19 +98,19 @@ namespace Avalonia.Media.TextFormatting
}
var collapsingProperties = collapsingPropertiesList[0];
var runIndex = 0;
var currentWidth = 0.0;
var textRange = TextRange;
var collapsedLength = 0;
TextLineMetrics textLineMetrics;
var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol);
var availableWidth = collapsingProperties.Width - shapedSymbol.Size.Width;
while (runIndex < _textRuns.Count)
while (runIndex < _shapedTextRuns.Count)
{
var currentRun = _textRuns[runIndex];
var currentRun = _shapedTextRuns[runIndex];
currentWidth += currentRun.Size.Width;
@ -87,14 +126,14 @@ namespace Avalonia.Media.TextFormatting
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
var nextBreakPosition = lineBreaker.Current.PositionWrap;
var nextBreakPosition = lineBreaker.Current.PositionMeasure;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition > measuredLength)
if (nextBreakPosition >= measuredLength)
{
break;
}
@ -108,7 +147,7 @@ namespace Avalonia.Media.TextFormatting
collapsedLength += measuredLength;
var splitResult = TextFormatterImpl.SplitTextRuns(_textRuns, collapsedLength);
var splitResult = TextFormatterImpl.SplitTextRuns(_shapedTextRuns, collapsedLength);
var shapedTextCharacters = new List<ShapedTextCharacters>(splitResult.First.Count + 1);
@ -118,12 +157,8 @@ namespace Avalonia.Media.TextFormatting
textRange = new TextRange(textRange.Start, collapsedLength);
var shapedWidth = GetShapedWidth(shapedTextCharacters);
textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height),
LineMetrics.TextBaseline, textRange, false);
return new TextLineImpl(shapedTextCharacters, textLineMetrics, TextLineBreak, true);
return new TextLineImpl(shapedTextCharacters, textRange, _paragraphWidth, _paragraphProperties,
TextLineBreak, true);
}
availableWidth -= currentRun.Size.Width;
@ -133,17 +168,71 @@ namespace Avalonia.Media.TextFormatting
runIndex++;
}
textLineMetrics =
new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Size.Width),
LineMetrics.TextBaseline, TextRange, LineMetrics.HasOverflowed);
return this;
}
private TextLineMetrics CreateLineMetrics()
{
var width = 0d;
var widthIncludingWhitespace = 0d;
var trailingWhitespaceLength = 0;
var newLineLength = 0;
var ascent = 0d;
var descent = 0d;
var lineGap = 0d;
for (var index = 0; index < _shapedTextRuns.Count; index++)
{
var textRun = _shapedTextRuns[index];
var fontMetrics =
new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
if (ascent > fontMetrics.Ascent)
{
ascent = fontMetrics.Ascent;
}
if (descent < fontMetrics.Descent)
{
descent = fontMetrics.Descent;
}
if (lineGap < fontMetrics.LineGap)
{
lineGap = fontMetrics.LineGap;
}
if (index == _shapedTextRuns.Count - 1)
{
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
}
else
{
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
}
}
var start = GetParagraphOffsetX(width, _paragraphWidth, _paragraphProperties.TextAlignment);
return new TextLineImpl(new List<ShapedTextCharacters>(_textRuns) { shapedSymbol }, textLineMetrics, null,
true);
var lineHeight = _paragraphProperties.LineHeight;
var height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
descent - ascent + lineGap :
lineHeight;
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
-ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
}
/// <inheritdoc/>
public override CharacterHit GetCharacterHitFromDistance(double distance)
{
distance -= Start;
if (distance < 0)
{
// hit happens before the line, return the first position
@ -153,7 +242,7 @@ namespace Avalonia.Media.TextFormatting
// process hit that happens within the line
var characterHit = new CharacterHit();
foreach (var run in _textRuns)
foreach (var run in _shapedTextRuns)
{
characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _);
@ -171,7 +260,32 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0));
var characterIndex = characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0);
if (characterIndex > TextRange.End)
{
if (NewLineLength > 0)
{
return Start + Width;
}
return Start + WidthIncludingTrailingWhitespace;
}
var currentDistance = Start;
foreach (var textRun in _shapedTextRuns)
{
if (characterIndex > textRun.Text.End)
{
currentDistance += textRun.Size.Width;
continue;
}
return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(characterIndex));
}
return currentDistance;
}
/// <inheritdoc/>
@ -189,7 +303,7 @@ namespace Avalonia.Media.TextFormatting
var runIndex = GetRunIndexAtCodepointIndex(TextRange.End);
var textRun = _textRuns[runIndex];
var textRun = _shapedTextRuns[runIndex];
characterHit = textRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
@ -219,28 +333,6 @@ namespace Avalonia.Media.TextFormatting
return GetPreviousCaretCharacterHit(characterHit);
}
/// <summary>
/// Get distance from line start to the specified codepoint index.
/// </summary>
private double DistanceFromCodepointIndex(int codepointIndex)
{
var currentDistance = 0.0;
foreach (var textRun in _textRuns)
{
if (codepointIndex > textRun.Text.End)
{
currentDistance += textRun.Size.Width;
continue;
}
return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(codepointIndex));
}
return currentDistance;
}
/// <summary>
/// Tries to find the next character hit.
/// </summary>
@ -258,26 +350,28 @@ namespace Avalonia.Media.TextFormatting
return false; // Cannot go forward anymore
}
if (codepointIndex < TextRange.Start)
{
codepointIndex = TextRange.Start;
}
var runIndex = GetRunIndexAtCodepointIndex(codepointIndex);
while (runIndex < TextRuns.Count)
while (runIndex < _shapedTextRuns.Count)
{
var run = _textRuns[runIndex];
var run = _shapedTextRuns[runIndex];
var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
var foundCharacterHit =
run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
TextRange.Length;
var characterIndex = codepointIndex - run.Text.Start;
var codepoint = Codepoint.ReadAt(run.GlyphRun.Characters, characterIndex, out _);
if (codepoint.IsBreakChar)
if (characterIndex < 0 && characterHit.TrailingLength == 0)
{
foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(codepointIndex - 1, out _);
isAtEnd = true;
foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
}
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
@ -323,7 +417,7 @@ namespace Avalonia.Media.TextFormatting
while (runIndex >= 0)
{
var run = _textRuns[runIndex];
var run = _shapedTextRuns[runIndex];
var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
@ -349,9 +443,9 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The text run index.</returns>
private int GetRunIndexAtCodepointIndex(int codepointIndex)
{
if (codepointIndex >= TextRange.End)
if (codepointIndex > TextRange.End)
{
return _textRuns.Count - 1;
return _shapedTextRuns.Count - 1;
}
if (codepointIndex <= 0)
@ -361,11 +455,11 @@ namespace Avalonia.Media.TextFormatting
var runIndex = 0;
while (runIndex < _textRuns.Count)
while (runIndex < _shapedTextRuns.Count)
{
var run = _textRuns[runIndex];
var run = _shapedTextRuns[runIndex];
if (run.Text.End > codepointIndex)
if (run.Text.End >= codepointIndex)
{
return runIndex;
}
@ -392,24 +486,5 @@ namespace Avalonia.Media.TextFormatting
return new ShapedTextCharacters(glyphRun, textRun.Properties);
}
/// <summary>
/// Gets the shaped width of specified shaped text characters.
/// </summary>
/// <param name="shapedTextCharacters">The shaped text characters.</param>
/// <returns>
/// The shaped width.
/// </returns>
private static double GetShapedWidth(IReadOnlyList<ShapedTextCharacters> shapedTextCharacters)
{
var shapedWidth = 0.0;
for (var i = 0; i < shapedTextCharacters.Count; i++)
{
shapedWidth += shapedTextCharacters[i].Size.Width;
}
return shapedWidth;
}
}
}

101
src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs

@ -1,7 +1,4 @@
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Represents a metric for a <see cref="TextLine"/> objects,
@ -9,29 +6,39 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public readonly struct TextLineMetrics
{
public TextLineMetrics(Size size, double textBaseline, TextRange textRange, bool hasOverflowed)
public TextLineMetrics(bool hasOverflowed, double height, int newLineLength, double start, double textBaseline,
int trailingWhitespaceLength, double width,
double widthIncludingTrailingWhitespace)
{
Size = size;
TextBaseline = textBaseline;
TextRange = textRange;
HasOverflowed = hasOverflowed;
Height = height;
NewLineLength = newLineLength;
Start = start;
TextBaseline = textBaseline;
TrailingWhitespaceLength = trailingWhitespaceLength;
Width = width;
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
}
/// <summary>
/// Gets the text range that is covered by the text line.
/// Gets a value that indicates whether content of the line overflows the specified paragraph width.
/// </summary>
/// <value>
/// The text range that is covered by the text line.
/// </value>
public TextRange TextRange { get; }
public bool HasOverflowed { get; }
/// <summary>
/// Gets the size of the text line.
/// Gets the height of a line of text.
/// </summary>
/// <value>
/// The size.
/// </value>
public Size Size { get; }
public double Height { get; }
/// <summary>
/// Gets the number of newline characters at the end of a line.
/// </summary>
public int NewLineLength { get; }
/// <summary>
/// Gets the distance from the start of a paragraph to the starting point of a line.
/// </summary>
public double Start { get; }
/// <summary>
/// Gets the distance from the top to the baseline of the line of text.
@ -39,58 +46,18 @@ namespace Avalonia.Media.TextFormatting
public double TextBaseline { get; }
/// <summary>
/// Gets a boolean value that indicates whether content of the line overflows
/// the specified paragraph width.
/// Gets the number of whitespace code points beyond the last non-blank character in a line.
/// </summary>
public bool HasOverflowed { get; }
public int TrailingWhitespaceLength { get; }
/// <summary>
/// Creates the text line metrics.
/// Gets the width of a line of text, excluding trailing whitespace characters.
/// </summary>
/// <param name="textRuns">The text runs.</param>
/// <param name="textRange">The text range that is covered by the text line.</param>
/// <param name="paragraphWidth">The paragraph width.</param>
/// <param name="paragraphProperties">The text alignment.</param>
/// <returns></returns>
public static TextLineMetrics Create(IEnumerable<TextRun> textRuns, TextRange textRange, double paragraphWidth,
TextParagraphProperties paragraphProperties)
{
var lineWidth = 0.0;
var ascent = 0.0;
var descent = 0.0;
var lineGap = 0.0;
public double Width { get; }
foreach (var textRun in textRuns)
{
var shapedRun = (ShapedTextCharacters)textRun;
var fontMetrics =
new FontMetrics(shapedRun.Properties.Typeface, shapedRun.Properties.FontRenderingEmSize);
lineWidth += shapedRun.Size.Width;
if (ascent > fontMetrics.Ascent)
{
ascent = fontMetrics.Ascent;
}
if (descent < fontMetrics.Descent)
{
descent = fontMetrics.Descent;
}
if (lineGap < fontMetrics.LineGap)
{
lineGap = fontMetrics.LineGap;
}
}
var size = new Size(lineWidth,
double.IsNaN(paragraphProperties.LineHeight) || MathUtilities.IsZero(paragraphProperties.LineHeight) ?
descent - ascent + lineGap :
paragraphProperties.LineHeight);
return new TextLineMetrics(size, -ascent, textRange, size.Width > paragraphWidth);
}
/// <summary>
/// Gets the width of a line of text, including trailing whitespace characters.
/// </summary>
public double WidthIncludingTrailingWhitespace { get; }
}
}

37
src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs

@ -5,11 +5,36 @@
/// </summary>
public abstract class TextParagraphProperties
{
/// <summary>
/// This property specifies whether the primary text advance
/// direction shall be left-to-right, right-to-left, or top-to-bottom.
/// </summary>
public abstract FlowDirection FlowDirection { get; }
/// <summary>
/// Gets the text alignment.
/// </summary>
public abstract TextAlignment TextAlignment { get; }
/// <summary>
/// Paragraph's line height
/// </summary>
public abstract double LineHeight { get; }
/// <summary>
/// Indicates the first line of the paragraph.
/// </summary>
public abstract bool FirstLineInParagraph { get; }
/// <summary>
/// If true, the formatted line may always be collapsed. If false (the default),
/// only lines that overflow the paragraph width are collapsed.
/// </summary>
public virtual bool AlwaysCollapsible
{
get { return false; }
}
/// <summary>
/// Gets the default text style.
/// </summary>
@ -27,8 +52,16 @@
public abstract TextWrapping TextWrapping { get; }
/// <summary>
/// Paragraph's line height
/// Line indentation
/// </summary>
public abstract double LineHeight { get; }
public abstract double Indent { get; }
/// <summary>
/// Paragraph indentation
/// </summary>
public virtual double ParagraphIndent
{
get { return 0; }
}
}
}

2
src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs

@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting
[DebuggerTypeProxy(typeof(TextRunDebuggerProxy))]
public abstract class TextRun
{
public static readonly int DefaultTextSourceLength = 1;
public const int DefaultTextSourceLength = 1;
/// <summary>
/// Gets the text source length.

7
src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs

@ -42,6 +42,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public abstract CultureInfo CultureInfo { get; }
/// <summary>
/// Run vertical box alignment
/// </summary>
public abstract BaselineAlignment BaselineAlignment { get; }
public bool Equals(TextRunProperties other)
{
if (ReferenceEquals(null, other))
@ -66,7 +71,7 @@ namespace Avalonia.Media.TextFormatting
{
unchecked
{
var hashCode = (Typeface != null ? Typeface.GetHashCode() : 0);
var hashCode = Typeface.GetHashCode();
hashCode = (hashCode * 397) ^ FontRenderingEmSize.GetHashCode();
hashCode = (hashCode * 397) ^ (TextDecorations != null ? TextDecorations.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (ForegroundBrush != null ? ForegroundBrush.GetHashCode() : 0);

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

@ -168,9 +168,8 @@ namespace Avalonia.Platform
/// Creates a platform implementation of a glyph run.
/// </summary>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="width">The glyph run's width.</param>
/// <returns></returns>
IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width);
IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun);
bool SupportsIndividualRoundRects { get; }

12
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -196,7 +196,7 @@ namespace Avalonia.Skia
private static readonly ThreadLocal<SKTextBlobBuilder> s_textBlobBuilderThreadLocal = new ThreadLocal<SKTextBlobBuilder>(() => new SKTextBlobBuilder());
/// <inheritdoc />
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
var count = glyphRun.GlyphIndices.Length;
var textBlobBuilder = s_textBlobBuilderThreadLocal.Value;
@ -208,11 +208,8 @@ namespace Avalonia.Skia
s_font.Size = (float)glyphRun.FontRenderingEmSize;
s_font.Typeface = typeface;
SKTextBlob textBlob;
width = 0;
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
if (glyphRun.GlyphOffsets.IsEmpty)
@ -222,8 +219,6 @@ namespace Avalonia.Skia
textBlobBuilder.AddRun(glyphRun.GlyphIndices.Buffer.Span, s_font);
textBlob = textBlobBuilder.Build();
width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
else
{
@ -231,6 +226,8 @@ namespace Avalonia.Skia
var positions = buffer.GetPositionSpan();
var width = 0d;
for (var i = 0; i < count; i++)
{
positions[i] = (float)width;
@ -276,13 +273,10 @@ namespace Avalonia.Skia
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
width = currentX;
textBlob = textBlobBuilder.Build();
}
return new GlyphRunImpl(textBlob);
}
public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi)

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

@ -235,7 +235,7 @@ namespace Avalonia.Direct2D1
return new WicBitmapImpl(format, alphaFormat, data, size, dpi, stride);
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
@ -258,8 +258,6 @@ namespace Avalonia.Direct2D1
run.Advances = new float[glyphCount];
width = 0;
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
if (glyphRun.GlyphAdvances.IsEmpty)
@ -269,8 +267,6 @@ namespace Avalonia.Direct2D1
var advance = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
run.Advances[i] = advance;
width += advance;
}
}
else
@ -280,8 +276,6 @@ namespace Avalonia.Direct2D1
var advance = (float)glyphRun.GlyphAdvances[i];
run.Advances[i] = advance;
width += advance;
}
}

49
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs

@ -5,6 +5,7 @@ using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Data;
using Avalonia.Layout;
using Xunit;
namespace Avalonia.Base.UnitTests
@ -104,6 +105,25 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("baz", target.Foo);
}
[Fact]
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_3()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.SetValue(TestClass.BazProperty, Orientation.Horizontal, BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Equal(TestClass.BazProperty, raised[0].Property);
Assert.Equal(Orientation.Vertical, raised[0].OldValue);
Assert.Equal(Orientation.Horizontal, raised[0].NewValue);
Assert.Equal(Orientation.Horizontal, target.Baz);
}
[Fact]
public void SetValue_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update()
{
@ -234,6 +254,26 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("baz", raised[0].NewValue);
}
[Fact]
public void Binding_Change_Should_Be_Raised_After_Batch_Update_3()
{
var target = new TestClass();
var observable = new TestObservable<Orientation>(Orientation.Horizontal);
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Bind(TestClass.BazProperty, observable, BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Equal(TestClass.BazProperty, raised[0].Property);
Assert.Equal(Orientation.Vertical, raised[0].OldValue);
Assert.Equal(Orientation.Horizontal, raised[0].NewValue);
Assert.Equal(Orientation.Horizontal, target.Baz);
}
[Fact]
public void Binding_Completion_Should_Be_Raised_After_Batch_Update()
{
@ -579,6 +619,9 @@ namespace Avalonia.Base.UnitTests
public static readonly StyledProperty<string> BarProperty =
AvaloniaProperty.Register<TestClass, string>(nameof(Bar));
public static readonly StyledProperty<Orientation> BazProperty =
AvaloniaProperty.Register<TestClass, Orientation>(nameof(Bar), Orientation.Vertical);
public string Foo
{
get => GetValue(FooProperty);
@ -590,6 +633,12 @@ namespace Avalonia.Base.UnitTests
get => GetValue(BarProperty);
set => SetValue(BarProperty, value);
}
public Orientation Baz
{
get => GetValue(BazProperty);
set => SetValue(BazProperty, value);
}
}
public class TestObservable<T> : ObservableBase<BindingValue<T>>

4
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -108,10 +108,8 @@ namespace Avalonia.Benchmarks
return new MockFontManagerImpl();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
width = default;
return new NullGlyphRun();
}

123
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -379,6 +379,17 @@ namespace Avalonia.Controls.UnitTests
Assert.DoesNotContain(":empty", target.Classes);
}
[Fact]
public void Empty_Class_Should_Be_Set_When_Items_Not_Set()
{
var target = new ItemsControl()
{
Template = GetTemplate(),
};
Assert.Contains(":empty", target.Classes);
}
[Fact]
public void Empty_Class_Should_Be_Set_When_Empty_Collection_Set()
{
@ -393,6 +404,118 @@ namespace Avalonia.Controls.UnitTests
Assert.Contains(":empty", target.Classes);
}
[Fact]
public void Item_Count_Should_Be_Set_When_Items_Added()
{
var target = new ItemsControl()
{
Template = GetTemplate(),
Items = new[] { 1, 2, 3 },
};
Assert.Equal(3, target.ItemCount);
}
[Fact]
public void Item_Count_Should_Be_Set_When_Items_Changed()
{
var items = new ObservableCollection<int>() { 1, 2, 3 };
var target = new ItemsControl()
{
Template = GetTemplate(),
Items = items,
};
items.Add(4);
Assert.Equal(4, target.ItemCount);
items.Clear();
Assert.Equal(0, target.ItemCount);
}
[Fact]
public void Empty_Class_Should_Be_Set_When_Items_Collection_Cleared()
{
var items = new ObservableCollection<int>() { 1, 2, 3 };
var target = new ItemsControl()
{
Template = GetTemplate(),
Items = items,
};
items.Clear();
Assert.Contains(":empty", target.Classes);
}
[Fact]
public void Empty_Class_Should_Not_Be_Set_When_Items_Collection_Count_Increases()
{
var items = new ObservableCollection<int>() { };
var target = new ItemsControl()
{
Template = GetTemplate(),
Items = items,
};
items.Add(1);
Assert.DoesNotContain(":empty", target.Classes);
}
[Fact]
public void Single_Item_Class_Should_Be_Set_When_Items_Collection_Count_Increases_To_One()
{
var items = new ObservableCollection<int>() { };
var target = new ItemsControl()
{
Template = GetTemplate(),
Items = items,
};
items.Add(1);
Assert.Contains(":singleitem", target.Classes);
}
[Fact]
public void Empty_Class_Should_Not_Be_Set_When_Items_Collection_Cleared()
{
var items = new ObservableCollection<int>() { 1, 2, 3 };
var target = new ItemsControl()
{
Template = GetTemplate(),
Items = items,
};
items.Clear();
Assert.DoesNotContain(":singleitem", target.Classes);
}
[Fact]
public void Single_Item_Class_Should_Not_Be_Set_When_Items_Collection_Count_Increases_Beyond_One()
{
var items = new ObservableCollection<int>() { 1 };
var target = new ItemsControl()
{
Template = GetTemplate(),
Items = items,
};
items.Add(2);
Assert.DoesNotContain(":singleitem", target.Classes);
}
[Fact]
public void Setting_Presenter_Explicitly_Should_Set_Item_Parent()
{

28
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -1,3 +1,4 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Collections;
@ -457,6 +458,33 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Initial_Binding_Of_SelectedItems_Should_Not_Cause_Write_To_SelectedItems()
{
var target = new ListBox
{
[!ListBox.ItemsProperty] = new Binding("Items"),
[!ListBox.SelectedItemsProperty] = new Binding("SelectedItems"),
};
var viewModel = new
{
Items = new[] { "Foo", "Bar", "Baz " },
SelectedItems = new ObservableCollection<string> { "Bar" },
};
var raised = 0;
viewModel.SelectedItems.CollectionChanged += (s, e) => ++raised;
target.DataContext = viewModel;
Assert.Equal(0, raised);
Assert.Equal(new[] { "Bar" }, viewModel.SelectedItems);
Assert.Equal(new[] { "Bar" }, target.SelectedItems);
Assert.Equal(new[] { "Bar" }, target.Selection.SelectedItems);
}
private FuncControlTemplate ListBoxTemplate()
{
return new FuncControlTemplate<ListBox>((parent, scope) =>

2
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@ -37,7 +37,7 @@ namespace Avalonia.Controls.UnitTests
target.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
target.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
Assert.Equal(new[] { true, false, true }, values);
Assert.Equal(new[] { false, true }, values);
}
[Fact]

2
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -378,7 +378,7 @@ namespace Avalonia.Controls.UnitTests
[Theory]
[InlineData(new object[] { false, TextWrapping.NoWrap, ScrollBarVisibility.Hidden })]
[InlineData(new object[] { false, TextWrapping.Wrap, ScrollBarVisibility.Hidden })]
[InlineData(new object[] { false, TextWrapping.Wrap, ScrollBarVisibility.Disabled })]
[InlineData(new object[] { true, TextWrapping.NoWrap, ScrollBarVisibility.Auto })]
[InlineData(new object[] { true, TextWrapping.Wrap, ScrollBarVisibility.Disabled })]
public void Has_Correct_Horizontal_ScrollBar_Visibility(

4
tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs

@ -162,7 +162,7 @@ namespace Avalonia.Layout.UnitTests
{
var root = CreateRoot();
var target = new Canvas { Width = 200, Height = 200 };
var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate() };
var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate(), HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden };
var raised = 0;
target.EffectiveViewportChanged += (s, e) =>
@ -185,7 +185,7 @@ namespace Avalonia.Layout.UnitTests
{
var root = CreateRoot();
var target = new Canvas { Width = 200, Height = 200 };
var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate() };
var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate(), HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden };
var raised = 0;
root.Child = scroller;

20
tests/Avalonia.RenderTests/Media/VisualBrushTests.cs

@ -14,10 +14,6 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
public class VisualBrushTests : TestBase
{
//Whitespaces are used here to be able to compare rendering results in a platform independent way.
//Otherwise tests will fail because of slightly different glyph rendering.
private static readonly string s_visualBrushText = " ";
public VisualBrushTests()
: base(@"Media\VisualBrush")
{
@ -46,13 +42,11 @@ namespace Avalonia.Direct2D1.RenderTests.Media
BorderThickness = new Thickness(2),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock
Child = new Panel
{
FontSize = 24,
FontFamily = TestFontFamily,
Background = Brushes.Green,
Foreground = Brushes.Yellow,
Text = s_visualBrushText
Height = 26,
Width = 150,
Background = Brushes.Green
}
}
}
@ -392,10 +386,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
Background = Brushes.Yellow,
HorizontalAlignment = HorizontalAlignment.Left,
Child = new TextBlock
Child = new Panel
{
FontFamily = TestFontFamily,
Text = s_visualBrushText
Height = 10,
Width = 50
}
}),
new Border

46
tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs

@ -0,0 +1,46 @@
using System;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Skia.UnitTests.Media
{
public class GlyphRunTests
{
[InlineData("ABC \r", 29, 4, 1)]
[InlineData("ABC \r", 23, 3, 1)]
[InlineData("ABC \r", 17, 2, 1)]
[InlineData("ABC \r", 11, 1, 1)]
[InlineData("ABC \r", 7, 1, 0)]
[InlineData("ABC \r", 5, 0, 1)]
[InlineData("ABC \r", 2, 0, 0)]
[Theory]
public void Should_Get_Distance_From_CharacterHit(string text, double distance, int expectedIndex,
int expectedTrailingLength)
{
using (Start())
{
var glyphRun =
TextShaper.Current.ShapeText(text.AsMemory(), Typeface.Default, 10, CultureInfo.CurrentCulture);
var characterHit = glyphRun.GetCharacterHitFromDistance(distance, out _);
Assert.Equal(expectedIndex, characterHit.FirstCharacterIndex);
Assert.Equal(expectedTrailingLength, characterHit.TrailingLength);
}
}
private static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(null),
textShaperImpl: new TextShaperImpl(),
fontManagerImpl: new CustomFontManagerImpl()));
return disposable;
}
}
}

38
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs

@ -1,38 +0,0 @@
using System;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
internal class FormattableTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
private readonly TextRunProperties _defaultStyle;
private ReadOnlySlice<ValueSpan<TextRunProperties>> _styleSpans;
public FormattableTextSource(string text, TextRunProperties defaultStyle,
ReadOnlySlice<ValueSpan<TextRunProperties>> styleSpans)
{
_text = text.AsMemory();
_defaultStyle = defaultStyle;
_styleSpans = styleSpans;
}
public TextRun GetTextRun(int textSourceIndex)
{
if (_styleSpans.IsEmpty)
{
return new TextEndOfParagraph();
}
var currentSpan = _styleSpans[0];
_styleSpans = _styleSpans.Skip(1);
return new TextCharacters(_text.AsSlice(currentSpan.Start, currentSpan.Length),
_defaultStyle);
}
}
}

121
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattedTextSource.cs

@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
internal readonly struct FormattedTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
private readonly TextRunProperties _defaultProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>> _textModifier;
public FormattedTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties,
IReadOnlyList<ValueSpan<TextRunProperties>> textModifier)
{
_text = text;
_defaultProperties = defaultProperties;
_textModifier = textModifier;
}
public TextRun GetTextRun(int textSourceIndex)
{
if (textSourceIndex > _text.End)
{
return null;
}
var runText = _text.Skip(textSourceIndex);
if (runText.IsEmpty)
{
return new TextEndOfParagraph();
}
var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier);
return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
}
/// <summary>
/// Creates a span of text run properties that has modifier applied.
/// </summary>
/// <param name="text">The text to create the properties for.</param>
/// <param name="defaultProperties">The default text properties.</param>
/// <param name="textModifier">The text properties modifier.</param>
/// <returns>
/// The created text style run.
/// </returns>
private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySlice<char> text,
TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>> textModifier)
{
if (textModifier == null || textModifier.Count == 0)
{
return new ValueSpan<TextRunProperties>(text.Start, text.Length, defaultProperties);
}
var currentProperties = defaultProperties;
var hasOverride = false;
var i = 0;
var length = 0;
for (; i < textModifier.Count; i++)
{
var propertiesOverride = textModifier[i];
var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
if (textRange.End < text.Start)
{
continue;
}
if (textRange.Start > text.End)
{
length = text.Length;
break;
}
if (textRange.Start > text.Start)
{
if (propertiesOverride.Value != currentProperties)
{
length = Math.Min(Math.Abs(textRange.Start - text.Start), text.Length);
break;
}
}
length += Math.Min(text.Length - length, textRange.Length);
if (hasOverride)
{
continue;
}
hasOverride = true;
currentProperties = propertiesOverride.Value;
}
if (length < text.Length && i == textModifier.Count)
{
if (currentProperties == defaultProperties)
{
length = text.Length;
}
}
if (length != text.Length)
{
text = text.Take(length);
}
return new ValueSpan<TextRunProperties>(text.Start, length, currentProperties);
}
}
}

5
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs

@ -20,6 +20,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public TextRun GetTextRun(int textSourceIndex)
{
if (textSourceIndex > 50)
{
return null;
}
if (textSourceIndex == 50)
{
return new TextEndOfParagraph();

7
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs

@ -17,8 +17,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public TextRun GetTextRun(int textSourceIndex)
{
if (textSourceIndex > _text.Length)
{
return null;
}
var runText = _text.Skip(textSourceIndex);
if (runText.IsEmpty)
{
return new TextEndOfParagraph();

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

@ -81,7 +81,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new ValueSpan<TextRunProperties>(9, 1, defaultProperties)
};
var textSource = new FormattableTextSource(text, defaultProperties, GenericTextRunPropertiesRuns);
var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, GenericTextRunPropertiesRuns);
var formatter = new TextFormatterImpl();
@ -167,7 +167,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var textLine =
formatter.FormatLine(textSource, currentPosition, 1,
new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.WrapWithOverflow));
new GenericTextParagraphProperties(defaultProperties, textWrap : TextWrapping.WrapWithOverflow));
if (text.Length - currentPosition > expectedCharactersPerLine)
{
@ -223,7 +223,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var textLine =
formatter.FormatLine(textSource, currentPosition, paragraphWidth,
new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap));
new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap));
Assert.True(expected.Contains(textLine.TextRange.End));
@ -256,7 +256,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties, lineHeight: 50));
Assert.Equal(50, textLine.LineMetrics.Size.Height);
Assert.Equal(50, textLine.Height);
}
}
@ -273,7 +273,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
var textSource = new SingleBufferTextSource(text, defaultProperties);
@ -286,7 +286,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, textSourceIndex, 200, paragraphProperties);
Assert.True(textLine.LineMetrics.Size.Width <= 200);
Assert.True(textLine.Width <= 200);
textSourceIndex += textLine.TextRange.Length;
}
@ -301,7 +301,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
const string text = "012345";
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
@ -321,6 +321,87 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[InlineData("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor",
new []{ "Lorem ipsum ", "dolor sit amet, ", "consectetur ", "adipisicing elit, ", "sed do eiusmod "})]
[Theory]
public void Should_Produce_Wrapped_And_Trimmed_Lines(string text, string[] expectedLines)
{
using (Start())
{
var typeface = new Typeface("Verdana");
var defaultProperties = new GenericTextRunProperties(typeface, 32, foregroundBrush: Brushes.Black);
var styleSpans = new[]
{
new ValueSpan<TextRunProperties>(0, 5,
new GenericTextRunProperties(typeface, 48)),
new ValueSpan<TextRunProperties>(6, 11,
new GenericTextRunProperties(new Typeface("Verdana", weight: FontWeight.Bold), 32)),
new ValueSpan<TextRunProperties>(28, 28,
new GenericTextRunProperties(new Typeface("Verdana", FontStyle.Italic),32))
};
var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, styleSpans);
var formatter = new TextFormatterImpl();
var currentPosition = 0;
var currentHeight = 0d;
var currentLineIndex = 0;
while (currentPosition < text.Length && currentLineIndex < expectedLines.Length)
{
var textLine =
formatter.FormatLine(textSource, currentPosition, 300,
new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow));
currentPosition += textLine.TextRange.Length;
if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
{
textLine = textLine.Collapse(new TextTrailingWordEllipsis(300, defaultProperties));
}
currentHeight += textLine.Height;
var currentText = text.Substring(textLine.TextRange.Start, textLine.TextRange.Length);
Assert.Equal(expectedLines[currentLineIndex], currentText);
currentLineIndex++;
}
Assert.Equal(expectedLines.Length,currentLineIndex);
}
}
[InlineData(TextAlignment.Left)]
[InlineData(TextAlignment.Center)]
[InlineData(TextAlignment.Right)]
[Theory]
public void Should_Align_TextLine(TextAlignment textAlignment)
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textAlignment);
var textSource = new SingleBufferTextSource("0123456789", defaultProperties);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, 100, paragraphProperties);
var expectedOffset = TextLine.GetParagraphOffsetX(textLine.Width, 100, textAlignment);
Assert.Equal(expectedOffset, textLine.Start);
}
}
public static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface

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

@ -11,8 +11,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
public class TextLayoutTests
{
private static readonly string s_singleLineText = "0123456789";
private static readonly string s_multiLineText = "012345678\r\r0123456789";
private const string SingleLineText = "0123456789";
private const string MultiLineText = "01 23 45 678\r\rabc def gh ij";
[InlineData("01234\r01234\r", 3)]
[InlineData("01234\r01234", 2)]
@ -45,7 +45,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
};
var layout = new TextLayout(
s_multiLineText,
MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@ -61,7 +61,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var actual = textRun.Text.Buffer.Span.ToString();
Assert.Equal("12", actual);
Assert.Equal("1 ", actual);
Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
}
@ -74,7 +74,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
for (var i = 4; i < s_multiLineText.Length; i++)
var expected = new TextLayout(
MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
textWrapping: TextWrapping.Wrap,
maxWidth: 25);
var expectedLines = expected.TextLines.Select(x => MultiLineText.Substring(x.TextRange.Start,
x.TextRange.Length)).ToList();
for (var i = 4; i < MultiLineText.Length; i++)
{
var spans = new[]
{
@ -82,16 +93,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
};
var expected = new TextLayout(
s_multiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
textWrapping: TextWrapping.Wrap,
maxWidth: 25);
var actual = new TextLayout(
s_multiLineText,
MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@ -99,14 +102,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
maxWidth: 25,
textStyleOverrides: spans);
Assert.Equal(expected.TextLines.Count, actual.TextLines.Count);
var actualLines = actual.TextLines.Select(x => MultiLineText.Substring(x.TextRange.Start,
x.TextRange.Length)).ToList();
Assert.Equal(expectedLines.Count, actualLines.Count);
for (var j = 0; j < actual.TextLines.Count; j++)
{
Assert.Equal(expected.TextLines[j].TextRange.Length, actual.TextLines[j].TextRange.Length);
var expectedText = expectedLines[j];
var actualText = actualLines[j];
Assert.Equal(expected.TextLines[j].TextRuns.Sum(x => x.Text.Length),
actual.TextLines[j].TextRuns.Sum(x => x.Text.Length));
Assert.Equal(expectedText, actualText);
}
}
}
@ -126,7 +133,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
};
var layout = new TextLayout(
s_singleLineText,
SingleLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@ -140,7 +147,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(2, textRun.Text.Length);
var actual = s_singleLineText.Substring(textRun.Text.Start,
var actual = SingleLineText.Substring(textRun.Text.Start,
textRun.Text.Length);
Assert.Equal("01", actual);
@ -163,7 +170,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
};
var layout = new TextLayout(
s_singleLineText,
SingleLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@ -261,12 +268,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var layout = new TextLayout(
s_multiLineText,
MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable());
Assert.Equal(s_multiLineText.Length, layout.TextLines.Sum(x => x.TextRange.Length));
Assert.Equal(MultiLineText.Length, layout.TextLines.Sum(x => x.TextRange.Length));
}
}
@ -276,13 +283,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var layout = new TextLayout(
s_multiLineText,
MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable());
Assert.Equal(
s_multiLineText.Length,
MultiLineText.Length,
layout.TextLines.Select(textLine =>
textLine.TextRuns.Sum(textRun => textRun.Text.Length))
.Sum());
@ -295,9 +302,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
const string text =
"Multiline TextBox with TextWrapping.\r\rLorem ipsum dolor sit amet, consectetur adipiscing elit. " +
"Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. " +
"Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.";
"Multiline TextBox with TextWrapping.\r\rLorem ipsum dolor sit amet";
var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
@ -338,7 +343,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
};
var layout = new TextLayout(
s_multiLineText,
MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@ -541,7 +546,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var layout = new TextLayout(
s_multiLineText,
MultiLineText,
Typeface.Default,
12,
Brushes.Black,
@ -549,7 +554,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
foreach (var line in layout.TextLines)
{
Assert.Equal(50, line.LineMetrics.Size.Height);
Assert.Equal(50, line.Height);
}
}
}
@ -601,7 +606,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var layout = new TextLayout(
s_singleLineText,
SingleLineText,
Typeface.Default,
12,
Brushes.Black,
@ -609,7 +614,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
maxWidth: 3);
//every character should be new line as there not enough space for even one character
Assert.Equal(s_singleLineText.Length, layout.TextLines.Count);
Assert.Equal(SingleLineText.Length, layout.TextLines.Count);
}
}

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

@ -71,6 +71,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
[InlineData("𐐷𐐷𐐷𐐷𐐷")]
[InlineData("01234567🎉\n")]
[InlineData("𐐷1234")]
[Theory]
public void Should_Get_Next_Caret_CharacterHit(string text)
@ -109,9 +110,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
for (var i = 0; i < clusters.Length; i++)
foreach (var cluster in clusters)
{
Assert.Equal(clusters[i], nextCharacterHit.FirstCharacterIndex);
Assert.Equal(cluster, nextCharacterHit.FirstCharacterIndex);
nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit);
}
@ -127,6 +128,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
[InlineData("𐐷𐐷𐐷𐐷𐐷")]
[InlineData("01234567🎉\n")]
[InlineData("𐐷1234")]
[Theory]
public void Should_Get_Previous_Caret_CharacterHit(string text)
@ -269,14 +271,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
characterHit = textLine.GetCharacterHitFromDistance(textLine.LineMetrics.Size.Width);
characterHit = textLine.GetCharacterHitFromDistance(textLine.Width);
Assert.Equal(MultiBufferTextSource.TextRange.End, characterHit.FirstCharacterIndex);
}
}
[InlineData("01234 01234", 8, TextCollapsingStyle.TrailingCharacter, "01234 0\u2026")]
[InlineData("01234 01234", 8, TextCollapsingStyle.TrailingWord, "01234 \u2026")]
[InlineData("01234 01234", 8, TextCollapsingStyle.TrailingWord, "01234\u2026")]
[Theory]
public void Should_Collapse_Line(string text, int numberOfCharacters, TextCollapsingStyle style, string expected)
{
@ -333,8 +335,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[Fact]
public void Should_Ignore_Invisible_Characters()
[Fact(Skip = "Verify this")]
public void Should_Ignore_NewLine_Characters()
{
using (Start())
{
@ -356,6 +358,28 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(new CharacterHit(8, 2), nextCharacterHit);
}
}
[Fact]
public void TextLineBreak_Should_Contain_TextEndOfLine()
{
using (Start())
{
var defaultTextRunProperties =
new GenericTextRunProperties(Typeface.Default);
const string text = "0123456789";
var source = new SingleBufferTextSource(text, defaultTextRunProperties);
var textParagraphProperties = new GenericTextParagraphProperties(defaultTextRunProperties);
var formatter = TextFormatter.Current;
var textLine = formatter.FormatLine(source, 0, double.PositiveInfinity, textParagraphProperties);
Assert.NotNull(textLine.TextLineBreak.TextEndOfLine);
}
}
private static IDisposable Start()
{

3
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -119,9 +119,8 @@ namespace Avalonia.UnitTests
throw new NotImplementedException();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
width = 0;
return Mock.Of<IGlyphRunImpl>();
}

2
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@ -74,7 +74,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
throw new NotImplementedException();
}

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 B

After

Width:  |  Height:  |  Size: 403 B

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

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

Loading…
Cancel
Save