diff --git a/native/Avalonia.Native/src/OSX/controlhost.mm b/native/Avalonia.Native/src/OSX/controlhost.mm
index f8e9a3b6d1..5683a5a975 100644
--- a/native/Avalonia.Native/src/OSX/controlhost.mm
+++ b/native/Avalonia.Native/src/OSX/controlhost.mm
@@ -36,7 +36,10 @@ public:
virtual void DestroyDefaultChild(void* child) override
{
// ARC will release the object for us
+ #pragma clang diagnostic push
+ #pragma clang diagnostic ignored "-Wunused-value"
(__bridge_transfer NSView*) child;
+ #pragma clang diagnostic pop
}
};
diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm
index 9b703c4838..620b750a40 100644
--- a/native/Avalonia.Native/src/OSX/window.mm
+++ b/native/Avalonia.Native/src/OSX/window.mm
@@ -457,7 +457,8 @@ public:
}
point = ConvertPointY(point);
- auto viewPoint = [Window convertScreenToBase:ToNSPoint(point)];
+ NSRect convertRect = [Window convertRectToScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)];
+ auto viewPoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y);
*ret = [View translateLocalPoint:ToAvnPoint(viewPoint)];
@@ -477,7 +478,8 @@ public:
}
auto cocoaViewPoint = ToNSPoint([View translateLocalPoint:point]);
- auto cocoaScreenPoint = [Window convertBaseToScreen:cocoaViewPoint];
+ NSRect convertRect = [Window convertRectToScreen:NSMakeRect(cocoaViewPoint.x, cocoaViewPoint.y, 0.0, 0.0)];
+ auto cocoaScreenPoint = NSPointFromCGPoint(NSMakePoint(convertRect.origin.x, convertRect.origin.y));
*ret = ConvertPointY(ToAvnPoint(cocoaScreenPoint));
return S_OK;
@@ -573,7 +575,8 @@ public:
if(!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited)
|| (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged)))
{
- auto nspoint = [Window convertBaseToScreen: ToNSPoint(point)];
+ NSRect convertRect = [Window convertRectToScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)];
+ auto nspoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y);
CGPoint cgpoint = NSPointToCGPoint(nspoint);
auto cgevent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, cgpoint, kCGMouseButtonLeft);
nsevent = [NSEvent eventWithCGEvent: cgevent];
diff --git a/readme.md b/readme.md
index 7e32dbc321..1cdaf3b8f8 100644
--- a/readme.md
+++ b/readme.md
@@ -1,7 +1,7 @@
[](https://t.me/Avalonia)
[](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) []( https://aka.ms/dotnet-discord) [](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [](#backers) [](#sponsors) 
-[](https://www.nuget.org/packages/Avalonia) [](https://www.nuget.org/packages/Avalonia) [](https://www.myget.org/gallery/avalonia-ci) 
+[](https://www.nuget.org/packages/Avalonia) [](https://www.nuget.org/packages/Avalonia) 
## 📖 About
diff --git a/samples/ControlCatalog/Pages/ButtonsPage.xaml b/samples/ControlCatalog/Pages/ButtonsPage.xaml
index 7de0872eae..059b4d9788 100644
--- a/samples/ControlCatalog/Pages/ButtonsPage.xaml
+++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml
@@ -147,6 +147,35 @@
+
+
+
+
+ A button with an added drop-down chevron to visually indicate it has a flyout with additional actions.
+
+
+
+
+
+
+
+
+
+
+
+ Disabled
+
+
+
+
+ Content="Re-themed"
+ Foreground="White">
+
+
+
+
+
+
+
@@ -138,6 +160,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -147,28 +180,17 @@
-
+
{
-
+
}
-
+
-
-
-
-
-
-
-
-
-
-
-
@@ -179,15 +201,15 @@
-
+
(
-
+
)
-
+
@@ -200,11 +222,11 @@
-
+
-
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
index 78919a2105..08ffe2c081 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
@@ -7,9 +7,13 @@ namespace Avalonia.Diagnostics.Views
{
internal class ControlDetailsView : UserControl
{
+ private DataGrid _dataGrid;
+
public ControlDetailsView()
{
InitializeComponent();
+
+ _dataGrid = this.GetControl("DataGrid");
}
private void InitializeComponent()
@@ -25,5 +29,25 @@ namespace Avalonia.Diagnostics.Views
}
}
+
+ private void PropertyNamePressed(object sender, PointerPressedEventArgs e)
+ {
+ var mainVm = (ControlDetailsViewModel?) DataContext;
+
+ if (mainVm is null)
+ {
+ return;
+ }
+
+ if (sender is Control control && control.DataContext is SetterViewModel setterVm)
+ {
+ mainVm.SelectProperty(setterVm.Property);
+
+ if (mainVm.SelectedProperty is not null)
+ {
+ _dataGrid.ScrollIntoView(mainVm.SelectedProperty, null);
+ }
+ }
+ }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml
index af6c84a76a..c404a5c382 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml
@@ -15,6 +15,7 @@
+
@@ -46,6 +47,7 @@
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
index a3cff7f3d3..3b143d3b58 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
@@ -11,6 +11,7 @@ using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
+using Avalonia.Themes.Default;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.Views
@@ -109,7 +110,7 @@ namespace Avalonia.Diagnostics.Views
{
#pragma warning disable CS0618 // Type or member is obsolete
var point = (topLevel as IInputRoot)?.MouseDevice?.GetPosition(topLevel) ?? default;
-#pragma warning restore CS0618 // Type or member is obsolete
+#pragma warning restore CS0618 // Type or member is obsolete
return (IControl?)topLevel.GetVisualsAt(point, x =>
{
@@ -161,13 +162,18 @@ namespace Avalonia.Diagnostics.Views
return;
}
- var root = Root as TopLevel
- ?? vm.PointerOverRoot as TopLevel;
+ var root = vm.PointerOverRoot as TopLevel;
+
if (root is null)
{
return;
}
+ if (root is PopupRoot pr && pr.ParentTopLevel != null)
+ {
+ root = pr.ParentTopLevel;
+ }
+
switch (e.Modifiers)
{
case RawInputModifiers.Control when (e.Key == Key.LeftShift || e.Key == Key.RightShift):
@@ -242,7 +248,25 @@ namespace Avalonia.Diagnostics.Views
private void RootClosed(object? sender, EventArgs e) => Close();
- public void SetOptions(DevToolsOptions options) =>
+ public void SetOptions(DevToolsOptions options)
+ {
(DataContext as MainViewModel)?.SetOptions(options);
+
+ if (options.UseDarkMode)
+ {
+ if (Styles[0] is SimpleTheme st)
+ {
+ st.Mode = SimpleThemeMode.Dark;
+ }
+ }
+ }
+
+ internal void SelectedControl(IControl? control)
+ {
+ if (control is { })
+ {
+ (DataContext as MainViewModel)?.SelectControl(control);
+ }
+ }
}
}
diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.cs b/src/Avalonia.Dialogs/ManagedFileChooser.cs
index 9058c405a3..199a4d6620 100644
--- a/src/Avalonia.Dialogs/ManagedFileChooser.cs
+++ b/src/Avalonia.Dialogs/ManagedFileChooser.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
@@ -9,6 +10,8 @@ using Avalonia.LogicalTree;
namespace Avalonia.Dialogs
{
+ [TemplatePart("QuickLinks", typeof(Control))]
+ [TemplatePart("Files", typeof(ListBox))]
public class ManagedFileChooser : TemplatedControl
{
private Control _quickLinksRoot;
diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs
index f22fb4f317..083b16c107 100644
--- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs
@@ -133,9 +133,12 @@ namespace Avalonia.Headless
class HeadlessTextShaperStub : ITextShaperImpl
{
- public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
- CultureInfo culture, sbyte bidiLevel)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options)
{
+ var typeface = options.Typeface;
+ var fontRenderingEmSize = options.FontRenderingEmSize;
+ var bidiLevel = options.BidLevel;
+
return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
}
}
diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs
index 30301788bc..c7b5b8e27f 100644
--- a/src/Avalonia.Input/FocusManager.cs
+++ b/src/Avalonia.Input/FocusManager.cs
@@ -73,7 +73,7 @@ namespace Avalonia.Input
else if (Current != null)
{
// If control is null, set focus to the topmost focus scope.
- foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToList())
+ foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToArray())
{
if (scope != Scope &&
_focusScopes.TryGetValue(scope, out var element) &&
diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs
index 12ad182bf8..ed7ec5465c 100644
--- a/src/Avalonia.Input/TouchDevice.cs
+++ b/src/Avalonia.Input/TouchDevice.cs
@@ -114,7 +114,7 @@ namespace Avalonia.Input
{
if (_disposed)
return;
- var values = _pointers.Values.ToList();
+ var values = _pointers.Values.ToArray();
_pointers.Clear();
_disposed = true;
foreach (var p in values)
diff --git a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs
index 0060767565..30e8661e89 100644
--- a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs
+++ b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs
@@ -5,14 +5,27 @@ using System.Collections.Generic;
namespace Avalonia.Controls.Metadata
{
+ ///
+ /// Defines all pseudoclasses by name referenced and implemented by a control.
+ ///
+ ///
+ /// This is currently used for code-completion in certain IDEs.
+ ///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class PseudoClassesAttribute : Attribute
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The list of pseudoclass names.
public PseudoClassesAttribute(params string[] pseudoClasses)
{
PseudoClasses = pseudoClasses;
}
+ ///
+ /// Gets the list of pseudoclass names.
+ ///
public IReadOnlyList PseudoClasses { get; }
}
}
diff --git a/src/Avalonia.Styling/Controls/Metadata/TemplatePartAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/TemplatePartAttribute.cs
new file mode 100644
index 0000000000..3b8f971713
--- /dev/null
+++ b/src/Avalonia.Styling/Controls/Metadata/TemplatePartAttribute.cs
@@ -0,0 +1,55 @@
+// This source file is adapted from the Windows Presentation Foundation project.
+// (https://github.com/dotnet/wpf/)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+
+#nullable enable
+
+namespace Avalonia.Controls.Metadata
+{
+ ///
+ /// Defines a control template part referenced by name in code.
+ /// Template part names should begin with the "PART_" prefix.
+ ///
+ ///
+ /// Style authors should be able to identify the part type used for styling the specific control.
+ /// The part is usually required in the style and should have a specific predefined name.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+ public sealed class TemplatePartAttribute : Attribute
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TemplatePartAttribute()
+ {
+ Name = string.Empty;
+ Type = typeof(object);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The part name used by the class to identify a required element in the style.
+ /// The type of the element that should be used as a part with name.
+ public TemplatePartAttribute(string name, Type type)
+ {
+ Name = name;
+ Type = type;
+ }
+
+ ///
+ /// Gets or sets the part name used by the class to identify a required element in the style.
+ /// Template part names should begin with the "PART_" prefix.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets the type of the element that should be used as a part with name specified
+ /// in .
+ ///
+ public Type Type { get; set; }
+ }
+}
diff --git a/src/Avalonia.Themes.Default/Controls/Button.xaml b/src/Avalonia.Themes.Default/Controls/Button.xaml
index da36abe7ec..a2971c3ff6 100644
--- a/src/Avalonia.Themes.Default/Controls/Button.xaml
+++ b/src/Avalonia.Themes.Default/Controls/Button.xaml
@@ -18,7 +18,7 @@
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
RecognizesAccessKey="True"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
diff --git a/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml
index cf469eeac5..be6642467f 100644
--- a/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml
+++ b/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml
@@ -4,7 +4,7 @@
-
+
@@ -151,7 +151,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Content="{TemplateBinding Content}"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"/>
@@ -196,7 +196,7 @@
diff --git a/src/Avalonia.Themes.Default/Controls/DropDownButton.xaml b/src/Avalonia.Themes.Default/Controls/DropDownButton.xaml
new file mode 100644
index 0000000000..662c6379fb
--- /dev/null
+++ b/src/Avalonia.Themes.Default/Controls/DropDownButton.xaml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml
index a9a03c8ed5..47398966f7 100644
--- a/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml
+++ b/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml
@@ -24,7 +24,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
diff --git a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml
index ce20a1a165..0c46ce3724 100644
--- a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml
+++ b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml
@@ -16,6 +16,7 @@
24
24
1
+ 24
1
@@ -59,8 +60,11 @@
+
+
+
@@ -146,7 +150,7 @@
SplitButton /template/ Button#PART_SecondaryButton:pointerover /template/ ContentPresenter">
-
+
diff --git a/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml
index b14a239c35..17fb2af16c 100644
--- a/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml
+++ b/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml
@@ -18,7 +18,7 @@
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
RecognizesAccessKey="True"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml
index 6d2af1de57..468b723f5b 100644
--- a/src/Avalonia.Themes.Default/DefaultTheme.xaml
+++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml
@@ -3,8 +3,9 @@
x:Class="Avalonia.Themes.Default.DefaultTheme">
+
+
-
@@ -13,6 +14,7 @@
+
@@ -22,7 +24,6 @@
-
diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml
index 533fabfb44..8b53804d68 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml
@@ -4,7 +4,7 @@
-
+
@@ -19,9 +19,7 @@
-
-
-
+
@@ -45,40 +43,40 @@
-
+
-
+
-
-
diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
index a9c8281cf0..0d1dd03c6e 100644
--- a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
@@ -61,17 +61,17 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
index 12e148d2f9..649a186c7e 100644
--- a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
@@ -29,7 +29,7 @@
@@ -165,7 +165,7 @@
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
CornerRadius="{TemplateBinding CornerRadius}"/>
@@ -212,7 +212,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml
new file mode 100644
index 0000000000..a51e17db85
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 32
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
index c5bc7b6a38..24bdbca740 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
@@ -110,7 +110,7 @@
BorderThickness="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
- TextBlock.Foreground="{DynamicResource ExpanderForeground}" />
+ TextElement.Foreground="{DynamicResource ExpanderForeground}" />
+
+
@@ -12,6 +14,7 @@
+
@@ -21,7 +24,6 @@
-
diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
index e41e1b5fde..8011ed9daf 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
@@ -10,7 +10,7 @@
@@ -49,7 +49,7 @@
@@ -57,7 +57,7 @@
@@ -65,7 +65,7 @@
@@ -73,7 +73,7 @@
@@ -81,7 +81,7 @@
@@ -89,6 +89,6 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
index 831537f578..09f11d9c11 100644
--- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
@@ -218,7 +218,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml
index 5ee2dc527f..fd3e3b0ed6 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml
@@ -45,7 +45,7 @@
-
@@ -107,7 +107,7 @@
-
@@ -208,7 +208,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml
index 694e9ef579..0b9bfcb200 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml
@@ -36,9 +36,9 @@
Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
- TextBlock.FontFamily="{TemplateBinding FontFamily}"
- TextBlock.FontSize="{TemplateBinding FontSize}"
- TextBlock.FontWeight="{TemplateBinding FontWeight}" />
+ TextElement.FontFamily="{TemplateBinding FontFamily}"
+ TextElement.FontSize="{TemplateBinding FontSize}"
+ TextElement.FontWeight="{TemplateBinding FontWeight}" />
@@ -69,7 +69,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
index 9aa73fc52e..6733aa39fa 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
@@ -51,7 +51,7 @@
ContentTemplate="{TemplateBinding HeaderTemplate}"
Margin="{DynamicResource TimePickerTopHeaderMargin}"
MaxWidth="{DynamicResource TimePickerThemeMaxWidth}"
- TextBlock.Foreground="{DynamicResource TimePickerHeaderForeground}"
+ TextElement.Foreground="{DynamicResource TimePickerHeaderForeground}"
HorizontalAlignment="Stretch"
VerticalAlignment="Top" />
@@ -77,7 +77,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Content="{TemplateBinding Content}"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" />
@@ -139,7 +139,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
index e66392113f..059e041e25 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
@@ -107,7 +107,7 @@
@@ -116,7 +116,7 @@
@@ -125,7 +125,7 @@
@@ -134,7 +134,7 @@
@@ -143,7 +143,7 @@
@@ -152,7 +152,7 @@
@@ -161,7 +161,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml b/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml
index 2ff5284d0d..8d14c2d972 100644
--- a/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml
@@ -9,7 +9,7 @@
-
+
diff --git a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs
index 2a6eae3e38..62119a0051 100644
--- a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs
+++ b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs
@@ -41,7 +41,7 @@ namespace Avalonia.Animation
{
var transitionTasks = PageTransitions
.Select(transition => transition.Start(from, to, forward, cancellationToken))
- .ToList();
+ .ToArray();
return Task.WhenAll(transitionTasks);
}
}
diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt
index b725993b44..00246272e8 100644
--- a/src/Avalonia.Visuals/ApiCompatBaseline.txt
+++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt
@@ -67,6 +67,8 @@ MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableRadialG
TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract.
MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableSolidColorBrush..ctor(Avalonia.Media.Color, System.Double)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Media.Immutable.ImmutableTileBrush..ctor(Avalonia.Media.AlignmentX, Avalonia.Media.AlignmentY, Avalonia.RelativeRect, System.Double, Avalonia.RelativeRect, Avalonia.Media.Stretch, Avalonia.Media.TileMode, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.DrawableTextRun.Baseline' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.DrawableTextRun.Baseline.get()' is abstract in the implementation but is missing in the contract.
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.
@@ -85,7 +87,7 @@ MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextC
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.ShapedTextCharacters.SplitTextCharactersResult Avalonia.Media.TextFormatting.ShapedTextCharacters.Split(System.Int32)' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Media.TextFormatting.ShapedTextCharacters.SplitTextCharactersResult' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected System.Boolean Avalonia.Media.TextFormatting.TextCharacters.TryGetRunProperties(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, Avalonia.Media.Typeface, System.Int32)' does not exist in the implementation but it does exist in the contract.
-CannotAddAbstractMembers : Member 'public System.Collections.Generic.IReadOnlyList Avalonia.Media.TextFormatting.TextCollapsingProperties.Collapse(Avalonia.Media.TextFormatting.TextLine)' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Collections.Generic.List Avalonia.Media.TextFormatting.TextCollapsingProperties.Collapse(Avalonia.Media.TextFormatting.TextLine)' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextCollapsingStyle Avalonia.Media.TextFormatting.TextCollapsingProperties.Style.get()' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Media.TextFormatting.TextCollapsingStyle' 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.
@@ -94,8 +96,10 @@ MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.
MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.TextLayout.Size.get()' 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.Int32 Avalonia.Media.TextFormatting.TextLine.FirstTextSourceIndex' 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.Length' 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.
@@ -108,18 +112,23 @@ CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextForma
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.Int32 Avalonia.Media.TextFormatting.TextLine.FirstTextSourceIndex.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Collections.Generic.IReadOnlyList Avalonia.Media.TextFormatting.TextLine.GetTextBounds(System.Int32, System.Int32)' 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.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.Length.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.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextRange Avalonia.Media.TextFormatting.TextLine.TextRange.get()' does not exist in the implementation but it does exist 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.TextLineBreak..ctor(System.Collections.Generic.IReadOnlyList)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public System.Collections.Generic.IReadOnlyList Avalonia.Media.TextFormatting.TextLineBreak.RemainingCharacters.get()' does not exist in the implementation but it does exist 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.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.
@@ -130,8 +139,6 @@ CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextForma
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.
MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Media.TextFormatting.TextShaper.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
CannotSealType : Type 'Avalonia.Media.TextFormatting.TextTrailingCharacterEllipsis' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextTrailingCharacterEllipsis..ctor(System.Double, Avalonia.Media.TextFormatting.TextRunProperties)' does not exist in the implementation but it does exist in the contract.
@@ -171,9 +178,9 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avaloni
InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.IPlatformSettings.TouchDoubleClickSize.get()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime.get()' is present in the implementation but not in the contract.
-InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.TextFormatting.ShapedBuffer Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.GlyphTypeface, System.Double, System.Globalization.CultureInfo, System.SByte)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.TextFormatting.ShapedBuffer Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.TextFormatting.TextShaperOptions)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice..ctor(System.ReadOnlyMemory, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 177
+Total Issues: 184
diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs
index e974bbb100..aa730b3219 100644
--- a/src/Avalonia.Visuals/Media/Color.cs
+++ b/src/Avalonia.Visuals/Media/Color.cs
@@ -258,7 +258,7 @@ namespace Avalonia.Media
public override string ToString()
{
uint rgb = ToUint32();
- return KnownColors.GetKnownColorName(rgb) ?? $"#{rgb:x8}";
+ return KnownColors.GetKnownColorName(rgb) ?? $"#{rgb.ToString("x8", CultureInfo.InvariantCulture)}";
}
///
diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs
index cdea5f5fa3..0e485d0db8 100644
--- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs
+++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs
@@ -142,52 +142,6 @@ namespace Avalonia.Media
Color IExperimentalAcrylicMaterial.TintColor => _effectiveTintColor;
- private struct HsvColor
- {
- public float Hue { get; set; }
- public float Saturation { get; set; }
- public float Value { get; set; }
- }
-
- private static HsvColor RgbToHsv(Color color)
- {
- var r = color.R / 255.0f;
- var g = color.G / 255.0f;
- var b = color.B / 255.0f;
- var max = Math.Max(r, Math.Max(g, b));
- var min = Math.Min(r, Math.Min(g, b));
-
- float h, s, v;
- h = v = max;
-
- var d = max - min;
- s = max == 0 ? 0 : d / max;
-
- if (max == min)
- {
- h = 0; // achromatic
- }
- else
- {
- if (max == r)
- {
- h = (g - b) / d + (g < b ? 6 : 0);
- }
- else if (max == g)
- {
- h = (b - r) / d + 2;
- }
- else if (max == b)
- {
- h = (r - g) / d + 4;
- }
-
- h /= 6;
- }
-
- return new HsvColor { Hue = h, Saturation = s, Value = v };
- }
-
private static Color GetEffectiveTintColor(Color tintColor, double tintOpacity)
{
// Update tintColor's alpha with the combined opacity value
@@ -198,7 +152,7 @@ namespace Avalonia.Media
private static double GetTintOpacityModifier(Color tintColor)
{
- // This method supresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by
+ // This method suppresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by
// compressing the range of allowable values - for example, a user-defined value of 100% will be mapped to 45% for pure
// white (100% luminosity), 85% for pure black (0% luminosity), and 90% for pure gray (50% luminosity). The intensity of
// the effect increases linearly as luminosity deviates from 50%. After this effect is calculated, we cancel it out
@@ -210,22 +164,22 @@ namespace Avalonia.Media
const double midPointMaxOpacity = 0.45; // 50% luminosity
const double blackMaxOpacity = 0.45; // 0% luminosity
- var hsv = RgbToHsv(tintColor);
+ var hsv = tintColor.ToHsv();
double opacityModifier = midPointMaxOpacity;
- if (hsv.Value != midPoint)
+ if (hsv.V != midPoint)
{
// Determine maximum suppression amount
double lowestMaxOpacity = midPointMaxOpacity;
double maxDeviation = midPoint;
- if (hsv.Value > midPoint)
+ if (hsv.V > midPoint)
{
lowestMaxOpacity = whiteMaxOpacity; // At white (100% hsvV)
maxDeviation = 1 - maxDeviation;
}
- else if (hsv.Value < midPoint)
+ else if (hsv.V < midPoint)
{
lowestMaxOpacity = blackMaxOpacity; // At black (0% hsvV)
}
@@ -233,14 +187,14 @@ namespace Avalonia.Media
double maxOpacitySuppression = midPointMaxOpacity - lowestMaxOpacity;
// Determine normalized deviation from the midpoint
- double deviation = Math.Abs(hsv.Value - midPoint);
+ double deviation = Math.Abs(hsv.V - midPoint);
double normalizedDeviation = deviation / maxDeviation;
// If we have saturation, reduce opacity suppression to allow that color to come through more
- if (hsv.Saturation > 0)
+ if (hsv.S > 0)
{
// Dampen opacity suppression based on how much saturation there is
- maxOpacitySuppression *= Math.Max(1 - (hsv.Saturation * 2), 0.0);
+ maxOpacitySuppression *= Math.Max(1 - (hsv.S * 2), 0.0);
}
double opacitySuppression = maxOpacitySuppression * normalizedDeviation;
diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs
index 1cac3243e3..7bdf59def0 100644
--- a/src/Avalonia.Visuals/Media/FormattedText.cs
+++ b/src/Avalonia.Visuals/Media/FormattedText.cs
@@ -56,7 +56,7 @@ namespace Avalonia.Media
FlowDirection flowDirection,
Typeface typeface,
double emSize,
- IBrush foreground)
+ IBrush? foreground)
{
if (culture is null)
{
@@ -160,7 +160,7 @@ namespace Avalonia.Media
/// Foreground brush
/// The start index of initial character to apply the change to.
/// The number of characters the change should be applied to.
- public void SetForegroundBrush(IBrush foregroundBrush, int startIndex, int count)
+ public void SetForegroundBrush(IBrush? foregroundBrush, int startIndex, int count)
{
var limit = ValidateRange(startIndex, count);
for (var i = startIndex; i < limit;)
@@ -768,7 +768,7 @@ namespace Avalonia.Media
// as a result of the next line measurement
// maybe there is no next line at all
- if (Position + Current.TextRange.Length < _that._text.Length)
+ if (Position + Current.Length < _that._text.Length)
{
bool nextLineFits;
@@ -780,7 +780,7 @@ namespace Avalonia.Media
{
_nextLine = FormatLine(
_textSource,
- Position + Current.TextRange.Length,
+ Position + Current.Length,
MaxLineLength(_lineCount + 1),
_that._defaultParaProps,
currentLineBreak
@@ -819,7 +819,7 @@ namespace Avalonia.Media
_previousHeight = Current.Height;
- Length = Current.TextRange.Length;
+ Length = Current.Length;
_previousLineBreak = currentLineBreak;
@@ -839,17 +839,17 @@ namespace Avalonia.Media
lineBreak
);
- if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.TextRange.Length > 0)
+ if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0)
{
// what I really need here is the last displayed text run of the line
// textSourcePosition + line.Length - 1 works except the end of paragraph case,
// where line length includes the fake paragraph break run
- Debug.Assert(_that._text.Length > 0 && textSourcePosition + line.TextRange.Length <= _that._text.Length + 1);
+ Debug.Assert(_that._text.Length > 0 && textSourcePosition + line.Length <= _that._text.Length + 1);
var thatFormatRider = new SpanRider(
_that._formatRuns,
_that._latestPosition,
- Math.Min(textSourcePosition + line.TextRange.Length - 1, _that._text.Length - 1)
+ Math.Min(textSourcePosition + line.Length - 1, _that._text.Length - 1)
);
var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
diff --git a/src/Avalonia.Visuals/Media/GradientStops.cs b/src/Avalonia.Visuals/Media/GradientStops.cs
index efc11bacd6..c3d0bb5ceb 100644
--- a/src/Avalonia.Visuals/Media/GradientStops.cs
+++ b/src/Avalonia.Visuals/Media/GradientStops.cs
@@ -17,7 +17,7 @@ namespace Avalonia.Media
public IReadOnlyList ToImmutable()
{
- return this.Select(x => new ImmutableGradientStop(x.Offset, x.Color)).ToList();
+ return this.Select(x => new ImmutableGradientStop(x.Offset, x.Color)).ToArray();
}
}
}
diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs
index 8b2f10d088..4a0277b4d4 100644
--- a/src/Avalonia.Visuals/Media/HsvColor.cs
+++ b/src/Avalonia.Visuals/Media/HsvColor.cs
@@ -4,6 +4,8 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
+using System.Globalization;
+using System.Text;
using Avalonia.Utilities;
namespace Avalonia.Media
@@ -20,7 +22,8 @@ namespace Avalonia.Media
/// Initializes a new instance of the struct.
///
/// The Alpha (transparency) channel value in the range from 0..1.
- /// The Hue channel value in the range from 0..360.
+ /// The Hue channel value in the range from 0..360.
+ /// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// The Saturation channel value in the range from 0..1.
/// The Value channel value in the range from 0..1.
public HsvColor(
@@ -33,6 +36,12 @@ namespace Avalonia.Media
H = MathUtilities.Clamp(hue, 0.0, 360.0);
S = MathUtilities.Clamp(saturation, 0.0, 1.0);
V = MathUtilities.Clamp(value, 0.0, 1.0);
+
+ // The maximum value of Hue is technically 360 minus epsilon (just below 360).
+ // This is because, in a color circle, 360 degrees is equivalent to 0 degrees.
+ // However, that is too tricky to work with in code and isn't as intuitive.
+ // Therefore, since 360 == 0, just wrap 360 if needed back to 0.
+ H = (H == 360.0 ? 0 : H);
}
///
@@ -43,7 +52,8 @@ namespace Avalonia.Media
/// Whether or not the channel values are in the correct ranges must be known.
///
/// The Alpha (transparency) channel value in the range from 0..1.
- /// The Hue channel value in the range from 0..360.
+ /// The Hue channel value in the range from 0..360.
+ /// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// The Saturation channel value in the range from 0..1.
/// The Value channel value in the range from 0..1.
/// Whether to clamp channel values to their required ranges.
@@ -60,6 +70,9 @@ namespace Avalonia.Media
H = MathUtilities.Clamp(hue, 0.0, 360.0);
S = MathUtilities.Clamp(saturation, 0.0, 1.0);
V = MathUtilities.Clamp(value, 0.0, 1.0);
+
+ // See comments in constructor above
+ H = (H == 360.0 ? 0 : H);
}
else
{
@@ -91,6 +104,7 @@ namespace Avalonia.Media
///
/// Gets the Hue channel value in the range from 0..360.
+ /// Note that 360 is equivalent to 0 and will be adjusted automatically.
///
public double H { get; }
@@ -155,6 +169,129 @@ namespace Avalonia.Media
return HsvColor.ToRgb(H, S, V, A);
}
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+
+ // Use a format similar to HSL in HTML/CSS "hsla(0, 100%, 50%, 0.5)"
+ //
+ // However:
+ // - To ensure precision is never lost, allow decimal places
+ // - To maintain numerical consistency do not use percent
+ //
+ // Example:
+ //
+ // hsva(hue, saturation, value, alpha)
+ // hsva(230, 1.0, 0.5, 1.0)
+ //
+ // Where:
+ //
+ // hue : double from 0 to 360
+ // saturation : double from 0 to 1
+ // (HTML uses a percentage)
+ // value : double from 0 to 1
+ // (HTML uses a percentage)
+ // alpha : double from 0 to 1
+ // (HTML does not use a percentage for alpha)
+
+ sb.Append("hsva(");
+ sb.Append(H.ToString(CultureInfo.InvariantCulture));
+ sb.Append(", ");
+ sb.Append(S.ToString(CultureInfo.InvariantCulture));
+ sb.Append(", ");
+ sb.Append(V.ToString(CultureInfo.InvariantCulture));
+ sb.Append(", ");
+ sb.Append(A.ToString(CultureInfo.InvariantCulture));
+ sb.Append(')');
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Parses an HSV color string.
+ ///
+ /// The HSV color string to parse.
+ /// The parsed .
+ public static HsvColor Parse(string s)
+ {
+ if (s is null)
+ {
+ throw new ArgumentNullException(nameof(s));
+ }
+
+ if (TryParse(s, out HsvColor hsvColor))
+ {
+ return hsvColor;
+ }
+
+ throw new FormatException($"Invalid HSV color string: '{s}'.");
+ }
+
+ ///
+ /// Parses an HSV color string.
+ ///
+ /// The HSV color string to parse.
+ /// The parsed .
+ /// True if parsing was successful; otherwise, false.
+ public static bool TryParse(string s, out HsvColor hsvColor)
+ {
+ hsvColor = default;
+
+ if (s is null)
+ {
+ return false;
+ }
+
+ string workingString = s.Trim();
+
+ if (workingString.Length == 0 ||
+ workingString.IndexOf(",", StringComparison.Ordinal) < 0)
+ {
+ return false;
+ }
+
+ if (workingString.Length > 6 &&
+ workingString.StartsWith("hsva(", StringComparison.OrdinalIgnoreCase) &&
+ workingString.EndsWith(")", StringComparison.Ordinal))
+ {
+ workingString = workingString.Substring(5, workingString.Length - 6);
+ }
+
+ if (workingString.Length > 5 &&
+ workingString.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) &&
+ workingString.EndsWith(")", StringComparison.Ordinal))
+ {
+ workingString = workingString.Substring(4, workingString.Length - 5);
+ }
+
+ string[] components = workingString.Split(',');
+
+ if (components.Length == 3) // HSV
+ {
+ if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
+ double.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out double saturation) &&
+ double.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out double value))
+ {
+ hsvColor = new HsvColor(1.0, hue, saturation, value);
+ return true;
+ }
+ }
+ else if (components.Length == 4) // HSVA
+ {
+ if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
+ double.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out double saturation) &&
+ double.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out double value) &&
+ double.TryParse(components[3], NumberStyles.Number, CultureInfo.InvariantCulture, out double alpha))
+ {
+ hsvColor = new HsvColor(alpha, hue, saturation, value);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
///
/// Creates a new from individual color channel values.
///
diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs
index 88aba8365e..ed6065eae4 100644
--- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs
+++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs
@@ -41,7 +41,7 @@ namespace Avalonia.Media
///
/// The destination point.
/// The radii of an oval whose perimeter is used to draw the angle.
- /// The rotation angle of the oval that specifies the curve.
+ /// The rotation angle (in radians) of the oval that specifies the curve.
/// true to draw the arc greater than 180 degrees; otherwise, false.
///
/// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
@@ -59,7 +59,7 @@ namespace Avalonia.Media
///
/// The destination point.
/// The radii of an oval whose perimeter is used to draw the angle.
- /// The rotation angle of the oval that specifies the curve.
+ /// The rotation angle (in radians) of the oval that specifies the curve.
/// true to draw the arc greater than 180 degrees; otherwise, false.
///
/// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
diff --git a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs
index 2dced2252e..2d7bd17b20 100644
--- a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs
+++ b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs
@@ -10,6 +10,16 @@ namespace Avalonia.Media
///
public class TextDecorationCollection : AvaloniaList
{
+ public TextDecorationCollection()
+ {
+
+ }
+
+ public TextDecorationCollection(IEnumerable textDecorations) : base(textDecorations)
+ {
+
+ }
+
///
/// Parses a string.
///
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
index 3757a4506a..e63ed421ba 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
@@ -9,6 +9,11 @@
/// Gets the size.
///
public abstract Size Size { get; }
+
+ ///
+ /// Run baseline in ratio relative to run height
+ ///
+ public abstract double Baseline { get; }
///
/// Draws the at the given origin.
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
index 1b0feaa718..97df87d3d9 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
@@ -30,10 +29,10 @@ namespace Avalonia.Media.TextFormatting
if (runText.IsEmpty)
{
- return new TextEndOfParagraph();
+ return null;
}
- var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier);
+ var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier);
return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
}
@@ -42,17 +41,18 @@ namespace Avalonia.Media.TextFormatting
/// Creates a span of text run properties that has modifier applied.
///
/// The text to create the properties for.
+ /// The first text source index.
/// The default text properties.
/// The text properties modifier.
///
/// The created text style run.
///
- private static ValueSpan CreateTextStyleRun(ReadOnlySlice text,
+ private static ValueSpan CreateTextStyleRun(ReadOnlySlice text, int firstTextSourceIndex,
TextRunProperties defaultProperties, IReadOnlyList>? textModifier)
{
if (textModifier == null || textModifier.Count == 0)
{
- return new ValueSpan(text.Start, text.Length, defaultProperties);
+ return new ValueSpan(firstTextSourceIndex, text.Length, defaultProperties);
}
var currentProperties = defaultProperties;
@@ -69,28 +69,28 @@ namespace Avalonia.Media.TextFormatting
var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
- if (textRange.Start + textRange.Length <= text.Start)
+ if (textRange.Start + textRange.Length <= firstTextSourceIndex)
{
continue;
}
- if (textRange.Start > text.End)
+ if (textRange.Start > firstTextSourceIndex + text.Length)
{
length = text.Length;
break;
}
- if (textRange.Start > text.Start)
+ if (textRange.Start > firstTextSourceIndex)
{
if (propertiesOverride.Value != currentProperties)
{
- length = Math.Min(Math.Abs(textRange.Start - text.Start), text.Length);
+ length = Math.Min(Math.Abs(textRange.Start - firstTextSourceIndex), text.Length);
break;
}
}
- length += Math.Max(0, textRange.Start + textRange.Length - text.Start);
+ length = Math.Max(0, textRange.Start + textRange.Length - firstTextSourceIndex);
if (hasOverride)
{
@@ -116,12 +116,7 @@ namespace Avalonia.Media.TextFormatting
length = text.Length;
}
- if (length != text.Length)
- {
- text = text.Take(length);
- }
-
- return new ValueSpan(text.Start, length, currentProperties);
+ return new ValueSpan(firstTextSourceIndex, length, currentProperties);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
index fb85766003..53287a264d 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
@@ -38,6 +38,8 @@ namespace Avalonia.Media.TextFormatting
public FontMetrics FontMetrics { get; }
+ public override double Baseline => -FontMetrics.Ascent;
+
public override Size Size => GlyphRun.Size;
public GlyphRun GlyphRun
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextBounds.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextBounds.cs
new file mode 100644
index 0000000000..a0b51671f0
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextBounds.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Media.TextFormatting
+{
+ ///
+ /// The bounding rectangle of a range of characters
+ ///
+ public sealed class TextBounds
+ {
+ ///
+ /// Constructing TextBounds object
+ ///
+ internal TextBounds(Rect bounds, FlowDirection flowDirection)
+ {
+ Rectangle = bounds;
+ FlowDirection = flowDirection;
+ }
+
+ ///
+ /// Bounds rectangle
+ ///
+ public Rect Rectangle { get; }
+
+ ///
+ /// Text flow direction inside the boundary rectangle
+ ///
+ public FlowDirection FlowDirection { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
index c4b2dfb3a5..ab72601c3e 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
@@ -38,7 +38,7 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of .
///
/// The shapeable text characters.
- internal IList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel,
+ internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel,
ref TextRunProperties? previousProperties)
{
var shapeableCharacters = new List(2);
@@ -72,11 +72,11 @@ namespace Avalonia.Media.TextFormatting
var currentTypeface = defaultTypeface;
var previousTypeface = previousProperties?.Typeface;
- if (TryGetShapeableLength(text, currentTypeface, out var count, out var script))
+ if (TryGetShapeableLength(text, currentTypeface, null, out var count, out var script))
{
if (script == Script.Common && previousTypeface is not null)
{
- if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _))
+ if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _))
{
return new ShapeableTextCharacters(text.Take(fallbackCount),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
@@ -89,7 +89,7 @@ namespace Avalonia.Media.TextFormatting
if (previousTypeface is not null)
{
- if(TryGetShapeableLength(text, previousTypeface.Value, out count, out _))
+ if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _))
{
return new ShapeableTextCharacters(text.Take(count),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
@@ -118,7 +118,7 @@ namespace Avalonia.Media.TextFormatting
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out currentTypeface);
- if (matchFound && TryGetShapeableLength(text, currentTypeface, out count, out _))
+ if (matchFound && TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count, out _))
{
//Fallback found
return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
@@ -152,14 +152,19 @@ namespace Avalonia.Media.TextFormatting
///
/// The text.
/// The typeface that is used to find matching characters.
+ ///
/// The shapeable length.
///
///
- protected static bool TryGetShapeableLength(ReadOnlySlice text, Typeface typeface, out int length,
+ protected static bool TryGetShapeableLength(
+ ReadOnlySlice text,
+ Typeface typeface,
+ Typeface? defaultTypeface,
+ out int length,
out Script script)
{
length = 0;
- script = Script.Unknown;
+ script = Script.Unknown;
if (text.Length == 0)
{
@@ -167,6 +172,7 @@ namespace Avalonia.Media.TextFormatting
}
var font = typeface.GlyphTypeface;
+ var defaultFont = defaultTypeface?.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text);
@@ -176,6 +182,11 @@ namespace Avalonia.Media.TextFormatting
var currentScript = currentGrapheme.FirstCodepoint.Script;
+ if (currentScript != Script.Common && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
+ {
+ break;
+ }
+
//Stop at the first missing glyph
if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
index a46f9537d0..f677617b14 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
@@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting
/// Collapses given text line.
///
/// Text line to collapse.
- public abstract IReadOnlyList? Collapse(TextLine textLine);
+ public abstract List? Collapse(TextLine textLine);
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs
index 2031c2ec99..5a2169630b 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs
@@ -3,13 +3,11 @@ using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Media.TextFormatting
{
- internal class TextEllipsisHelper
+ internal static class TextEllipsisHelper
{
- public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
+ public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
{
- var shapedTextRuns = textLine.TextRuns as List;
-
- if (shapedTextRuns is null)
+ if (textLine.TextRuns is not List textRuns || textRuns.Count == 0)
{
return null;
}
@@ -17,74 +15,103 @@ namespace Avalonia.Media.TextFormatting
var runIndex = 0;
var currentWidth = 0.0;
var collapsedLength = 0;
- var textRange = textLine.TextRange;
var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight);
if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
{
- return new List(0);
+ //Not enough space to fit in the symbol
+ return new List(0);
}
var availableWidth = properties.Width - shapedSymbol.Size.Width;
- while (runIndex < shapedTextRuns.Count)
+ while (runIndex < textRuns.Count)
{
- var currentRun = shapedTextRuns[runIndex];
-
- currentWidth += currentRun.Size.Width;
+ var currentRun = textRuns[runIndex];
- if (currentWidth > availableWidth)
+ switch (currentRun)
{
- if (currentRun.TryMeasureCharacters(availableWidth, out var measuredLength))
+ case ShapedTextCharacters shapedRun:
{
- if (isWordEllipsis && measuredLength < textRange.End)
+ currentWidth += shapedRun.Size.Width;
+
+ if (currentWidth > availableWidth)
{
- var currentBreakPosition = 0;
+ if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
+ {
+ if (isWordEllipsis && measuredLength < textLine.Length)
+ {
+ var currentBreakPosition = 0;
- var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+ var lineBreaker = new LineBreakEnumerator(currentRun.Text);
- while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
- {
- var nextBreakPosition = lineBreaker.Current.PositionMeasure;
+ while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
+ {
+ var nextBreakPosition = lineBreaker.Current.PositionMeasure;
- if (nextBreakPosition == 0)
- {
- break;
- }
+ if (nextBreakPosition == 0)
+ {
+ break;
+ }
- if (nextBreakPosition >= measuredLength)
- {
- break;
+ if (nextBreakPosition >= measuredLength)
+ {
+ break;
+ }
+
+ currentBreakPosition = nextBreakPosition;
+ }
+
+ measuredLength = currentBreakPosition;
}
+ }
+
+ collapsedLength += measuredLength;
- currentBreakPosition = nextBreakPosition;
+ var collapsedRuns = new List(textRuns.Count);
+
+ if (collapsedLength > 0)
+ {
+ var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+
+ collapsedRuns.AddRange(splitResult.First);
}
- measuredLength = currentBreakPosition;
+ collapsedRuns.Add(shapedSymbol);
+
+ return collapsedRuns;
}
- }
- collapsedLength += measuredLength;
+ availableWidth -= currentRun.Size.Width;
- var shapedTextCharacters = new List(shapedTextRuns.Count);
+
+ break;
+ }
- if (collapsedLength > 0)
+ case { } drawableRun:
{
- var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, collapsedLength);
+ //The whole run needs to fit into available space
+ if (currentWidth + drawableRun.Size.Width > availableWidth)
+ {
+ var collapsedRuns = new List(textRuns.Count);
- shapedTextCharacters.AddRange(splitResult.First);
+ if (collapsedLength > 0)
+ {
+ var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
- TextLineImpl.SortRuns(shapedTextCharacters);
- }
+ collapsedRuns.AddRange(splitResult.First);
+ }
- shapedTextCharacters.Add(shapedSymbol);
+ collapsedRuns.Add(shapedSymbol);
- return shapedTextCharacters;
+ return collapsedRuns;
+ }
+
+ break;
+ }
}
- availableWidth -= currentRun.Size.Width;
-
- collapsedLength += currentRun.GlyphRun.Characters.Length;
+ collapsedLength += currentRun.TextSourceLength;
runIndex++;
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
index 13ed850715..7c60f73b8d 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
-using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
@@ -17,22 +15,22 @@ namespace Avalonia.Media.TextFormatting
var textWrapping = paragraphProperties.TextWrapping;
FlowDirection flowDirection;
TextLineBreak? nextLineBreak = null;
- List shapedRuns;
+ List drawableTextRuns;
var textRuns = FetchTextRuns(textSource, firstTextSourceIndex,
- out var textEndOfLine, out var textRange);
+ out var textEndOfLine, out var textSourceLength);
- if (previousLineBreak?.RemainingCharacters != null)
+ if (previousLineBreak?.RemainingRuns != null)
{
flowDirection = previousLineBreak.FlowDirection;
- shapedRuns = previousLineBreak.RemainingCharacters.ToList();
+ drawableTextRuns = previousLineBreak.RemainingRuns.ToList();
nextLineBreak = previousLineBreak;
}
else
{
- shapedRuns = ShapeTextRuns(textRuns, paragraphProperties.FlowDirection,out flowDirection);
+ drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out flowDirection);
- if(nextLineBreak == null && textEndOfLine != null)
+ if (nextLineBreak == null && textEndOfLine != null)
{
nextLineBreak = new TextLineBreak(textEndOfLine, flowDirection);
}
@@ -44,10 +42,8 @@ namespace Avalonia.Media.TextFormatting
{
case TextWrapping.NoWrap:
{
- TextLineImpl.SortRuns(shapedRuns);
-
- textLine = new TextLineImpl(shapedRuns, textRange, paragraphWidth, paragraphProperties,
- flowDirection, nextLineBreak);
+ textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength,
+ paragraphWidth, paragraphProperties, flowDirection, nextLineBreak);
textLine.FinalizeLine();
@@ -56,7 +52,7 @@ namespace Avalonia.Media.TextFormatting
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
- textLine = PerformTextWrapping(shapedRuns, textRange, paragraphWidth, paragraphProperties,
+ textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
flowDirection, nextLineBreak);
break;
}
@@ -73,7 +69,7 @@ namespace Avalonia.Media.TextFormatting
/// The text run's.
/// The length to split at.
/// The split text runs.
- internal static SplitResult> SplitShapedRuns(List textRuns, int length)
+ internal static SplitResult> SplitDrawableRuns(List textRuns, int length)
{
var currentLength = 0;
@@ -83,13 +79,14 @@ namespace Avalonia.Media.TextFormatting
if (currentLength + currentRun.Text.Length < length)
{
- currentLength += currentRun.Text.Length;
+ currentLength += currentRun.TextSourceLength;
+
continue;
}
var firstCount = currentRun.Text.Length >= 1 ? i + 1 : i;
- var first = new List(firstCount);
+ var first = new List(firstCount);
if (firstCount > 1)
{
@@ -103,7 +100,7 @@ namespace Avalonia.Media.TextFormatting
if (currentLength + currentRun.Text.Length == length)
{
- var second = secondCount > 0 ? new List(secondCount) : null;
+ var second = secondCount > 0 ? new List(secondCount) : null;
if (second != null)
{
@@ -117,15 +114,20 @@ namespace Avalonia.Media.TextFormatting
first.Add(currentRun);
- return new SplitResult>(first, second);
+ return new SplitResult>(first, second);
}
else
{
secondCount++;
- var second = new List(secondCount);
+ var second = new List(secondCount);
+
+ if (currentRun is not ShapedTextCharacters shapedTextCharacters)
+ {
+ throw new NotSupportedException("Only shaped runs can be split in between.");
+ }
- var split = currentRun.Split(length - currentLength);
+ var split = shapedTextCharacters.Split(length - currentLength);
first.Add(split.First);
@@ -136,32 +138,43 @@ namespace Avalonia.Media.TextFormatting
second.Add(textRuns[i + j]);
}
- return new SplitResult>(first, second);
+ return new SplitResult>(first, second);
}
}
- return new SplitResult>(textRuns, null);
+ return new SplitResult>(textRuns, null);
}
///
/// Shape specified text runs with specified paragraph embedding.
///
/// The text runs to shape.
- /// The paragraph embedding level.
+ /// The default paragraph properties.
/// The resolved flow direction.
///
/// A list of shaped text characters.
///
- private static List ShapeTextRuns(List textRuns,
- FlowDirection flowDirection, out FlowDirection resolvedFlowDirection)
+ private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties,
+ out FlowDirection resolvedFlowDirection)
{
- var shapedTextCharacters = new List();
+ var flowDirection = paragraphProperties.FlowDirection;
+ var drawableTextRuns = new List();
var biDiData = new BidiData((sbyte)flowDirection);
foreach (var textRun in textRuns)
{
- biDiData.Append(textRun.Text);
+ if (textRun.Text.IsEmpty)
+ {
+ var text = new char[textRun.TextSourceLength];
+
+ biDiData.Append(text);
+ }
+ else
+ {
+ biDiData.Append(textRun.Text);
+ }
+
}
var biDi = BidiAlgorithm.Instance.Value!;
@@ -173,68 +186,90 @@ namespace Avalonia.Media.TextFormatting
resolvedFlowDirection =
(resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
- var shapeableRuns = new List(textRuns.Count);
+ var processedRuns = new List(textRuns.Count);
foreach (var coalescedRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels))
{
- shapeableRuns.AddRange(coalescedRuns);
+ processedRuns.AddRange(coalescedRuns);
}
- for (var index = 0; index < shapeableRuns.Count; index++)
+ for (var index = 0; index < processedRuns.Count; index++)
{
- var currentRun = shapeableRuns[index];
- var groupedRuns = new List(2) { currentRun };
- var text = currentRun.Text;
- var start = currentRun.Text.Start;
- var length = currentRun.Text.Length;
- var bufferOffset = currentRun.Text.BufferOffset;
-
- while (index + 1 < shapeableRuns.Count)
- {
- var nextRun = shapeableRuns[index + 1];
-
- if (currentRun.CanShapeTogether(nextRun))
- {
- groupedRuns.Add(nextRun);
+ var currentRun = processedRuns[index];
- length += nextRun.Text.Length;
-
- if (start > nextRun.Text.Start)
+ switch (currentRun)
+ {
+ case DrawableTextRun drawableRun:
{
- start = nextRun.Text.Start;
+ drawableTextRuns.Add(drawableRun);
+
+ break;
}
- if (bufferOffset > nextRun.Text.BufferOffset)
+ case ShapeableTextCharacters shapeableRun:
{
- bufferOffset = nextRun.Text.BufferOffset;
- }
+ var groupedRuns = new List(2) { shapeableRun };
+ var text = currentRun.Text;
+ var start = currentRun.Text.Start;
+ var length = currentRun.Text.Length;
+ var bufferOffset = currentRun.Text.BufferOffset;
- text = new ReadOnlySlice(text.Buffer, start, length, bufferOffset);
-
- index++;
+ while (index + 1 < processedRuns.Count)
+ {
+ if (processedRuns[index + 1] is not ShapeableTextCharacters nextRun)
+ {
+ break;
+ }
- currentRun = nextRun;
+ if (shapeableRun.CanShapeTogether(nextRun))
+ {
+ groupedRuns.Add(nextRun);
- continue;
- }
+ length += nextRun.Text.Length;
- break;
- }
+ if (start > nextRun.Text.Start)
+ {
+ start = nextRun.Text.Start;
+ }
+
+ if (bufferOffset > nextRun.Text.BufferOffset)
+ {
+ bufferOffset = nextRun.Text.BufferOffset;
+ }
+
+ text = new ReadOnlySlice(text.Buffer, start, length, bufferOffset);
- shapedTextCharacters.AddRange(ShapeTogether(groupedRuns, text));
+ index++;
+
+ shapeableRun = nextRun;
+
+ continue;
+ }
+
+ break;
+ }
+
+ var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
+ currentRun.Properties.FontRenderingEmSize,
+ shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab);
+
+ drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
+
+ break;
+ }
+ }
}
- return shapedTextCharacters;
+ return drawableTextRuns;
}
private static IReadOnlyList ShapeTogether(
- IReadOnlyList textRuns, ReadOnlySlice text)
+ IReadOnlyList textRuns, ReadOnlySlice text, TextShaperOptions options)
{
var shapedRuns = new List(textRuns.Count);
var firstRun = textRuns[0];
- var shapedBuffer = TextShaper.Current.ShapeText(text, firstRun.Properties.Typeface.GlyphTypeface,
- firstRun.Properties.FontRenderingEmSize, firstRun.Properties.CultureInfo, firstRun.BidiLevel);
+ var shapedBuffer = TextShaper.Current.ShapeText(text, options);
for (var i = 0; i < textRuns.Count; i++)
{
@@ -256,8 +291,8 @@ namespace Avalonia.Media.TextFormatting
/// The text characters to form from.
/// The bidi levels.
///
- private static IEnumerable> CoalesceLevels(
- IReadOnlyList textCharacters,
+ private static IEnumerable> CoalesceLevels(
+ IReadOnlyList textCharacters,
ReadOnlySlice levels)
{
if (levels.Length == 0)
@@ -275,7 +310,19 @@ namespace Avalonia.Media.TextFormatting
for (var i = 0; i < textCharacters.Count; i++)
{
var j = 0;
- currentRun = textCharacters[i];
+ currentRun = textCharacters[i] as TextCharacters;
+
+ if (currentRun == null)
+ {
+ var drawableRun = textCharacters[i];
+
+ yield return new[] { drawableRun };
+
+ levelIndex += drawableRun.TextSourceLength;
+
+ continue;
+ }
+
runText = currentRun.Text;
for (; j < runText.Length;)
@@ -330,18 +377,18 @@ namespace Avalonia.Media.TextFormatting
/// The text source.
/// The first text source index.
///
- ///
+ ///
///
/// The formatted text runs.
///
- private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
- out TextEndOfLine? endOfLine, out TextRange textRange)
+ private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
+ out TextEndOfLine? endOfLine, out int textSourceLength)
{
- var length = 0;
+ textSourceLength = 0;
endOfLine = null;
- var textRuns = new List();
+ var textRuns = new List();
var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex);
@@ -349,8 +396,19 @@ namespace Avalonia.Media.TextFormatting
{
var textRun = textRunEnumerator.Current;
- if(textRun == null)
+ if (textRun == null)
+ {
+ break;
+ }
+
+ if (textRun is TextEndOfLine textEndOfLine)
{
+ endOfLine = textEndOfLine;
+
+ textRuns.Add(textRun);
+
+ textSourceLength += textRun.TextSourceLength;
+
break;
}
@@ -365,9 +423,7 @@ namespace Avalonia.Media.TextFormatting
textRuns.Add(splitResult);
- length += runLineBreak.PositionWrap;
-
- textRange = new TextRange(firstTextSourceIndex, length);
+ textSourceLength += runLineBreak.PositionWrap;
return textRuns;
}
@@ -376,16 +432,16 @@ namespace Avalonia.Media.TextFormatting
break;
}
- case TextEndOfLine textEndOfLine:
- endOfLine = textEndOfLine;
- break;
+ case DrawableTextRun drawableTextRun:
+ {
+ textRuns.Add(drawableTextRun);
+ break;
+ }
}
- length += textRun.Text.Length;
+ textSourceLength += textRun.TextSourceLength;
}
- textRange = new TextRange(firstTextSourceIndex, length);
-
return textRuns;
}
@@ -415,48 +471,74 @@ namespace Avalonia.Media.TextFormatting
return false;
}
- private static int MeasureLength(IReadOnlyList textRuns, TextRange textRange,
- double paragraphWidth)
+ private static bool TryMeasureLength(IReadOnlyList textRuns, int firstTextSourceIndex, double paragraphWidth, out int measuredLength)
{
+ measuredLength = 0;
var currentWidth = 0.0;
- var lastCluster = textRange.Start;
+ var lastCluster = firstTextSourceIndex;
foreach (var currentRun in textRuns)
{
- for (var i = 0; i < currentRun.ShapedBuffer.Length; i++)
+ switch (currentRun)
{
- var glyphInfo = currentRun.ShapedBuffer[i];
+ case ShapedTextCharacters shapedTextCharacters:
+ {
+ for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
+ {
+ var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
- if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
- {
- var measuredLength = lastCluster - textRange.Start;
+ if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
+ {
+ goto found;
+ }
- return measuredLength == 0 ? 1 : measuredLength;
- }
+ lastCluster = glyphInfo.GlyphCluster;
+ currentWidth += glyphInfo.GlyphAdvance;
+ }
+
+ break;
+ }
+
+ case { } drawableTextRun:
+ {
+ if (currentWidth + drawableTextRun.Size.Width > paragraphWidth)
+ {
+ goto found;
+ }
- lastCluster = glyphInfo.GlyphCluster;
- currentWidth += glyphInfo.GlyphAdvance;
+ lastCluster += currentRun.TextSourceLength;
+ currentWidth += currentRun.Size.Width;
+
+ break;
+ }
}
}
- return textRange.Length;
+ found:
+
+ measuredLength = Math.Max(0, lastCluster - firstTextSourceIndex + 1);
+
+ return measuredLength != 0;
}
///
/// Performs text wrapping returns a list of text lines.
///
///
- /// The text range that is covered by the text runs.
+ /// The first text source index.
/// The paragraph width.
/// The text paragraph properties.
///
/// The current line break if the line was explicitly broken.
/// The wrapped text line.
- private static TextLineImpl PerformTextWrapping(List textRuns, TextRange textRange,
+ private static TextLineImpl PerformTextWrapping(List textRuns, int firstTextSourceIndex,
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection,
TextLineBreak? currentLineBreak)
{
- var measuredLength = MeasureLength(textRuns, textRange, paragraphWidth);
+ if (!TryMeasureLength(textRuns, firstTextSourceIndex, paragraphWidth, out var measuredLength))
+ {
+ measuredLength = 1;
+ }
var currentLength = 0;
@@ -568,9 +650,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
- var splitResult = SplitShapedRuns(textRuns, measuredLength);
-
- textRange = new TextRange(textRange.Start, measuredLength);
+ var splitResult = SplitDrawableRuns(textRuns, measuredLength);
var remainingCharacters = splitResult.Second;
@@ -583,9 +663,8 @@ namespace Avalonia.Media.TextFormatting
lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, flowDirection);
}
- TextLineImpl.SortRuns(splitResult.First);
-
- var textLine = new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, flowDirection,
+ var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength,
+ paragraphWidth, paragraphProperties, flowDirection,
lineBreak);
return textLine.FinalizeLine();
@@ -644,7 +723,9 @@ namespace Avalonia.Media.TextFormatting
var cultureInfo = textRun.Properties.CultureInfo;
- var shapedBuffer = textShaper.ShapeText(textRun.Text, glyphTypeface, fontRenderingEmSize, cultureInfo, (sbyte)flowDirection);
+ var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo);
+
+ var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);
return new ShapedTextCharacters(shapedBuffer, textRun.Properties);
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
index 0ff127694b..e3bcdee014 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
@@ -162,7 +162,9 @@ namespace Avalonia.Media.TextFormatting
foreach (var textLine in TextLines)
{
- if (textLine.TextRange.End < textPosition)
+ var end = textLine.FirstTextSourceIndex + textLine.Length - 1;
+
+ if (end < textPosition)
{
currentY += textLine.Height;
@@ -193,187 +195,34 @@ namespace Avalonia.Media.TextFormatting
var result = new List(TextLines.Count);
var currentY = 0d;
- var currentPosition = 0;
- var currentRect = Rect.Empty;
foreach (var textLine in TextLines)
{
//Current line isn't covered.
- if (currentPosition + textLine.TextRange.Length <= start)
+ if (textLine.FirstTextSourceIndex + textLine.Length <= start)
{
currentY += textLine.Height;
- currentPosition += textLine.TextRange.Length;
continue;
}
- //The whole line is covered.
- if (currentPosition >= start && start + length > currentPosition + textLine.TextRange.Length)
- {
- currentRect = new Rect(textLine.Start, currentY, textLine.WidthIncludingTrailingWhitespace,
- textLine.Height);
-
- result.Add(currentRect);
-
- currentY += textLine.Height;
- currentPosition += textLine.TextRange.Length;
-
- continue;
- }
-
- var startX = textLine.Start;
-
- //A portion of the line is covered.
- for (var index = 0; index < textLine.TextRuns.Count; index++)
- {
- var currentRun = (ShapedTextCharacters)textLine.TextRuns[index];
- ShapedTextCharacters? nextRun = null;
+ var textBounds = textLine.GetTextBounds(start, length);
- if (index + 1 < textLine.TextRuns.Count)
- {
- nextRun = (ShapedTextCharacters)textLine.TextRuns[index + 1];
- }
-
- if (nextRun != null)
- {
- if (nextRun.Text.Start < currentRun.Text.Start && start + length < currentRun.Text.End)
- {
- goto skip;
- }
-
- if (currentRun.Text.Start >= start + length)
- {
- goto skip;
- }
-
- if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < start)
- {
- goto skip;
- }
-
- if (currentRun.Text.End < start)
- {
- goto skip;
- }
-
- goto noop;
-
- skip:
- {
- startX += currentRun.Size.Width;
-
- currentPosition = currentRun.Text.Start;
- }
-
- continue;
-
- noop:{ }
- }
-
- var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(
- currentRun.ShapedBuffer.IsLeftToRight ?
- new CharacterHit(start + length) :
- new CharacterHit(start));
-
- var endX = startX + endOffset;
-
- var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(
- currentRun.ShapedBuffer.IsLeftToRight ?
- new CharacterHit(start) :
- new CharacterHit(start + length));
-
- startX += startOffset;
-
- var characterHit = currentRun.GlyphRun.IsLeftToRight ?
- currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _) :
- currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
-
- currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
-
- if(nextRun != null)
- {
- if (currentRun.ShapedBuffer.IsLeftToRight == nextRun.ShapedBuffer.IsLeftToRight)
- {
- endOffset = nextRun.GlyphRun.GetDistanceFromCharacterHit(
- nextRun.ShapedBuffer.IsLeftToRight ?
- new CharacterHit(start + length) :
- new CharacterHit(start));
-
- index++;
-
- endX += endOffset;
-
- currentRun = nextRun;
-
- if (currentRun.ShapedBuffer.IsLeftToRight)
- {
- characterHit = nextRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
-
- currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
- }
- }
- }
-
- if (endX < startX)
- {
- (endX, startX) = (startX, endX);
- }
-
- var width = endX - startX;
+ foreach (var bounds in textBounds)
+ {
+ Rect? last = result.Count > 0 ? result[result.Count - 1] : null;
- if (result.Count > 0 && MathUtilities.AreClose(currentRect.Top, currentY) &&
- MathUtilities.AreClose(currentRect.Right, startX))
- {
- result[result.Count - 1] = currentRect.WithWidth(currentRect.Width + width);
- }
- else
- {
- currentRect = new Rect(startX, currentY, width, textLine.Height);
-
- result.Add(currentRect);
- }
-
- if (currentRun.ShapedBuffer.IsLeftToRight)
+ if (last.HasValue && MathUtilities.AreClose(last.Value.Right, bounds.Rectangle.Left) && MathUtilities.AreClose(last.Value.Top, currentY))
{
- if (nextRun != null)
- {
- if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= start + length)
- {
- break;
- }
-
- currentPosition = nextRun.Text.End;
- }
- else
- {
- if (currentPosition >= start + length)
- {
- break;
- }
- }
+ result[result.Count - 1] = last.Value.WithWidth(last.Value.Width + bounds.Rectangle.Width);
}
else
{
- if (currentPosition <= start)
- {
- break;
- }
- }
-
- if (!currentRun.ShapedBuffer.IsLeftToRight && currentPosition != currentRun.Text.Start)
- {
- endX += currentRun.GlyphRun.Size.Width - endOffset;
- }
-
- startX = endX;
+ result.Add(bounds.Rectangle.WithY(currentY));
+ }
}
- if (currentPosition == start || currentPosition == start + length)
- {
- break;
- }
-
- if (textLine.TextRange.Start + textLine.TextRange.Length >= start + length)
+ if(textLine.FirstTextSourceIndex + textLine.Length >= start + length)
{
break;
}
@@ -433,12 +282,13 @@ namespace Avalonia.Media.TextFormatting
{
var textLine = TextLines[index];
- if (textLine.TextRange.Start + textLine.TextRange.Length < charIndex)
+ if (textLine.FirstTextSourceIndex + textLine.Length < charIndex)
{
continue;
}
- if (charIndex >= textLine.TextRange.Start && charIndex <= textLine.TextRange.End + (trailingEdge ? 1 : 0))
+ if (charIndex >= textLine.FirstTextSourceIndex &&
+ charIndex <= textLine.FirstTextSourceIndex + textLine.Length - (trailingEdge ? 0 : 1))
{
return index;
}
@@ -451,11 +301,11 @@ namespace Avalonia.Media.TextFormatting
{
var (x, y) = point;
- var lastTrailingIndex = textLine.TextRange.Start + textLine.TextRange.Length;
+ var lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height;
- if (x >= textLine.Width && textLine.TextRange.Length > 0 && textLine.NewLineLength > 0)
+ if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
{
lastTrailingIndex -= textLine.NewLineLength;
}
@@ -465,7 +315,7 @@ namespace Avalonia.Media.TextFormatting
var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
y > Bounds.Bottom;
- if (textPosition == textLine.TextRange.Start + textLine.TextRange.Length)
+ if (textPosition == textLine.FirstTextSourceIndex + textLine.Length)
{
textPosition -= textLine.NewLineLength;
}
@@ -529,23 +379,21 @@ namespace Avalonia.Media.TextFormatting
/// Creates an empty text line.
///
/// The empty text line.
- private TextLine CreateEmptyTextLine(int startingIndex)
+ private TextLine CreateEmptyTextLine(int firstTextSourceIndex)
{
var flowDirection = _paragraphProperties.FlowDirection;
var properties = _paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface;
- var text = new ReadOnlySlice(s_empty, startingIndex, 1);
+ var text = new ReadOnlySlice(s_empty, firstTextSourceIndex, 1);
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
- var glyphInfos = new[] { new GlyphInfo(glyph, startingIndex) };
+ var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection);
- var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) };
-
- var textRange = new TextRange(startingIndex, 1);
+ var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) };
- return new TextLineImpl(textRuns, textRange, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine();
+ return new TextLineImpl(textRuns, firstTextSourceIndex, 1, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine();
}
private IReadOnlyList CreateTextLines()
@@ -576,13 +424,13 @@ namespace Avalonia.Media.TextFormatting
_paragraphProperties, previousLine?.TextLineBreak);
#if DEBUG
- if (textLine.TextRange.Length == 0)
+ if (textLine.Length == 0)
{
throw new InvalidOperationException($"{nameof(textLine)} should not be empty.");
}
#endif
- currentPosition += textLine.TextRange.Length;
+ currentPosition += textLine.Length;
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
index 74c4573630..5a14eda245 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
@@ -17,7 +17,7 @@ namespace Avalonia.Media.TextFormatting
/// Text used as collapsing symbol.
/// Length of leading prefix.
/// width in which collapsing is constrained to
- /// text run properties of ellispis symbol
+ /// text run properties of ellipsis symbol
public TextLeadingPrefixCharacterEllipsis(
ReadOnlySlice ellipsis,
int prefixLength,
@@ -35,16 +35,14 @@ namespace Avalonia.Media.TextFormatting
}
///
- public sealed override double Width { get; }
+ public override double Width { get; }
///
- public sealed override TextRun Symbol { get; }
+ public override TextRun Symbol { get; }
- public override IReadOnlyList? Collapse(TextLine textLine)
+ public override List? Collapse(TextLine textLine)
{
- var shapedTextRuns = textLine.TextRuns as List;
-
- if (shapedTextRuns is null)
+ if (textLine.TextRuns is not List textRuns || textRuns.Count == 0)
{
return null;
}
@@ -55,84 +53,103 @@ namespace Avalonia.Media.TextFormatting
if (Width < shapedSymbol.GlyphRun.Size.Width)
{
- return new List(0);
+ return new List(0);
}
// Overview of ellipsis structure
// Prefix length run | Ellipsis symbol | Post split run growing from the end |
var availableWidth = Width - shapedSymbol.Size.Width;
- while (runIndex < shapedTextRuns.Count)
+ while (runIndex < textRuns.Count)
{
- var currentRun = shapedTextRuns[runIndex];
-
- currentWidth += currentRun.Size.Width;
+ var currentRun = textRuns[runIndex];
- if (currentWidth > availableWidth)
+ switch (currentRun)
{
- currentRun.TryMeasureCharacters(availableWidth, out var measuredLength);
-
- var shapedTextCharacters = new List(shapedTextRuns.Count);
-
- if (measuredLength > 0)
+ case ShapedTextCharacters shapedRun:
{
- List? preSplitRuns = null;
- List? postSplitRuns = null;
+ currentWidth += currentRun.Size.Width;
- if (_prefixLength > 0)
+ if (currentWidth > availableWidth)
{
- var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, Math.Min(_prefixLength, measuredLength));
+ shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
- shapedTextCharacters.AddRange(splitResult.First);
+ var collapsedRuns = new List(textRuns.Count);
- TextLineImpl.SortRuns(shapedTextCharacters);
-
- preSplitRuns = splitResult.First;
- postSplitRuns = splitResult.Second;
- }
- else
- {
- postSplitRuns = shapedTextRuns;
- }
+ if (measuredLength > 0)
+ {
+ List? preSplitRuns = null;
+ List? postSplitRuns;
- shapedTextCharacters.Add(shapedSymbol);
+ if (_prefixLength > 0)
+ {
+ var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns,
+ Math.Min(_prefixLength, measuredLength));
- if (measuredLength > _prefixLength && postSplitRuns is not null)
- {
- var availableSuffixWidth = availableWidth;
+ collapsedRuns.AddRange(splitResult.First);
- if (preSplitRuns is not null)
- {
- foreach (var run in preSplitRuns)
+ preSplitRuns = splitResult.First;
+ postSplitRuns = splitResult.Second;
+ }
+ else
{
- availableSuffixWidth -= run.Size.Width;
+ postSplitRuns = textRuns;
}
- }
- for (int i = postSplitRuns.Count - 1; i >= 0; i--)
- {
- var run = postSplitRuns[i];
+ collapsedRuns.Add(shapedSymbol);
- if (run.TryMeasureCharactersBackwards(availableSuffixWidth, out int suffixCount, out double suffixWidth))
+ if (measuredLength <= _prefixLength || postSplitRuns is null)
{
- availableSuffixWidth -= suffixWidth;
+ return collapsedRuns;
+ }
+
+ var availableSuffixWidth = availableWidth;
- if (suffixCount > 0)
+ if (preSplitRuns is not null)
+ {
+ foreach (var run in preSplitRuns)
{
- var splitSuffix = run.Split(run.TextSourceLength - suffixCount);
+ availableSuffixWidth -= run.Size.Width;
+ }
+ }
+
+ for (var i = postSplitRuns.Count - 1; i >= 0; i--)
+ {
+ var run = postSplitRuns[i];
- shapedTextCharacters.Add(splitSuffix.Second!);
+ switch (run)
+ {
+ case ShapedTextCharacters endShapedRun:
+ {
+ if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
+ out var suffixCount, out var suffixWidth))
+ {
+ availableSuffixWidth -= suffixWidth;
+
+ if (suffixCount > 0)
+ {
+ var splitSuffix =
+ endShapedRun.Split(run.TextSourceLength - suffixCount);
+
+ collapsedRuns.Add(splitSuffix.Second!);
+ }
+ }
+
+ break;
+ }
}
}
}
+ else
+ {
+ collapsedRuns.Add(shapedSymbol);
+ }
+
+ return collapsedRuns;
}
- }
- else
- {
- shapedTextCharacters.Add(shapedSymbol);
- }
- return shapedTextCharacters;
+ break;
+ }
}
availableWidth -= currentRun.Size.Width;
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
index 130d0e9c39..1f69c15acc 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
@@ -16,13 +16,9 @@ namespace Avalonia.Media.TextFormatting
///
public abstract IReadOnlyList TextRuns { get; }
- ///
- /// Gets the text range that is covered by the line.
- ///
- ///
- /// The text range that is covered by the line.
- ///
- public abstract TextRange TextRange { get; }
+ public abstract int FirstTextSourceIndex { get; }
+
+ public abstract int Length { get; }
///
/// Gets the state of the line when broken by line breaking process.
@@ -189,6 +185,14 @@ namespace Avalonia.Media.TextFormatting
/// The after backspacing.
public abstract CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit);
+ ///
+ /// Get an array of bounding rectangles of a range of characters within a text line.
+ ///
+ /// index of first character of specified range
+ /// number of characters of the specified range
+ /// an array of bounding rectangles.
+ public abstract IReadOnlyList GetTextBounds(int firstTextSourceCharacterIndex, int textLength);
+
///
/// Gets the text line offset x.
///
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
index be9661c2bf..ce35e47fbd 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
@@ -5,11 +5,11 @@ namespace Avalonia.Media.TextFormatting
public class TextLineBreak
{
public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight,
- IReadOnlyList? remainingCharacters = null)
+ IReadOnlyList? remainingRuns = null)
{
TextEndOfLine = textEndOfLine;
FlowDirection = flowDirection;
- RemainingCharacters = remainingCharacters;
+ RemainingRuns = remainingRuns;
}
///
@@ -23,8 +23,8 @@ namespace Avalonia.Media.TextFormatting
public FlowDirection FlowDirection { get; }
///
- /// Get the remaining shaped characters that were split up by the during the formatting process.
+ /// Get the remaining runs that were split up by the during the formatting process.
///
- public IReadOnlyList? RemainingCharacters { get; }
+ public IReadOnlyList? RemainingRuns { get; }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
index 49bee6e776..30e3728d1f 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
@@ -1,32 +1,27 @@
using System;
using System.Collections.Generic;
-using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal class TextLineImpl : TextLine
{
- private static readonly Comparer s_compareStart = Comparer.Default;
-
- private static readonly Comparison s_compareLogicalOrder =
- (a, b) => s_compareStart.Compare(a.Text.Start, b.Text.Start);
-
- private readonly List _textRuns;
+ private readonly List _textRuns;
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
private TextLineMetrics _textLineMetrics;
private readonly FlowDirection _flowDirection;
- public TextLineImpl(List textRuns, TextRange textRange, double paragraphWidth,
- TextParagraphProperties paragraphProperties, FlowDirection flowDirection = FlowDirection.LeftToRight,
+ public TextLineImpl(List textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
+ TextParagraphProperties paragraphProperties, FlowDirection flowDirection = FlowDirection.LeftToRight,
TextLineBreak? lineBreak = null, bool hasCollapsed = false)
{
- TextRange = textRange;
+ FirstTextSourceIndex = firstTextSourceIndex;
+ Length = length;
TextLineBreak = lineBreak;
HasCollapsed = hasCollapsed;
- _textRuns = textRuns;
+ _textRuns = textRuns;
_paragraphWidth = paragraphWidth;
_paragraphProperties = paragraphProperties;
@@ -37,7 +32,10 @@ namespace Avalonia.Media.TextFormatting
public override IReadOnlyList TextRuns => _textRuns;
///
- public override TextRange TextRange { get; }
+ public override int FirstTextSourceIndex { get; }
+
+ ///
+ public override int Length { get; }
///
public override TextLineBreak? TextLineBreak { get; }
@@ -88,7 +86,7 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in _textRuns)
{
- var offsetY = Baseline - textRun.GlyphRun.BaselineOrigin.Y;
+ var offsetY = GetBaselineOffset(this, textRun);
textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
@@ -96,6 +94,30 @@ namespace Avalonia.Media.TextFormatting
}
}
+ private static double GetBaselineOffset(TextLine textLine, DrawableTextRun textRun)
+ {
+ var baseline = textRun.Baseline;
+ var baselineAlignment = textRun.Properties?.BaselineAlignment;
+
+ switch (baselineAlignment)
+ {
+ case BaselineAlignment.Top:
+ return 0;
+ case BaselineAlignment.Center:
+ return textLine.Height / 2 - textRun.Size.Height / 2;
+ case BaselineAlignment.Bottom:
+ return textLine.Height - textRun.Size.Height;
+ case BaselineAlignment.Baseline:
+ case BaselineAlignment.TextTop:
+ case BaselineAlignment.TextBottom:
+ case BaselineAlignment.Subscript:
+ case BaselineAlignment.Superscript:
+ return textLine.Baseline - baseline;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(baselineAlignment), baselineAlignment, null);
+ }
+ }
+
///
public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList)
{
@@ -108,47 +130,82 @@ namespace Avalonia.Media.TextFormatting
var collapsedRuns = collapsingProperties.Collapse(this);
- if (collapsedRuns is List shapedRuns)
+ if (collapsedRuns is null)
{
- var collapsedLine = new TextLineImpl(shapedRuns, TextRange, _paragraphWidth, _paragraphProperties, _flowDirection, TextLineBreak, true);
+ return this;
+ }
- if (shapedRuns.Count > 0)
- {
- collapsedLine.FinalizeLine();
- }
+ var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties,
+ _flowDirection, TextLineBreak, true);
- return collapsedLine;
+ if (collapsedRuns.Count > 0)
+ {
+ collapsedLine.FinalizeLine();
}
- return this;
+ return collapsedLine;
+
}
///
public override CharacterHit GetCharacterHitFromDistance(double distance)
{
+ if (_textRuns.Count == 0)
+ {
+ return new CharacterHit();
+ }
+
distance -= Start;
-
+
if (distance <= 0)
{
// hit happens before the line, return the first position
var firstRun = _textRuns[0];
- return firstRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+ if (firstRun is ShapedTextCharacters shapedTextCharacters)
+ {
+ return shapedTextCharacters.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+ }
+
+ return _flowDirection == FlowDirection.LeftToRight ?
+ new CharacterHit(FirstTextSourceIndex) :
+ new CharacterHit(FirstTextSourceIndex + Length);
}
// process hit that happens within the line
var characterHit = new CharacterHit();
+ var currentPosition = FirstTextSourceIndex;
- foreach (var run in _textRuns)
+ foreach (var currentRun in _textRuns)
{
- characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+ switch (currentRun)
+ {
+ case ShapedTextCharacters shapedRun:
+ {
+ characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+ break;
+ }
+ default:
+ {
+ if (distance < currentRun.Size.Width / 2)
+ {
+ characterHit = new CharacterHit(currentPosition);
+ }
+ else
+ {
+ characterHit = new CharacterHit(currentPosition, currentRun.TextSourceLength);
+ }
+ break;
+ }
+ }
- if (distance <= run.Size.Width)
+ if (distance <= currentRun.Size.Width)
{
break;
}
- distance -= run.Size.Width;
+ distance -= currentRun.Size.Width;
+ currentPosition += currentRun.TextSourceLength;
}
return characterHit;
@@ -158,91 +215,119 @@ namespace Avalonia.Media.TextFormatting
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0);
-
var currentDistance = Start;
+ var currentPosition = FirstTextSourceIndex;
GlyphRun? lastRun = null;
for (var index = 0; index < _textRuns.Count; index++)
{
var textRun = _textRuns[index];
- var currentRun = textRun.GlyphRun;
- if (lastRun != null)
+ switch (textRun)
{
- if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight &&
- currentRun.Characters.Start == characterHit.FirstCharacterIndex &&
- characterHit.TrailingLength == 0)
- {
- return currentDistance;
- }
- }
-
- //Look for a hit in within the current run
- if (characterIndex >= textRun.Text.Start && characterIndex <= textRun.Text.End)
- {
- var distance = currentRun.GetDistanceFromCharacterHit(characterHit);
+ case ShapedTextCharacters shapedTextCharacters:
+ {
+ var currentRun = shapedTextCharacters.GlyphRun;
- return currentDistance + distance;
- }
+ if (lastRun != null)
+ {
+ if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight &&
+ currentRun.Characters.Start == characterHit.FirstCharacterIndex &&
+ characterHit.TrailingLength == 0)
+ {
+ return currentDistance;
+ }
+ }
- //Look at the left and right edge of the current run
- if (currentRun.IsLeftToRight)
- {
- if (lastRun == null || lastRun.IsLeftToRight)
- {
- if (characterIndex <= textRun.Text.Start)
- {
- return currentDistance;
- }
- }
- else
- {
- if (characterIndex == textRun.Text.Start)
- {
- return currentDistance;
- }
- }
+ //Look for a hit in within the current run
+ if (characterIndex >= textRun.Text.Start && characterIndex <= textRun.Text.Start + textRun.Text.Length)
+ {
+ var distance = currentRun.GetDistanceFromCharacterHit(characterHit);
- if (characterIndex == textRun.Text.Start + textRun.Text.Length && characterHit.TrailingLength > 0)
- {
- return currentDistance + currentRun.Size.Width;
- }
- }
- else
- {
- if (characterIndex == textRun.Text.Start)
- {
- return currentDistance + currentRun.Size.Width;
- }
+ return currentDistance + distance;
+ }
- var nextRun = index + 1 < _textRuns.Count ? _textRuns[index + 1] : null;
+ //Look at the left and right edge of the current run
+ if (currentRun.IsLeftToRight)
+ {
+ if (lastRun == null || lastRun.IsLeftToRight)
+ {
+ if (characterIndex <= textRun.Text.Start)
+ {
+ return currentDistance;
+ }
+ }
+ else
+ {
+ if (characterIndex == textRun.Text.Start)
+ {
+ return currentDistance;
+ }
+ }
+
+ if (characterIndex == textRun.Text.Start + textRun.Text.Length &&
+ characterHit.TrailingLength > 0)
+ {
+ return currentDistance + currentRun.Size.Width;
+ }
+ }
+ else
+ {
+ if (characterIndex == textRun.Text.Start)
+ {
+ return currentDistance + currentRun.Size.Width;
+ }
+
+ var nextRun = index + 1 < _textRuns.Count ?
+ _textRuns[index + 1] as ShapedTextCharacters :
+ null;
+
+ if (nextRun != null)
+ {
+ if (characterHit.FirstCharacterIndex == textRun.Text.End &&
+ nextRun.ShapedBuffer.IsLeftToRight)
+ {
+ return currentDistance;
+ }
+
+ if (characterIndex > textRun.Text.End && nextRun.Text.End < textRun.Text.End)
+ {
+ return currentDistance;
+ }
+ }
+ else
+ {
+ if (characterIndex > textRun.Text.End)
+ {
+ return currentDistance;
+ }
+ }
+ }
- if (nextRun != null)
- {
- if (characterHit.FirstCharacterIndex == textRun.Text.End && nextRun.ShapedBuffer.IsLeftToRight)
- {
- return currentDistance;
- }
+ lastRun = currentRun;
- if (characterIndex > textRun.Text.End && nextRun.Text.End < textRun.Text.End)
- {
- return currentDistance;
+ break;
}
- }
- else
- {
- if (characterIndex > textRun.Text.End)
+ default:
{
- return currentDistance;
+ if (characterIndex == currentPosition)
+ {
+ return currentDistance;
+ }
+
+ if (characterIndex == currentPosition + textRun.TextSourceLength)
+ {
+ return currentDistance + textRun.Size.Width;
+ }
+
+ break;
}
- }
}
//No hit hit found so we add the full width
- currentDistance += currentRun.Size.Width;
-
- lastRun = currentRun;
+ currentDistance += textRun.Size.Width;
+ currentPosition += textRun.TextSourceLength;
}
return currentDistance;
@@ -251,19 +336,38 @@ namespace Avalonia.Media.TextFormatting
///
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{
+ if (_textRuns.Count == 0)
+ {
+ return new CharacterHit();
+ }
+
if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit))
{
return nextCharacterHit;
}
+ var lastTextPosition = FirstTextSourceIndex + Length;
+
// Can't move, we're after the last character
- var runIndex = GetRunIndexAtCharacterIndex(TextRange.End, LogicalDirection.Forward);
+ var runIndex = GetRunIndexAtCharacterIndex(lastTextPosition, LogicalDirection.Forward, out var currentPosition);
- var textRun = _textRuns[runIndex];
+ var currentRun = _textRuns[runIndex];
- characterHit = textRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
+ switch (currentRun)
+ {
+ case ShapedTextCharacters shapedRun:
+ {
+ characterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
+ break;
+ }
+ default:
+ {
+ characterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
+ break;
+ }
+ }
- return characterHit;
+ return characterHit;
}
///
@@ -274,9 +378,9 @@ namespace Avalonia.Media.TextFormatting
return previousCharacterHit;
}
- if (characterHit.FirstCharacterIndex <= TextRange.Start)
+ if (characterHit.FirstCharacterIndex <= FirstTextSourceIndex)
{
- characterHit = new CharacterHit(TextRange.Start);
+ characterHit = new CharacterHit(FirstTextSourceIndex);
}
return characterHit; // Can't move, we're before the first character
@@ -289,9 +393,199 @@ namespace Avalonia.Media.TextFormatting
return GetPreviousCaretCharacterHit(characterHit);
}
- public static void SortRuns(List textRuns)
+ public override IReadOnlyList GetTextBounds(int firstTextSourceCharacterIndex, int textLength)
{
- textRuns.Sort(s_compareLogicalOrder);
+ if (firstTextSourceCharacterIndex + textLength <= FirstTextSourceIndex)
+ {
+ return Array.Empty();
+ }
+
+ var result = new List(TextRuns.Count);
+ var lastDirection = _flowDirection;
+ var currentDirection = lastDirection;
+ var currentPosition = 0;
+ var currentRect = Rect.Empty;
+ var startX = Start;
+
+ //A portion of the line is covered.
+ for (var index = 0; index < TextRuns.Count; index++)
+ {
+ var currentRun = TextRuns[index] as DrawableTextRun;
+
+ if (currentRun is null)
+ {
+ continue;
+ }
+
+ TextRun? nextRun = null;
+
+ if (index + 1 < TextRuns.Count)
+ {
+ nextRun = TextRuns[index + 1];
+ }
+
+ if (nextRun != null)
+ {
+ if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
+ {
+ goto skip;
+ }
+
+ if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
+ {
+ goto skip;
+ }
+
+ if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
+ {
+ goto skip;
+ }
+
+ if (currentRun.Text.End < firstTextSourceCharacterIndex)
+ {
+ goto skip;
+ }
+
+ goto noop;
+
+ skip:
+ {
+ startX += currentRun.Size.Width;
+ }
+
+ continue;
+
+ noop:
+ {
+ }
+ }
+
+
+ var endX = startX;
+ var endOffset = 0d;
+
+ switch (currentRun)
+ {
+ case ShapedTextCharacters shapedRun:
+ {
+ endOffset = shapedRun.GlyphRun.GetDistanceFromCharacterHit(
+ shapedRun.ShapedBuffer.IsLeftToRight ?
+ new CharacterHit(firstTextSourceCharacterIndex + textLength) :
+ new CharacterHit(firstTextSourceCharacterIndex));
+
+ endX += endOffset;
+
+ var startOffset = shapedRun.GlyphRun.GetDistanceFromCharacterHit(
+ shapedRun.ShapedBuffer.IsLeftToRight ?
+ new CharacterHit(firstTextSourceCharacterIndex) :
+ new CharacterHit(firstTextSourceCharacterIndex + textLength));
+
+ startX += startOffset;
+
+ var characterHit = shapedRun.GlyphRun.IsLeftToRight ?
+ shapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _) :
+ shapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+
+ currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
+ currentDirection = shapedRun.ShapedBuffer.IsLeftToRight ?
+ FlowDirection.LeftToRight :
+ FlowDirection.RightToLeft;
+
+ if (nextRun is ShapedTextCharacters nextShaped)
+ {
+ if (shapedRun.ShapedBuffer.IsLeftToRight == nextShaped.ShapedBuffer.IsLeftToRight)
+ {
+ endOffset = nextShaped.GlyphRun.GetDistanceFromCharacterHit(
+ nextShaped.ShapedBuffer.IsLeftToRight ?
+ new CharacterHit(firstTextSourceCharacterIndex + textLength) :
+ new CharacterHit(firstTextSourceCharacterIndex));
+
+ index++;
+
+ endX += endOffset;
+
+ currentRun = nextShaped;
+
+ if (nextShaped.ShapedBuffer.IsLeftToRight)
+ {
+ characterHit = nextShaped.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+
+ currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+ }
+ }
+ }
+
+ break;
+ }
+ default:
+ {
+ if (firstTextSourceCharacterIndex + textLength >= currentRun.Text.Start + currentRun.Text.Length)
+ {
+ endX += currentRun.Size.Width;
+ }
+
+ break;
+ }
+ }
+
+ if (endX < startX)
+ {
+ (endX, startX) = (startX, endX);
+ }
+
+ var width = endX - startX;
+
+ if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
+ {
+ var textBounds = new TextBounds(currentRect.WithWidth(currentRect.Width + width), currentDirection);
+
+ result[result.Count - 1] = textBounds;
+ }
+ else
+ {
+ currentRect = new Rect(startX, 0, width, Height);
+
+ result.Add(new TextBounds(currentRect, currentDirection));
+ }
+
+ if (currentDirection == FlowDirection.LeftToRight)
+ {
+ if (nextRun != null)
+ {
+ if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
+ {
+ break;
+ }
+
+ currentPosition = nextRun.Text.End;
+ }
+ else
+ {
+ if (currentPosition >= firstTextSourceCharacterIndex + textLength)
+ {
+ break;
+ }
+ }
+ }
+ else
+ {
+ if (currentPosition <= firstTextSourceCharacterIndex)
+ {
+ break;
+ }
+
+ if (currentPosition != currentRun.Text.Start)
+ {
+ endX += currentRun.Size.Width - endOffset;
+ }
+ }
+
+ lastDirection = currentDirection;
+ startX = endX;
+ }
+
+ return result;
}
public TextLineImpl FinalizeLine()
@@ -303,18 +597,37 @@ namespace Avalonia.Media.TextFormatting
return this;
}
+ private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection)
+ {
+ if (run is ShapedTextCharacters shapedTextCharacters)
+ {
+ return shapedTextCharacters.BidiLevel;
+ }
+
+ var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1;
+
+ return (sbyte)defaultLevel;
+ }
+
private void BidiReorder()
{
+ if (_textRuns.Count == 0)
+ {
+ return;
+ }
+
// Build up the collection of ordered runs.
var run = _textRuns[0];
- OrderedBidiRun orderedRun = new(run);
+
+ OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _flowDirection));
+
var current = orderedRun;
for (var i = 1; i < _textRuns.Count; i++)
{
run = _textRuns[i];
- current.Next = new OrderedBidiRun(run);
+ current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _flowDirection));
current = current.Next;
}
@@ -331,7 +644,9 @@ namespace Avalonia.Media.TextFormatting
for (var i = 0; i < _textRuns.Count; i++)
{
- var level = _textRuns[i].BidiLevel;
+ var currentRun = _textRuns[i];
+
+ var level = GetRunBidiLevel(currentRun, _flowDirection);
if (level > max)
{
@@ -366,9 +681,9 @@ namespace Avalonia.Media.TextFormatting
{
if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
{
- if (!current.Run.IsReversed)
+ if (current.Run is ShapedTextCharacters { IsReversed: false } shapedTextCharacters)
{
- current.Run.Reverse();
+ shapedTextCharacters.Reverse();
}
}
@@ -464,53 +779,73 @@ namespace Avalonia.Media.TextFormatting
nextCharacterHit = characterHit;
var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+ var lastCodepointIndex = FirstTextSourceIndex + Length;
- if (codepointIndex >= TextRange.End)
+ if (codepointIndex >= lastCodepointIndex)
{
return false; // Cannot go forward anymore
}
- if (codepointIndex < TextRange.Start)
+ if (codepointIndex < FirstTextSourceIndex)
{
- codepointIndex = TextRange.Start;
+ codepointIndex = FirstTextSourceIndex;
}
- var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward);
+ var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition);
while (runIndex < _textRuns.Count)
{
- var run = _textRuns[runIndex];
+ var currentRun = _textRuns[runIndex];
+
+ switch (currentRun)
+ {
+ case ShapedTextCharacters shapedRun:
+ {
+ var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
- var foundCharacterHit =
- run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength,
- out _);
+ var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == FirstTextSourceIndex + Length;
- var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
- TextRange.Start + TextRange.Length;
+ if (isAtEnd && !shapedRun.GlyphRun.IsLeftToRight)
+ {
+ nextCharacterHit = foundCharacterHit;
- if (isAtEnd && !run.GlyphRun.IsLeftToRight)
- {
- nextCharacterHit = foundCharacterHit;
+ return true;
+ }
- return true;
- }
+ var characterIndex = codepointIndex - shapedRun.Text.Start;
- var characterIndex = codepointIndex - run.Text.Start;
+ if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
+ {
+ foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
+ }
- if (characterIndex < 0 && run.ShapedBuffer.IsLeftToRight)
- {
- foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
- }
+ nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
+ foundCharacterHit :
+ new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
+
+ if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex)
+ {
+ return true;
+ }
- nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
- foundCharacterHit :
- new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
+ break;
+ }
+ default:
+ {
+ var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
- if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex)
- {
- return true;
+ if (textPosition == currentPosition)
+ {
+ nextCharacterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
+
+ return true;
+ }
+
+ break;
+ }
}
+ currentPosition += currentRun.TextSourceLength;
runIndex++;
}
@@ -527,45 +862,67 @@ namespace Avalonia.Media.TextFormatting
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
- if (characterIndex == TextRange.Start)
+ if (characterIndex == FirstTextSourceIndex)
{
- previousCharacterHit = new CharacterHit(TextRange.Start);
+ previousCharacterHit = new CharacterHit(FirstTextSourceIndex);
return true;
}
previousCharacterHit = characterHit;
- if (characterIndex < TextRange.Start)
+ if (characterIndex < FirstTextSourceIndex)
{
return false; // Cannot go backward anymore.
}
- var runIndex = GetRunIndexAtCharacterIndex(characterIndex, LogicalDirection.Backward);
+ var runIndex = GetRunIndexAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
while (runIndex >= 0)
{
- var run = _textRuns[runIndex];
-
- var foundCharacterHit =
- run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
+ var currentRun = _textRuns[runIndex];
- if (foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength < characterIndex)
+ switch (currentRun)
{
- previousCharacterHit = foundCharacterHit;
-
- return true;
- }
-
- previousCharacterHit = characterHit.TrailingLength != 0 ?
- foundCharacterHit :
- new CharacterHit(foundCharacterHit.FirstCharacterIndex);
+ case ShapedTextCharacters shapedRun:
+ {
+ var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
- if (previousCharacterHit != characterHit)
- {
- return true;
+ if (foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength < characterIndex)
+ {
+ previousCharacterHit = foundCharacterHit;
+
+ return true;
+ }
+
+ var previousPosition = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength;
+
+ if (foundCharacterHit.TrailingLength > 0 && previousPosition == characterIndex)
+ {
+ previousCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
+ }
+
+ if (previousCharacterHit != characterHit)
+ {
+ return true;
+ }
+
+ break;
+ }
+ default:
+ {
+ if (characterIndex == currentPosition + currentRun.TextSourceLength)
+ {
+ previousCharacterHit = new CharacterHit(currentPosition);
+
+ return true;
+ }
+
+ break;
+ }
}
+ currentPosition -= currentRun.TextSourceLength;
runIndex--;
}
@@ -577,59 +934,83 @@ namespace Avalonia.Media.TextFormatting
///
/// The codepoint index.
/// The logical direction.
+ /// The text position of the found run index.
/// The text run index.
- private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction)
+ private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition)
{
var runIndex = 0;
- ShapedTextCharacters? previousRun = null;
+ textPosition = FirstTextSourceIndex;
+ DrawableTextRun? previousRun = null;
while (runIndex < _textRuns.Count)
{
var currentRun = _textRuns[runIndex];
- if (previousRun != null && !previousRun.ShapedBuffer.IsLeftToRight)
+ switch (currentRun)
{
- if (currentRun.ShapedBuffer.IsLeftToRight)
- {
- if (currentRun.Text.Start >= codepointIndex)
+ case ShapedTextCharacters shapedRun:
{
- return --runIndex;
+ if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
+ {
+ if (shapedRun.ShapedBuffer.IsLeftToRight)
+ {
+ if (currentRun.Text.Start >= codepointIndex)
+ {
+ return --runIndex;
+ }
+ }
+ else
+ {
+ if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length)
+ {
+ return --runIndex;
+ }
+ }
+ }
+
+ if (direction == LogicalDirection.Forward)
+ {
+ if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End)
+ {
+ return runIndex;
+ }
+ }
+ else
+ {
+ if (codepointIndex > currentRun.Text.Start &&
+ codepointIndex <= currentRun.Text.Start + currentRun.Text.Length)
+ {
+ return runIndex;
+ }
+ }
+
+ if (runIndex + 1 >= _textRuns.Count)
+ {
+ return runIndex;
+ }
+
+ break;
}
- }
- else
- {
- if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length)
+
+ default:
{
- return --runIndex;
- }
- }
- }
+ if (codepointIndex == textPosition)
+ {
+ return runIndex;
+ }
- if (direction == LogicalDirection.Forward)
- {
- if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End)
- {
- return runIndex;
- }
- }
- else
- {
- if (codepointIndex > currentRun.Text.Start &&
- codepointIndex <= currentRun.Text.Start + currentRun.Text.Length)
- {
- return runIndex;
- }
- }
+ if (runIndex + 1 >= _textRuns.Count)
+ {
+ return runIndex;
+ }
- if (runIndex + 1 < _textRuns.Count)
- {
- runIndex++;
- previousRun = currentRun;
- }
- else
- {
- break;
+ break;
+ }
}
+
+ runIndex++;
+ previousRun = currentRun;
+ textPosition += currentRun.TextSourceLength;
}
return runIndex;
@@ -637,6 +1018,8 @@ namespace Avalonia.Media.TextFormatting
private TextLineMetrics CreateLineMetrics()
{
+ var start = 0d;
+ var height = 0d;
var width = 0d;
var widthIncludingWhitespace = 0d;
var trailingWhitespaceLength = 0;
@@ -646,78 +1029,150 @@ namespace Avalonia.Media.TextFormatting
var lineGap = 0d;
var fontRenderingEmSize = 0d;
- for (var index = 0; index < _textRuns.Count; index++)
+ var lineHeight = _paragraphProperties.LineHeight;
+
+ if (_textRuns.Count == 0)
{
- var textRun = _textRuns[index];
+ var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface;
+ fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
+ var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight;
+ ascent = glyphTypeface.Ascent * scale;
+ height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
+ descent - ascent + lineGap :
+ lineHeight;
- var fontMetrics =
- new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
+ return new TextLineMetrics(false, height, 0, start, -ascent, 0, 0, 0);
+ }
- if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
+ for (var index = 0; index < _textRuns.Count; index++)
+ {
+ switch (_textRuns[index])
{
- fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;
-
- if (ascent > fontMetrics.Ascent)
- {
- ascent = fontMetrics.Ascent;
- }
-
- if (descent < fontMetrics.Descent)
- {
- descent = fontMetrics.Descent;
- }
+ case ShapedTextCharacters textRun:
+ {
+ var fontMetrics =
+ new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
- if (lineGap < fontMetrics.LineGap)
- {
- lineGap = fontMetrics.LineGap;
- }
- }
+ if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
+ {
+ fontRenderingEmSize = 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 (descent - ascent + lineGap > height)
+ {
+ height = descent - ascent + lineGap;
+ }
+ }
- switch (_paragraphProperties.FlowDirection)
- {
- case FlowDirection.LeftToRight:
- {
- if (index == _textRuns.Count - 1)
+ switch (_paragraphProperties.FlowDirection)
{
- width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
- trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
- newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
+ case FlowDirection.LeftToRight:
+ {
+ if (index == _textRuns.Count - 1)
+ {
+ width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
+ trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
+ newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
+ }
+
+ break;
+ }
+
+ case FlowDirection.RightToLeft:
+ {
+ if (index == _textRuns.Count - 1)
+ {
+ var firstRun = _textRuns[0];
+
+ if (firstRun is ShapedTextCharacters shapedTextCharacters)
+ {
+ var offset = shapedTextCharacters.GlyphRun.Metrics.WidthIncludingTrailingWhitespace -
+ shapedTextCharacters.GlyphRun.Metrics.Width;
+
+ width = widthIncludingWhitespace +
+ textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - offset;
+
+ trailingWhitespaceLength = shapedTextCharacters.GlyphRun.Metrics.TrailingWhitespaceLength;
+ newLineLength = shapedTextCharacters.GlyphRun.Metrics.NewlineLength;
+ }
+ }
+
+ break;
+ }
}
+ widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
+
break;
}
- case FlowDirection.RightToLeft:
+ case { } drawableTextRun:
{
- if (index == _textRuns.Count - 1)
- {
- var firstRun = _textRuns[0];
+ widthIncludingWhitespace += drawableTextRun.Size.Width;
- var offset = firstRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace -
- firstRun.GlyphRun.Metrics.Width;
+ switch (_paragraphProperties.FlowDirection)
+ {
+ case FlowDirection.LeftToRight:
+ {
+ if (index == _textRuns.Count - 1)
+ {
+ width = widthIncludingWhitespace;
+ trailingWhitespaceLength = 0;
+ newLineLength = 0;
+ }
+
+ break;
+ }
+
+ case FlowDirection.RightToLeft:
+ {
+ if (index == _textRuns.Count - 1)
+ {
+ width = widthIncludingWhitespace;
+ trailingWhitespaceLength = 0;
+ newLineLength = 0;
+ }
+
+ break;
+ }
+ }
- width = widthIncludingWhitespace +
- textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - offset;
+ if (drawableTextRun.Size.Height > height)
+ {
+ height = drawableTextRun.Size.Height;
+ }
- trailingWhitespaceLength = firstRun.GlyphRun.Metrics.TrailingWhitespaceLength;
- newLineLength = firstRun.GlyphRun.Metrics.NewlineLength;
+ if (ascent > -drawableTextRun.Baseline)
+ {
+ ascent = -drawableTextRun.Baseline;
}
break;
}
}
-
- widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
}
- var start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
+ start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
_paragraphProperties.TextAlignment, _paragraphProperties.FlowDirection);
- var lineHeight = _paragraphProperties.LineHeight;
-
- var height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
- descent - ascent + lineGap :
- lineHeight;
+ if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
+ {
+ height = lineHeight;
+ }
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
-ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
@@ -725,15 +1180,17 @@ namespace Avalonia.Media.TextFormatting
private sealed class OrderedBidiRun
{
- public OrderedBidiRun(ShapedTextCharacters run) => Run = run;
+ public OrderedBidiRun(DrawableTextRun run, sbyte level)
+ {
+ Run = run;
+ Level = level;
+ }
- public sbyte Level => Run.BidiLevel;
+ public sbyte Level { get; }
- public ShapedTextCharacters Run { get; }
+ public DrawableTextRun Run { get; }
public OrderedBidiRun? Next { get; set; }
-
- public void Reverse() => Run.ShapedBuffer.GlyphInfos.Span.Reverse();
}
private sealed class BidiRange
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
index b799567a60..82a0ba14d8 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
@@ -63,5 +63,13 @@
{
get { return 0; }
}
+
+ ///
+ /// Default Incremental Tab
+ ///
+ public virtual double DefaultIncrementalTab
+ {
+ get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; }
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
index 99fcbd805f..86b701cb4b 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
@@ -45,7 +45,7 @@ namespace Avalonia.Media.TextFormatting
///
/// Run vertical box alignment
///
- public abstract BaselineAlignment BaselineAlignment { get; }
+ public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline;
public bool Equals(TextRunProperties? other)
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
index c982a435c3..615b1553b6 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
@@ -45,10 +45,9 @@ namespace Avalonia.Media.TextFormatting
}
///
- public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
- CultureInfo? culture, sbyte bidiLevel)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options)
{
- return _platformImpl.ShapeText(text, typeface, fontRenderingEmSize, culture, bidiLevel);
+ return _platformImpl.ShapeText(text, options);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextShaperOptions.cs
new file mode 100644
index 0000000000..a7fe92dc9a
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextShaperOptions.cs
@@ -0,0 +1,49 @@
+using System.Globalization;
+
+namespace Avalonia.Media.TextFormatting
+{
+ ///
+ /// Options to customize text shaping.
+ ///
+ public readonly struct TextShaperOptions
+ {
+ public TextShaperOptions(
+ GlyphTypeface typeface,
+ double fontRenderingEmSize = 12,
+ sbyte bidiLevel = 0,
+ CultureInfo? culture = null,
+ double incrementalTabWidth = 0)
+ {
+ Typeface = typeface;
+ FontRenderingEmSize = fontRenderingEmSize;
+ BidLevel = bidiLevel;
+ Culture = culture;
+ IncrementalTabWidth = incrementalTabWidth;
+ }
+
+ ///
+ /// Get the typeface.
+ ///
+ public GlyphTypeface Typeface { get; }
+ ///
+ /// Get the font rendering em size.
+ ///
+ public double FontRenderingEmSize { get; }
+
+ ///
+ /// Get the bidi level of the text.
+ ///
+ public sbyte BidLevel { get; }
+
+ ///
+ /// Get the culture.
+ ///
+ public CultureInfo? Culture { get; }
+
+ ///
+ /// Get the incremental tab width.
+ ///
+ public double IncrementalTabWidth { get; }
+
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
index 83acaa021e..670d94e928 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
@@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
///
/// Text used as collapsing symbol.
/// Width in which collapsing is constrained to.
- /// Text run properties of ellispis symbol.
+ /// Text run properties of ellipsis symbol.
public TextTrailingCharacterEllipsis(ReadOnlySlice ellipsis, double width, TextRunProperties textRunProperties)
{
Width = width;
@@ -22,12 +22,12 @@ namespace Avalonia.Media.TextFormatting
}
///
- public sealed override double Width { get; }
+ public override double Width { get; }
///
- public sealed override TextRun Symbol { get; }
+ public override TextRun Symbol { get; }
- public override IReadOnlyList? Collapse(TextLine textLine)
+ public override List? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, false);
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
index ff2e4cf325..dbffbdf060 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
@@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
///
/// Text used as collapsing symbol.
/// width in which collapsing is constrained to.
- /// text run properties of ellispis symbol.
+ /// text run properties of ellipsis symbol.
public TextTrailingWordEllipsis(
ReadOnlySlice ellipsis,
double width,
@@ -26,12 +26,12 @@ namespace Avalonia.Media.TextFormatting
}
///
- public sealed override double Width { get; }
+ public override double Width { get; }
///
- public sealed override TextRun Symbol { get; }
+ public override TextRun Symbol { get; }
- public override IReadOnlyList? Collapse(TextLine textLine)
+ public override List? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, true);
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
index 404956d1e1..d18a4b2a87 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
@@ -302,7 +302,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
///
/// The data to be evaluated
/// The resolved embedding level
- public sbyte ResolveEmbeddingLevel(ReadOnlySlice data)
+ public sbyte ResolveEmbeddingLevel(ArraySlice data)
{
// P2
for (var i = 0; i < data.Length; ++i)
diff --git a/src/Avalonia.Visuals/Platform/IGeometryContext.cs b/src/Avalonia.Visuals/Platform/IGeometryContext.cs
index 3f7273b53b..87db9f1dd4 100644
--- a/src/Avalonia.Visuals/Platform/IGeometryContext.cs
+++ b/src/Avalonia.Visuals/Platform/IGeometryContext.cs
@@ -13,7 +13,7 @@ namespace Avalonia.Platform
///
/// The destination point.
/// The radii of an oval whose perimeter is used to draw the angle.
- /// The rotation angle of the oval that specifies the curve.
+ /// The rotation angle (in radians) of the oval that specifies the curve.
/// true to draw the arc greater than 180 degrees; otherwise, false.
///
/// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
@@ -60,4 +60,4 @@ namespace Avalonia.Platform
/// The fill rule.
void SetFillRule(FillRule fillRule);
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
index e4a280938d..444cc2eb8c 100644
--- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
@@ -168,6 +168,13 @@ namespace Avalonia.Platform
///
IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun);
+ ///
+ /// Gets a value indicating whether the platform directly supports rectangles with rounded corners.
+ ///
+ ///
+ /// Some platform renderers can't directly handle rounded corners on rectangles.
+ /// In this case, code that requires rounded corners must generate and retain a geometry instead.
+ ///
bool SupportsIndividualRoundRects { get; }
///
diff --git a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
index aced05c9d8..11be9e3f09 100644
--- a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
+++ b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
@@ -1,6 +1,4 @@
-using System.Globalization;
-using Avalonia.Media;
-using Avalonia.Media.TextFormatting;
+using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Platform
@@ -14,11 +12,8 @@ namespace Avalonia.Platform
/// Shapes the specified region within the text and returns a shaped buffer.
///
/// The text.
- /// The typeface.
- /// The font rendering em size.
- /// The culture.
- /// The bidi level.
+ /// Text shaper options to customize the shaping process.
/// A shaped glyph run.
- ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize, CultureInfo? culture, sbyte bidiLevel);
- }
+ ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options);
+ }
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs
index 504256b932..c1fc6a81f6 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs
@@ -59,6 +59,7 @@ namespace Avalonia.Rendering.SceneGraph
public override void Render(IDrawingContextImpl context)
{
+ context.Transform = Transform;
context.DrawEllipse(Brush, Pen, Rect);
}
diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs
index fcdc10e999..1e70608168 100644
--- a/src/Avalonia.X11/Glx/GlxDisplay.cs
+++ b/src/Avalonia.X11/Glx/GlxDisplay.cs
@@ -9,7 +9,7 @@ namespace Avalonia.X11.Glx
unsafe class GlxDisplay
{
private readonly X11Info _x11;
- private readonly List _probeProfiles;
+ private readonly GlVersion[] _probeProfiles;
private readonly IntPtr _fbconfig;
private readonly XVisualInfo* _visual;
private string[] _displayExtensions;
@@ -21,7 +21,7 @@ namespace Avalonia.X11.Glx
public GlxDisplay(X11Info x11, IList probeProfiles)
{
_x11 = x11;
- _probeProfiles = probeProfiles.ToList();
+ _probeProfiles = probeProfiles.ToArray();
_displayExtensions = Glx.GetExtensions(_x11.Display);
var baseAttribs = new[]
@@ -76,10 +76,10 @@ namespace Avalonia.X11.Glx
if (Glx.GetFBConfigAttrib(_x11.Display, _fbconfig, GLX_STENCIL_SIZE, out var stencil) == 0)
stencilSize = stencil;
- var pbuffers = Enumerable.Range(0, 2).Select(_ => Glx.CreatePbuffer(_x11.Display, _fbconfig, new[]
- {
- GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0
- })).ToList();
+ var attributes = new[] { GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 };
+
+ Glx.CreatePbuffer(_x11.Display, _fbconfig, attributes);
+ Glx.CreatePbuffer(_x11.Display, _fbconfig, attributes);
XLib.XFlush(_x11.Display);
@@ -104,7 +104,6 @@ namespace Avalonia.X11.Glx
$"Renderer '{glInterface.Renderer}' is blacklisted by '{item}'");
}
}
-
}
IntPtr CreatePBuffer()
diff --git a/src/Avalonia.X11/X11Globals.cs b/src/Avalonia.X11/X11Globals.cs
index 057693f810..39834a44b3 100644
--- a/src/Avalonia.X11/X11Globals.cs
+++ b/src/Avalonia.X11/X11Globals.cs
@@ -41,7 +41,7 @@ namespace Avalonia.X11
{
_wmName = value;
// The collection might change during enumeration
- foreach (var s in _subscribers.ToList())
+ foreach (var s in _subscribers.ToArray())
s.WmChanged(value);
}
}
@@ -69,7 +69,7 @@ namespace Avalonia.X11
{
_isCompositionEnabled = value;
// The collection might change during enumeration
- foreach (var s in _subscribers.ToList())
+ foreach (var s in _subscribers.ToArray())
s.CompositionChanged(value);
}
}
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 520b35dd03..066156a652 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -134,8 +134,8 @@ namespace Avalonia.X11
SetWindowValuemask.WinGravity | SetWindowValuemask.BackingStore)), ref attr);
else
_renderHandle = _handle;
-
- Handle = new PlatformHandle(_handle, "XID");
+
+ Handle = new SurfacePlatformHandle(this);
_realSize = new PixelSize(defaultWidth, defaultHeight);
platform.Windows[_handle] = OnEvent;
XEventMask ignoredMask = XEventMask.SubstructureRedirectMask
@@ -169,7 +169,9 @@ namespace Avalonia.X11
if (glx != null)
surfaces.Insert(0, new GlxGlPlatformSurface(glx.Display, glx.DeferredContext,
new SurfaceInfo(this, _x11.Display, _handle, _renderHandle)));
-
+
+ surfaces.Add(Handle);
+
Surfaces = surfaces.ToArray();
UpdateMotifHints();
UpdateSizeHints(null);
@@ -1142,5 +1144,23 @@ namespace Avalonia.X11
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8);
public bool NeedsManagedDecorations => false;
+
+
+ public class SurfacePlatformHandle : IPlatformNativeSurfaceHandle
+ {
+ private readonly X11Window _owner;
+
+ public PixelSize Size => _owner.ToPixelSize(_owner.ClientSize);
+
+ public double Scaling => _owner.RenderScaling;
+
+ public SurfacePlatformHandle(X11Window owner)
+ {
+ _owner = owner;
+ }
+
+ public IntPtr Handle => _owner._renderHandle;
+ public string? HandleDescriptor => "XID";
+ }
}
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
index e218da5f41..64fafc65f3 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
@@ -1,7 +1,9 @@
using System;
+using System.IO;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods;
using static Avalonia.LinuxFramebuffer.Output.LibDrm;
@@ -142,10 +144,28 @@ namespace Avalonia.LinuxFramebuffer.Output
public int Fd { get; private set; }
public DrmCard(string path = null)
{
- path = path ?? "/dev/dri/card0";
- Fd = open(path, 2, 0);
- if (Fd == -1)
- throw new Win32Exception("Couldn't open " + path);
+ if(path == null)
+ {
+ var files = Directory.GetFiles("/dev/dri/");
+
+ foreach(var file in files)
+ {
+ var match = Regex.Match(file, "card[0-9]+");
+
+ if(match.Success)
+ {
+ Fd = open(file, 2, 0);
+ if(Fd != -1) break;
+ }
+ }
+
+ if(Fd == -1) throw new Win32Exception("Couldn't open /dev/dri/card[0-9]+");
+ }
+ else
+ {
+ Fd = open(path, 2, 0);
+ if(Fd != -1) throw new Win32Exception($"Couldn't open {path}");
+ }
}
public DrmResources GetResources() => new DrmResources(Fd);
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index af3b570fd7..1c92c7d193 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -35,6 +35,9 @@ namespace Avalonia.Skia
var gl = AvaloniaLocator.Current.GetService();
if (gl != null)
_skiaGpu = new GlSkiaGpu(gl, maxResourceBytes);
+
+ //TODO: SKFont crashes when disposed in finalizer so we keep it alive
+ GC.SuppressFinalize(s_font);
}
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);
diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
index c4d11f4613..a0890262e7 100644
--- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs
+++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
@@ -13,12 +13,16 @@ namespace Avalonia.Skia
{
internal class TextShaperImpl : ITextShaperImpl
{
- public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
- CultureInfo culture, sbyte bidiLevel)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options)
{
+ var typeface = options.Typeface;
+ var fontRenderingEmSize = options.FontRenderingEmSize;
+ var bidiLevel = options.BidLevel;
+ var culture = options.Culture;
+
using (var buffer = new Buffer())
{
- buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length);
+ buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length);
MergeBreakPair(buffer);
@@ -61,6 +65,15 @@ namespace Avalonia.Skia
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
+ if(glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
+ {
+ glyphIndex = typeface.GetGlyph(' ');
+
+ glyphAdvance = options.IncrementalTabWidth > 0 ?
+ options.IncrementalTabWidth :
+ 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
+ }
+
var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
shapedBuffer[i] = targetInfo;
diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
index 62cf031f86..59027a663f 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
@@ -14,9 +14,13 @@ namespace Avalonia.Direct2D1.Media
internal class TextShaperImpl : ITextShaperImpl
{
- public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
- CultureInfo culture, sbyte bidiLevel)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options)
{
+ var typeface = options.Typeface;
+ var fontRenderingEmSize = options.FontRenderingEmSize;
+ var bidiLevel = options.BidLevel;
+ var culture = options.Culture;
+
using (var buffer = new Buffer())
{
buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length);
@@ -62,6 +66,15 @@ internal class TextShaperImpl : ITextShaperImpl
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
+ if (glyphIndex == 0 && text[glyphCluster] == '\t')
+ {
+ glyphIndex = typeface.GetGlyph(' ');
+
+ glyphAdvance = options.IncrementalTabWidth > 0 ?
+ options.IncrementalTabWidth :
+ 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
+ }
+
var targetInfo =
new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance,
glyphOffset);
diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs
index 23395dd9b5..6484ae6c54 100644
--- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs
+++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs
@@ -217,8 +217,9 @@ namespace Avalonia.Win32
}
public IReadOnlyList Screens =>
- _hiddenWindow.Screens.All.Select(s => new ManagedPopupPositionerScreenInfo(
- s.Bounds.ToRect(1), s.Bounds.ToRect(1))).ToList();
+ _hiddenWindow.Screens.All
+ .Select(s => new ManagedPopupPositionerScreenInfo(s.Bounds.ToRect(1), s.Bounds.ToRect(1)))
+ .ToArray();
public Rect ParentClientAreaScreenGeometry
{
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 41269ee7ca..f0036236ec 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -459,7 +459,7 @@ namespace Avalonia.Win32
}
}
- public IEnumerable