Browse Source

Merge branch 'master' into feature/context-management-8

pull/9639/head
Nikita Tsukanov 3 years ago
committed by GitHub
parent
commit
c1accad11a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .ncrunch/ReactiveUIDemo.v3.ncrunchproject
  2. 1
      Avalonia.Desktop.slnf
  3. 7
      Avalonia.sln
  4. 3
      samples/ControlCatalog/MainView.xaml
  5. 27
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  6. 36
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
  7. 26
      samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs
  8. 8
      samples/ReactiveUIDemo/App.axaml
  9. 37
      samples/ReactiveUIDemo/App.axaml.cs
  10. 19
      samples/ReactiveUIDemo/MainWindow.axaml
  11. 22
      samples/ReactiveUIDemo/MainWindow.axaml.cs
  12. 28
      samples/ReactiveUIDemo/ReactiveUIDemo.csproj
  13. 11
      samples/ReactiveUIDemo/ViewModels/BarViewModel.cs
  14. 11
      samples/ReactiveUIDemo/ViewModels/FooViewModel.cs
  15. 9
      samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs
  16. 21
      samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs
  17. 16
      samples/ReactiveUIDemo/Views/BarView.axaml
  18. 28
      samples/ReactiveUIDemo/Views/BarView.axaml.cs
  19. 16
      samples/ReactiveUIDemo/Views/FooView.axaml
  20. 28
      samples/ReactiveUIDemo/Views/FooView.axaml.cs
  21. 2
      samples/RenderDemo/Pages/TextFormatterPage.axaml.cs
  22. 3
      src/Avalonia.Base/Animation/Animators/Animator`1.cs
  23. 152
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  24. 8
      src/Avalonia.Base/Input/Gestures.cs
  25. 5
      src/Avalonia.Base/Input/InputElement.cs
  26. 43
      src/Avalonia.Base/Input/PullGestureEventArgs.cs
  27. 8
      src/Avalonia.Base/Media/FormattedText.cs
  28. 419
      src/Avalonia.Base/Media/GlyphRun.cs
  29. 18
      src/Avalonia.Base/Media/GlyphRunMetrics.cs
  30. 293
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  31. 115
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs
  32. 13
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  33. 19
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  34. 20
      src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs
  35. 31
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  36. 19
      src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs
  37. 2
      src/Avalonia.Base/Media/TextFormatting/SplitResult.cs
  38. 129
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  39. 102
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  40. 4
      src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs
  41. 100
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  42. 2
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  43. 5
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  44. 358
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  45. 6
      src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs
  46. 4
      src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs
  47. 11
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  48. 11
      src/Avalonia.Base/Media/TextFormatting/TextShaper.cs
  49. 3
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  50. 2
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  51. 3
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  52. 9
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  53. 7
      src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  54. 8
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  55. 12
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  56. 15
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  57. 11
      src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs
  58. 11
      src/Avalonia.Base/Media/TextTrailingTrimming.cs
  59. 2
      src/Avalonia.Base/Media/TextTrimming.cs
  60. 5
      src/Avalonia.Base/Platform/ITextShaperImpl.cs
  61. 4
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
  62. 3
      src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
  63. 67
      src/Avalonia.Base/StyledElement.cs
  64. 3
      src/Avalonia.Base/Styling/StyleInstance.cs
  65. 8
      src/Avalonia.Base/Utilities/ArraySlice.cs
  66. 239
      src/Avalonia.Base/Utilities/ReadOnlySlice.cs
  67. 90
      src/Avalonia.Base/Visual.cs
  68. 6
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  69. 5
      src/Avalonia.Controls/ComboBox.cs
  70. 85
      src/Avalonia.Controls/Control.cs
  71. 4
      src/Avalonia.Controls/Documents/LineBreak.cs
  72. 2
      src/Avalonia.Controls/Documents/Run.cs
  73. 36
      src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs
  74. 252
      src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs
  75. 141
      src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs
  76. 42
      src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs
  77. 553
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
  78. 13
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs
  79. 14
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs
  80. 274
      src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs
  81. 27
      src/Avalonia.Controls/TextBlock.cs
  82. 4
      src/Avalonia.Controls/TextBox.cs
  83. 8
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  84. 6
      src/Avalonia.Controls/TopLevel.cs
  85. 20
      src/Avalonia.Controls/TreeView.cs
  86. 124
      src/Avalonia.Controls/TreeViewItem.cs
  87. 267
      src/Avalonia.Controls/Window.cs
  88. 58
      src/Avalonia.Controls/WindowBase.cs
  89. 6
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  90. 4
      src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
  91. 6
      src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
  92. 2
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  93. 24
      src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml
  94. 29
      src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml
  95. 1
      src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
  96. 3
      src/Avalonia.Themes.Simple/Accents/BaseDark.xaml
  97. 4
      src/Avalonia.Themes.Simple/Accents/BaseLight.xaml
  98. 24
      src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml
  99. 31
      src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml
  100. 2
      src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml

5
.ncrunch/ReactiveUIDemo.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

1
Avalonia.Desktop.slnf

@ -9,6 +9,7 @@
"samples\\MiniMvvm\\MiniMvvm.csproj",
"samples\\SampleControls\\ControlSamples.csproj",
"samples\\Sandbox\\Sandbox.csproj",
"samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"src\\Avalonia.Base\\Avalonia.Base.csproj",
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",

7
Avalonia.sln

@ -232,6 +232,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser", "s
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -543,6 +545,10 @@ Global
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.Build.0 = Release|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -607,6 +613,7 @@ Global
{47F8530C-F19B-4B1A-B4D6-EB231522AE5D} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

3
samples/ControlCatalog/MainView.xaml

@ -135,6 +135,9 @@
<TabItem Header="RadioButton">
<pages:RadioButtonPage />
</TabItem>
<TabItem Header="RefreshContainer">
<pages:RefreshContainerPage />
</TabItem>
<TabItem Header="RelativePanel">
<pages:RelativePanelPage />
</TabItem>

27
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

@ -0,0 +1,27 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="using:ControlCatalog.ViewModels"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:DataType="viewModels:RefreshContainerViewModel"
x:Class="ControlCatalog.Pages.RefreshContainerPage">
<DockPanel HorizontalAlignment="Stretch"
Height="600"
VerticalAlignment="Top">
<Label DockPanel.Dock="Top">A control that supports pull to refresh</Label>
<RefreshContainer Name="Refresh"
DockPanel.Dock="Bottom"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
PullDirection="TopToBottom"
RefreshRequested="RefreshContainerPage_RefreshRequested"
Margin="5">
<ListBox HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Items="{Binding Items}"/>
</RefreshContainer>
</DockPanel>
</UserControl>

36
samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs

@ -0,0 +1,36 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
public class RefreshContainerPage : UserControl
{
private RefreshContainerViewModel _viewModel;
public RefreshContainerPage()
{
this.InitializeComponent();
_viewModel = new RefreshContainerViewModel();
DataContext = _viewModel;
}
private async void RefreshContainerPage_RefreshRequested(object? sender, RefreshRequestedEventArgs e)
{
var deferral = e.GetDeferral();
await _viewModel.AddToTop();
deferral.Complete();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

26
samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs

@ -0,0 +1,26 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using ControlCatalog.Pages;
using MiniMvvm;
namespace ControlCatalog.ViewModels
{
public class RefreshContainerViewModel : ViewModelBase
{
public ObservableCollection<string> Items { get; }
public RefreshContainerViewModel()
{
Items = new ObservableCollection<string>(Enumerable.Range(1, 200).Select(i => $"Item {i}"));
}
public async Task AddToTop()
{
await Task.Delay(3000);
Items.Insert(0, $"Item {200 - Items.Count}");
}
}
}

8
samples/ReactiveUIDemo/App.axaml

@ -0,0 +1,8 @@
<Application
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ReactiveUIDemo.App">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

37
samples/ReactiveUIDemo/App.axaml.cs

@ -0,0 +1,37 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using ReactiveUI;
using ReactiveUIDemo.ViewModels;
using ReactiveUIDemo.Views;
using Splat;
namespace ReactiveUIDemo
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
Locator.CurrentMutable.Register(() => new FooView(), typeof(IViewFor<FooViewModel>));
Locator.CurrentMutable.Register(() => new BarView(), typeof(IViewFor<BarViewModel>));
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow = new MainWindow();
base.OnFrameworkInitializationCompleted();
}
public static int Main(string[] args)
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.UseReactiveUI()
.LogToTrace();
}
}

19
samples/ReactiveUIDemo/MainWindow.axaml

@ -0,0 +1,19 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class="ReactiveUIDemo.MainWindow"
xmlns:vm="using:ReactiveUIDemo.ViewModels"
xmlns:rxui="using:Avalonia.ReactiveUI"
Title="AvaloniaUI ReactiveUI Demo"
x:DataType="vm:MainWindowViewModel">
<TabControl TabStripPlacement="Left">
<TabItem Header="RoutedViewHost">
<DockPanel DataContext="{Binding RoutedViewHost}">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="8">
<Button Command="{Binding ShowFoo}">Foo</Button>
<Button Command="{Binding ShowBar}">Bar</Button>
</StackPanel>
<rxui:RoutedViewHost Router="{Binding Router}"/>
</DockPanel>
</TabItem>
</TabControl>
</Window>

22
samples/ReactiveUIDemo/MainWindow.axaml.cs

@ -0,0 +1,22 @@
using ReactiveUIDemo.ViewModels;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ReactiveUIDemo
{
public class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
this.DataContext = new MainWindowViewModel();
this.AttachDevTools();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

28
samples/ReactiveUIDemo/ReactiveUIDemo.csproj

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Views\BarView.axaml.cs">
<DependentUpon>BarView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\FooView.axaml.cs">
<DependentUpon>FooView.axaml</DependentUpon>
</Compile>
</ItemGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\ReactiveUI.props" />
</Project>

11
samples/ReactiveUIDemo/ViewModels/BarViewModel.cs

@ -0,0 +1,11 @@
using ReactiveUI;
namespace ReactiveUIDemo.ViewModels
{
internal class BarViewModel : ReactiveObject, IRoutableViewModel
{
public BarViewModel(IScreen screen) => HostScreen = screen;
public string UrlPathSegment => "Bar";
public IScreen HostScreen { get; }
}
}

11
samples/ReactiveUIDemo/ViewModels/FooViewModel.cs

@ -0,0 +1,11 @@
using ReactiveUI;
namespace ReactiveUIDemo.ViewModels
{
internal class FooViewModel : ReactiveObject, IRoutableViewModel
{
public FooViewModel(IScreen screen) => HostScreen = screen;
public string UrlPathSegment => "Foo";
public IScreen HostScreen { get; }
}
}

9
samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs

@ -0,0 +1,9 @@
using ReactiveUI;
namespace ReactiveUIDemo.ViewModels
{
internal class MainWindowViewModel : ReactiveObject
{
public RoutedViewHostPageViewModel RoutedViewHost { get; } = new();
}
}

21
samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs

@ -0,0 +1,21 @@
using ReactiveUI;
namespace ReactiveUIDemo.ViewModels
{
internal class RoutedViewHostPageViewModel : ReactiveObject, IScreen
{
public RoutedViewHostPageViewModel()
{
Foo = new(this);
Bar = new(this);
Router.Navigate.Execute(Foo);
}
public RoutingState Router { get; } = new();
public FooViewModel Foo { get; }
public BarViewModel Bar { get; }
public void ShowFoo() => Router.Navigate.Execute(Foo);
public void ShowBar() => Router.Navigate.Execute(Bar);
}
}

16
samples/ReactiveUIDemo/Views/BarView.axaml

@ -0,0 +1,16 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ReactiveUIDemo.Views.BarView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border Background="Blue">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="48">
Bar!
</TextBlock>
</Border>
</UserControl>

28
samples/ReactiveUIDemo/Views/BarView.axaml.cs

@ -0,0 +1,28 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ReactiveUI;
using ReactiveUIDemo.ViewModels;
namespace ReactiveUIDemo.Views
{
internal partial class BarView : UserControl, IViewFor<BarViewModel>
{
public BarView()
{
InitializeComponent();
}
public BarViewModel? ViewModel { get; set; }
object? IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (BarViewModel?)value;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

16
samples/ReactiveUIDemo/Views/FooView.axaml

@ -0,0 +1,16 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ReactiveUIDemo.Views.FooView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border Background="Red">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="48">
Foo!
</TextBlock>
</Border>
</UserControl>

28
samples/ReactiveUIDemo/Views/FooView.axaml.cs

@ -0,0 +1,28 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ReactiveUI;
using ReactiveUIDemo.ViewModels;
namespace ReactiveUIDemo.Views
{
internal partial class FooView : UserControl, IViewFor<FooViewModel>
{
public FooView()
{
InitializeComponent();
}
public FooViewModel? ViewModel { get; set; }
object? IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (FooViewModel?)value;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

2
samples/RenderDemo/Pages/TextFormatterPage.axaml.cs

@ -90,7 +90,7 @@ namespace RenderDemo.Pages
return new ControlRun(_control, _defaultProperties);
}
return new TextCharacters(_text.AsMemory(), _defaultProperties);
return new TextCharacters(_text, _defaultProperties);
}
}

3
src/Avalonia.Base/Animation/Animators/Animator`1.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Animation.Utils;
using Avalonia.Collections;
@ -39,7 +40,7 @@ namespace Avalonia.Animation.Animators
VerifyConvertKeyFrames();
var subject = new DisposeAnimationInstanceSubject<T>(this, animation, control, clock, onComplete);
return match.Subscribe(subject);
return new CompositeDisposable(match.Subscribe(subject), subject);
}
protected T InterpolationHandler(double animationTime, T neutralValue)

152
src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs

@ -0,0 +1,152 @@
using Avalonia.Input.GestureRecognizers;
namespace Avalonia.Input
{
public class PullGestureRecognizer : StyledElement, IGestureRecognizer
{
private IInputElement? _target;
private IGestureRecognizerActionsDispatcher? _actions;
private Point _initialPosition;
private int _gestureId;
private IPointer? _tracking;
private PullDirection _pullDirection;
/// <summary>
/// Defines the <see cref="PullDirection"/> property.
/// </summary>
public static readonly DirectProperty<PullGestureRecognizer, PullDirection> PullDirectionProperty =
AvaloniaProperty.RegisterDirect<PullGestureRecognizer, PullDirection>(
nameof(PullDirection),
o => o.PullDirection,
(o, v) => o.PullDirection = v);
public PullDirection PullDirection
{
get => _pullDirection;
set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value);
}
public PullGestureRecognizer(PullDirection pullDirection)
{
PullDirection = pullDirection;
}
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
{
_target = target;
_actions = actions;
_target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
_target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
}
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
PointerPressed(e);
}
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
PointerReleased(e);
}
public void PointerCaptureLost(IPointer pointer)
{
if (_tracking == pointer)
{
EndPull();
}
}
public void PointerMoved(PointerEventArgs e)
{
if (_tracking == e.Pointer && _target is Visual visual)
{
var currentPosition = e.GetPosition(visual);
_actions!.Capture(e.Pointer, this);
Vector delta = default;
switch (PullDirection)
{
case PullDirection.TopToBottom:
if (currentPosition.Y > _initialPosition.Y)
{
delta = new Vector(0, currentPosition.Y - _initialPosition.Y);
}
break;
case PullDirection.BottomToTop:
if (currentPosition.Y < _initialPosition.Y)
{
delta = new Vector(0, _initialPosition.Y - currentPosition.Y);
}
break;
case PullDirection.LeftToRight:
if (currentPosition.X > _initialPosition.X)
{
delta = new Vector(currentPosition.X - _initialPosition.X, 0);
}
break;
case PullDirection.RightToLeft:
if (currentPosition.X < _initialPosition.X)
{
delta = new Vector(_initialPosition.X - currentPosition.X, 0);
}
break;
}
_target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection));
}
}
public void PointerPressed(PointerPressedEventArgs e)
{
if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
{
var position = e.GetPosition(visual);
var canPull = false;
var bounds = visual.Bounds;
switch (PullDirection)
{
case PullDirection.TopToBottom:
canPull = position.Y < bounds.Height * 0.1;
break;
case PullDirection.BottomToTop:
canPull = position.Y > bounds.Height - (bounds.Height * 0.1);
break;
case PullDirection.LeftToRight:
canPull = position.X < bounds.Width * 0.1;
break;
case PullDirection.RightToLeft:
canPull = position.X > bounds.Width - (bounds.Width * 0.1);
break;
}
if (canPull)
{
_gestureId = PullGestureEventArgs.GetNextFreeId();
_tracking = e.Pointer;
_initialPosition = position;
}
}
}
public void PointerReleased(PointerReleasedEventArgs e)
{
if (_tracking == e.Pointer)
{
EndPull();
}
}
private void EndPull()
{
_tracking = null;
_initialPosition = default;
_target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection));
}
}
}

8
src/Avalonia.Base/Input/Gestures.cs

@ -46,6 +46,14 @@ namespace Avalonia.Input
private static readonly WeakReference<object?> s_lastPress = new WeakReference<object?>(null);
private static Point s_lastPressPoint;
public static readonly RoutedEvent<PullGestureEventArgs> PullGestureEvent =
RoutedEvent.Register<PullGestureEventArgs>(
"PullGesture", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<PullGestureEndedEventArgs> PullGestureEndedEvent =
RoutedEvent.Register<PullGestureEndedEventArgs>(
"PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
static Gestures()
{
InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed);

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

@ -442,6 +442,11 @@ namespace Avalonia.Input
{
SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value);
PseudoClasses.Set(":disabled", !value);
if (!IsEffectivelyEnabled && FocusManager.Instance?.Current == this)
{
FocusManager.Instance?.Focus(null);
}
}
}

43
src/Avalonia.Base/Input/PullGestureEventArgs.cs

@ -0,0 +1,43 @@
using System;
using Avalonia.Interactivity;
namespace Avalonia.Input
{
public class PullGestureEventArgs : RoutedEventArgs
{
public int Id { get; }
public Vector Delta { get; }
public PullDirection PullDirection { get; }
private static int _nextId = 1;
internal static int GetNextFreeId() => _nextId++;
public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(Gestures.PullGestureEvent)
{
Id = id;
Delta = delta;
PullDirection = pullDirection;
}
}
public class PullGestureEndedEventArgs : RoutedEventArgs
{
public int Id { get; }
public PullDirection PullDirection { get; }
public PullGestureEndedEventArgs(int id, PullDirection pullDirection) : base(Gestures.PullGestureEndedEvent)
{
Id = id;
PullDirection = pullDirection;
}
}
public enum PullDirection
{
TopToBottom,
BottomToTop,
LeftToRight,
RightToLeft
}
}

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

@ -1,10 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
@ -25,7 +23,7 @@ namespace Avalonia.Media
private const double MaxFontEmSize = RealInfiniteWidth / GreatestMultiplierOfEm;
// properties and format runs
private ReadOnlySlice<char> _text;
private string _text;
private readonly SpanVector _formatRuns = new SpanVector(null);
private SpanPosition _latestPosition;
@ -69,9 +67,7 @@ namespace Avalonia.Media
ValidateFontSize(emSize);
_text = textToFormat != null ?
new ReadOnlySlice<char>(textToFormat.AsMemory()) :
throw new ArgumentNullException(nameof(textToFormat));
_text = textToFormat;
var runProps = new GenericTextRunProperties(
typeface,

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

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -22,15 +21,12 @@ namespace Avalonia.Media
private Point? _baselineOrigin;
private GlyphRunMetrics? _glyphRunMetrics;
private ReadOnlySlice<char> _characters;
private IReadOnlyList<char> _characters;
private IReadOnlyList<ushort> _glyphIndices;
private IReadOnlyList<double>? _glyphAdvances;
private IReadOnlyList<Vector>? _glyphOffsets;
private IReadOnlyList<int>? _glyphClusters;
private int _offsetToFirstCharacter;
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// </summary>
@ -45,7 +41,7 @@ namespace Avalonia.Media
public GlyphRun(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
ReadOnlySlice<char> characters,
IReadOnlyList<char> characters,
IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double>? glyphAdvances = null,
IReadOnlyList<Vector>? glyphOffsets = null,
@ -54,19 +50,19 @@ namespace Avalonia.Media
{
_glyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
_fontRenderingEmSize = fontRenderingEmSize;
Characters = characters;
_characters = characters;
_glyphIndices = glyphIndices;
GlyphAdvances = glyphAdvances;
_glyphAdvances = glyphAdvances;
GlyphOffsets = glyphOffsets;
_glyphOffsets = glyphOffsets;
GlyphClusters = glyphClusters;
_glyphClusters = glyphClusters;
BiDiLevel = biDiLevel;
_biDiLevel = biDiLevel;
}
/// <summary>
@ -145,7 +141,7 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
/// </summary>
public ReadOnlySlice<char> Characters
public IReadOnlyList<char> Characters
{
get => _characters;
set => Set(ref _characters, value);
@ -219,7 +215,7 @@ namespace Avalonia.Media
/// </returns>
public double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter;
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var distance = 0.0;
@ -227,12 +223,12 @@ namespace Avalonia.Media
{
if (GlyphClusters != null)
{
if (characterIndex < GlyphClusters[0])
if (characterIndex < Metrics.FirstCluster)
{
return 0;
}
if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
if (characterIndex > Metrics.LastCluster)
{
return Metrics.WidthIncludingTrailingWhitespace;
}
@ -268,12 +264,12 @@ namespace Avalonia.Media
if (GlyphClusters != null && GlyphClusters.Count > 0)
{
if (characterIndex > GlyphClusters[0])
if (characterIndex > Metrics.LastCluster)
{
return 0;
}
if (characterIndex <= GlyphClusters[GlyphClusters.Count - 1])
if (characterIndex <= Metrics.FirstCluster)
{
return Size.Width;
}
@ -299,19 +295,12 @@ namespace Avalonia.Media
/// </returns>
public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
{
var characterIndex = 0;
// Before
if (distance <= 0)
{
isInside = false;
if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
var firstCharacterHit = FindNearestCharacterHit(characterIndex, out _);
var firstCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.FirstCluster : Metrics.LastCluster, out _);
return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit;
}
@ -321,18 +310,13 @@ namespace Avalonia.Media
{
isInside = false;
characterIndex = GlyphIndices.Count - 1;
if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);
var lastCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.LastCluster : Metrics.FirstCluster, out _);
return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
}
var characterIndex = 0;
//Within
var currentX = 0d;
@ -378,7 +362,7 @@ namespace Avalonia.Media
var characterHit = FindNearestCharacterHit(characterIndex, out var width);
var delta = width / 2;
var offset = IsLeftToRight ? Math.Round(distance - currentX, 3) : Math.Round(currentX - distance, 3);
var isTrailing = offset > delta;
@ -400,24 +384,15 @@ namespace Avalonia.Media
{
characterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
return textPosition > _characters.End ?
characterHit :
new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
var nextCharacterHit =
FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
if (characterHit.FirstCharacterIndex == Metrics.LastCluster)
{
return characterHit;
}
if (characterHit == nextCharacterHit)
{
return characterHit;
return new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
return characterHit.TrailingLength > 0 ?
nextCharacterHit :
new CharacterHit(nextCharacterHit.FirstCharacterIndex);
return FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
}
/// <summary>
@ -454,29 +429,24 @@ namespace Avalonia.Media
return characterIndex;
}
if (IsLeftToRight)
if (characterIndex > Metrics.LastCluster)
{
if (characterIndex < GlyphClusters[0])
if (IsLeftToRight)
{
return 0;
return GlyphIndices.Count - 1;
}
if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
{
return GlyphClusters.Count - 1;
}
return 0;
}
else
{
if (characterIndex < GlyphClusters[GlyphClusters.Count - 1])
{
return GlyphClusters.Count - 1;
}
if (characterIndex > GlyphClusters[0])
if (characterIndex < Metrics.FirstCluster)
{
if (IsLeftToRight)
{
return 0;
}
return GlyphIndices.Count - 1;
}
var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
@ -498,7 +468,7 @@ namespace Avalonia.Media
if (start < 0)
{
return -1;
goto result;
}
}
@ -517,6 +487,18 @@ namespace Avalonia.Media
}
}
result:
if (start < 0)
{
return 0;
}
if (start > GlyphIndices.Count - 1)
{
return GlyphIndices.Count - 1;
}
return start;
}
@ -532,20 +514,20 @@ namespace Avalonia.Media
{
width = 0.0;
var start = FindGlyphIndex(index);
var glyphIndex = FindGlyphIndex(index);
if (GlyphClusters == null)
{
width = GetGlyphAdvance(index, out _);
return new CharacterHit(start, 1);
return new CharacterHit(glyphIndex, 1);
}
var cluster = GlyphClusters[start];
var cluster = GlyphClusters[glyphIndex];
var nextCluster = cluster;
var currentIndex = start;
var currentIndex = glyphIndex;
while (nextCluster == cluster)
{
@ -571,20 +553,64 @@ namespace Avalonia.Media
}
nextCluster = GlyphClusters[currentIndex];
}
}
int trailingLength;
var clusterLength = Math.Max(0, nextCluster - cluster);
if (nextCluster == cluster)
{
trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster;
}
else
if (cluster == Metrics.LastCluster && clusterLength == 0)
{
trailingLength = nextCluster - cluster;
var characterLength = 0;
var currentCluster = Metrics.FirstCluster;
if (IsLeftToRight)
{
for (int i = 1; i < GlyphClusters.Count; i++)
{
nextCluster = GlyphClusters[i];
if (currentCluster > cluster)
{
break;
}
var length = nextCluster - currentCluster;
characterLength += length;
currentCluster = nextCluster;
}
}
else
{
for (int i = GlyphClusters.Count - 1; i >= 0; i--)
{
nextCluster = GlyphClusters[i];
if (currentCluster > cluster)
{
break;
}
var length = nextCluster - currentCluster;
characterLength += length;
currentCluster = nextCluster;
}
}
if (Characters != null)
{
clusterLength = Characters.Count - characterLength;
}
else
{
clusterLength = 1;
}
}
return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength);
return new CharacterHit(cluster, clusterLength);
}
/// <summary>
@ -618,22 +644,25 @@ namespace Avalonia.Media
private GlyphRunMetrics CreateGlyphRunMetrics()
{
var firstCluster = 0;
var lastCluster = Characters.Length - 1;
int firstCluster = 0, lastCluster = 0;
if (!IsLeftToRight)
if (_glyphClusters != null && _glyphClusters.Count > 0)
{
var cluster = firstCluster;
firstCluster = lastCluster;
lastCluster = cluster;
firstCluster = _glyphClusters[0];
lastCluster = _glyphClusters[_glyphClusters.Count - 1];
}
if (GlyphClusters != null && GlyphClusters.Count > 0)
else
{
firstCluster = GlyphClusters[0];
lastCluster = GlyphClusters[GlyphClusters.Count - 1];
if (Characters != null && Characters.Count > 0)
{
firstCluster = 0;
lastCluster = Characters.Count - 1;
}
}
_offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster);
if (!IsLeftToRight)
{
(lastCluster, firstCluster) = (firstCluster, lastCluster);
}
var isReversed = firstCluster > lastCluster;
@ -666,12 +695,19 @@ namespace Avalonia.Media
}
}
return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength,
height);
return new GlyphRunMetrics(
width,
widthIncludingTrailingWhitespace,
height,
trailingWhitespaceLength,
newLineLength,
firstCluster,
lastCluster
);
}
private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount)
{
{
if (isReversed)
{
return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
@ -681,66 +717,82 @@ namespace Avalonia.Media
newLineLength = 0;
var trailingWhitespaceLength = 0;
if (GlyphClusters == null)
if (Characters != null)
{
for (var i = _characters.Length - 1; i >= 0;)
if (GlyphClusters == null)
{
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (!codepoint.IsWhiteSpace)
for (var i = _characters.Count - 1; i >= 0;)
{
break;
}
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (codepoint.IsBreakChar)
{
newLineLength++;
}
if (!codepoint.IsWhiteSpace)
{
break;
}
trailingWhitespaceLength++;
if (codepoint.IsBreakChar)
{
newLineLength++;
}
trailingWhitespaceLength++;
i -= count;
glyphCount++;
i -= count;
glyphCount++;
}
}
}
else
{
for (var i = GlyphClusters.Count - 1; i >= 0; i--)
else
{
var currentCluster = GlyphClusters[i];
var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
if (!codepoint.IsWhiteSpace)
if (Characters.Count > 0)
{
break;
}
var characterIndex = Characters.Count - 1;
var clusterLength = 1;
for (var i = GlyphClusters.Count - 1; i >= 0; i--)
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
while(i - 1 >= 0)
{
var nextCluster = GlyphClusters[i - 1];
characterIndex -= characterLength;
if(currentCluster == nextCluster)
{
clusterLength++;
i--;
if (!codepoint.IsWhiteSpace)
{
break;
}
continue;
}
var clusterLength = 1;
break;
}
while (i - 1 >= 0)
{
var nextCluster = GlyphClusters[i - 1];
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
if (currentCluster == nextCluster)
{
clusterLength++;
i--;
if(characterIndex >= 0)
{
codepoint = Codepoint.ReadAt(_characters, characterIndex, out characterLength);
characterIndex -= characterLength;
}
continue;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
glyphCount++;
trailingWhitespaceLength += clusterLength;
glyphCount++;
}
}
}
}
@ -753,67 +805,73 @@ namespace Avalonia.Media
newLineLength = 0;
var trailingWhitespaceLength = 0;
if (GlyphClusters == null)
if (Characters != null)
{
for (var i = 0; i < Characters.Length;)
if (GlyphClusters == null)
{
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (!codepoint.IsWhiteSpace)
for (var i = 0; i < Characters.Count;)
{
break;
}
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (codepoint.IsBreakChar)
{
newLineLength++;
}
if (!codepoint.IsWhiteSpace)
{
break;
}
trailingWhitespaceLength++;
if (codepoint.IsBreakChar)
{
newLineLength++;
}
i += count;
glyphCount++;
trailingWhitespaceLength++;
i += count;
glyphCount++;
}
}
}
else
{
for (var i = 0; i < GlyphClusters.Count; i++)
else
{
var currentCluster = GlyphClusters[i];
var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
var characterIndex = 0;
if (!codepoint.IsWhiteSpace)
for (var i = 0; i < GlyphClusters.Count; i++)
{
break;
}
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
var clusterLength = 1;
characterIndex += characterLength;
var j = i;
if (!codepoint.IsWhiteSpace)
{
break;
}
while (j - 1 >= 0)
{
var nextCluster = GlyphClusters[--j];
var clusterLength = 1;
if (currentCluster == nextCluster)
var j = i;
while (j - 1 >= 0)
{
clusterLength++;
var nextCluster = GlyphClusters[--j];
continue;
}
if (currentCluster == nextCluster)
{
clusterLength++;
break;
}
continue;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
trailingWhitespaceLength += clusterLength;
glyphCount += clusterLength;
glyphCount += clusterLength;
}
}
}
@ -855,14 +913,9 @@ namespace Avalonia.Media
throw new InvalidOperationException();
}
_glyphRunImpl = CreateGlyphRunImpl();
}
private IGlyphRunImpl CreateGlyphRunImpl()
{
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
}
void IDisposable.Dispose()

18
src/Avalonia.Base/Media/GlyphRunMetrics.cs

@ -2,24 +2,30 @@
{
public readonly struct GlyphRunMetrics
{
public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, int trailingWhitespaceLength,
int newlineLength, double height)
public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, double height,
int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster)
{
Width = width;
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
TrailingWhitespaceLength = trailingWhitespaceLength;
NewlineLength = newlineLength;
Height = height;
TrailingWhitespaceLength = trailingWhitespaceLength;
NewLineLength= newLineLength;
FirstCluster = firstCluster;
LastCluster = lastCluster;
}
public double Width { get; }
public double WidthIncludingTrailingWhitespace { get; }
public double Height { get; }
public int TrailingWhitespaceLength { get; }
public int NewlineLength { get; }
public int NewLineLength { get; }
public double Height { get; }
public int FirstCluster { get; }
public int LastCluster { get; }
}
}

293
src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs

@ -0,0 +1,293 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
public readonly struct CharacterBufferRange : IReadOnlyList<char>
{
/// <summary>
/// Getting an empty character string
/// </summary>
public static CharacterBufferRange Empty => new CharacterBufferRange();
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from character array
/// </summary>
/// <param name="characterArray">character array</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
/// <param name="characterLength">character length</param>
public CharacterBufferRange(
char[] characterArray,
int offsetToFirstChar,
int characterLength
)
: this(
new CharacterBufferReference(characterArray, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from string
/// </summary>
/// <param name="characterString">character string</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
/// <param name="characterLength">character length</param>
public CharacterBufferRange(
string characterString,
int offsetToFirstChar,
int characterLength
)
: this(
new CharacterBufferReference(characterString, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from <see cref="CharacterBufferReference"/>
/// </summary>
/// <param name="characterBufferReference">character buffer reference</param>
/// <param name="characterLength">number of characters</param>
public CharacterBufferRange(
CharacterBufferReference characterBufferReference,
int characterLength
)
{
if (characterLength < 0)
{
throw new ArgumentOutOfRangeException("characterLength", "ParameterCannotBeNegative");
}
int maxLength = characterBufferReference.CharacterBuffer.Length > 0 ?
characterBufferReference.CharacterBuffer.Length - characterBufferReference.OffsetToFirstChar :
0;
if (characterLength > maxLength)
{
throw new ArgumentOutOfRangeException("characterLength", $"ParameterCannotBeGreaterThan {maxLength}");
}
CharacterBufferReference = characterBufferReference;
Length = characterLength;
}
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from part of another <see cref="CharacterBufferRange"/>
/// </summary>
internal CharacterBufferRange(
CharacterBufferRange characterBufferRange,
int offsetToFirstChar,
int characterLength
) :
this(
characterBufferRange.CharacterBuffer,
characterBufferRange.OffsetToFirstChar + offsetToFirstChar,
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from string
/// </summary>
internal CharacterBufferRange(
string charString
) :
this(
charString,
0,
charString.Length
)
{ }
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from memory buffer
/// </summary>
internal CharacterBufferRange(
ReadOnlyMemory<char> charBuffer,
int offsetToFirstChar,
int characterLength
) :
this(
new CharacterBufferReference(charBuffer, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> by extracting text info from a text run
/// </summary>
internal CharacterBufferRange(TextRun textRun)
{
CharacterBufferReference = textRun.CharacterBufferReference;
Length = textRun.Length;
}
public char this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
#if DEBUG
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
#endif
return Span[index];
}
}
/// <summary>
/// Gets a reference to the character buffer
/// </summary>
public CharacterBufferReference CharacterBufferReference { get; }
/// <summary>
/// Gets the number of characters in text source character store
/// </summary>
public int Length { get; }
/// <summary>
/// Gets a span from the character buffer range
/// </summary>
public ReadOnlySpan<char> Span =>
CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length);
/// <summary>
/// Gets the character memory buffer
/// </summary>
internal ReadOnlyMemory<char> CharacterBuffer
{
get { return CharacterBufferReference.CharacterBuffer; }
}
/// <summary>
/// Gets the character offset relative to the beginning of buffer to
/// the first character of the run
/// </summary>
internal int OffsetToFirstChar
{
get { return CharacterBufferReference.OffsetToFirstChar; }
}
/// <summary>
/// Indicate whether the character buffer range is empty
/// </summary>
internal bool IsEmpty
{
get { return CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0; }
}
internal CharacterBufferRange Take(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new CharacterBufferRange(CharacterBufferReference, length);
}
internal CharacterBufferRange Skip(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
if (length == Length)
{
return new CharacterBufferRange(new CharacterBufferReference(), 0);
}
var characterBufferReference = new CharacterBufferReference(
CharacterBufferReference.CharacterBuffer,
CharacterBufferReference.OffsetToFirstChar + length);
return new CharacterBufferRange(characterBufferReference, Length - length);
}
/// <summary>
/// Compute hash code
/// </summary>
public override int GetHashCode()
{
return CharacterBufferReference.GetHashCode() ^ Length;
}
/// <summary>
/// Test equality with the input object
/// </summary>
/// <param name="obj"> The object to test </param>
public override bool Equals(object? obj)
{
if (obj is CharacterBufferRange range)
{
return Equals(range);
}
return false;
}
/// <summary>
/// Test equality with the input CharacterBufferRange
/// </summary>
/// <param name="value"> The CharacterBufferRange value to test </param>
public bool Equals(CharacterBufferRange value)
{
return CharacterBufferReference.Equals(value.CharacterBufferReference)
&& Length == value.Length;
}
/// <summary>
/// Compare two CharacterBufferRange for equality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right)
{
return left.Equals(right);
}
/// <summary>
/// Compare two CharacterBufferRange for inequality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right)
{
return !(left == right);
}
int IReadOnlyCollection<char>.Count => Length;
public IEnumerator<char> GetEnumerator()
{
return new ImmutableReadOnlyListStructEnumerator<char>(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

115
src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs

@ -0,0 +1,115 @@
using System;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Text character buffer reference
/// </summary>
public readonly struct CharacterBufferReference : IEquatable<CharacterBufferReference>
{
/// <summary>
/// Construct character buffer reference from character array
/// </summary>
/// <param name="characterArray">character array</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0)
: this(characterArray.AsMemory(), offsetToFirstChar)
{ }
/// <summary>
/// Construct character buffer reference from string
/// </summary>
/// <param name="characterString">character string</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
public CharacterBufferReference(string characterString, int offsetToFirstChar = 0)
: this(characterString.AsMemory(), offsetToFirstChar)
{ }
/// <summary>
/// Construct character buffer reference from memory buffer
/// </summary>
internal CharacterBufferReference(ReadOnlyMemory<char> characterBuffer, int offsetToFirstChar = 0)
{
if (offsetToFirstChar < 0)
{
throw new ArgumentOutOfRangeException("offsetToFirstChar", "ParameterCannotBeNegative");
}
// maximum offset is one less than CharacterBuffer.Count, except that zero is always a valid offset
// even in the case of an empty or null character buffer
var maxOffset = characterBuffer.Length == 0 ? 0 : Math.Max(0, characterBuffer.Length - 1);
if (offsetToFirstChar > maxOffset)
{
throw new ArgumentOutOfRangeException("offsetToFirstChar", $"ParameterCannotBeGreaterThan, {maxOffset}");
}
CharacterBuffer = characterBuffer;
OffsetToFirstChar = offsetToFirstChar;
}
/// <summary>
/// Gets the character memory buffer
/// </summary>
public ReadOnlyMemory<char> CharacterBuffer { get; }
/// <summary>
/// Gets the character offset relative to the beginning of buffer to
/// the first character of the run
/// </summary>
public int OffsetToFirstChar { get; }
/// <summary>
/// Compute hash code
/// </summary>
public override int GetHashCode()
{
return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode();
}
/// <summary>
/// Test equality with the input object
/// </summary>
/// <param name="obj"> The object to test. </param>
public override bool Equals(object? obj)
{
if (obj is CharacterBufferReference reference)
{
return Equals(reference);
}
return false;
}
/// <summary>
/// Test equality with the input CharacterBufferReference
/// </summary>
/// <param name="value"> The characterBufferReference value to test </param>
public bool Equals(CharacterBufferReference value)
{
return CharacterBuffer.Equals(value.CharacterBuffer);
}
/// <summary>
/// Compare two CharacterBufferReference for equality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right)
{
return left.Equals(right);
}
/// <summary>
/// Compare two CharacterBufferReference for inequality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right)
{
return !(left == right);
}
}
}

13
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@ -7,14 +7,15 @@ namespace Avalonia.Media.TextFormatting
{
internal readonly struct FormattedTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
private readonly CharacterBufferRange _text;
private readonly int length;
private readonly TextRunProperties _defaultProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textModifier;
public FormattedTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties,
public FormattedTextSource(string text, TextRunProperties defaultProperties,
IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
_text = text;
_text = new CharacterBufferRange(text);
_defaultProperties = defaultProperties;
_textModifier = textModifier;
}
@ -35,7 +36,7 @@ namespace Avalonia.Media.TextFormatting
var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier);
return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
return new TextCharacters(runText.Take(textStyleRun.Length).CharacterBufferReference, textStyleRun.Length, textStyleRun.Value);
}
/// <summary>
@ -48,7 +49,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>
/// The created text style run.
/// </returns>
private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySlice<char> text, int firstTextSourceIndex,
private static ValueSpan<TextRunProperties> CreateTextStyleRun(CharacterBufferRange text, int firstTextSourceIndex,
TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
if (textModifier == null || textModifier.Count == 0)
@ -122,7 +123,7 @@ namespace Avalonia.Media.TextFormatting
return new ValueSpan<TextRunProperties>(firstTextSourceIndex, length, currentProperties);
}
private static int CoerceLength(ReadOnlySlice<char> text, int length)
private static int CoerceLength(CharacterBufferRange text, int length)
{
var finalLength = 0;

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

@ -46,28 +46,30 @@ namespace Avalonia.Media.TextFormatting
var breakOportunities = new Queue<int>();
var currentPosition = textLine.FirstTextSourceIndex;
foreach (var textRun in lineImpl.TextRuns)
{
var text = textRun.Text;
var text = new CharacterBufferRange(textRun);
if (text.IsEmpty)
{
continue;
}
var start = text.Start;
var lineBreakEnumerator = new LineBreakEnumerator(text);
while (lineBreakEnumerator.MoveNext())
{
var currentBreak = lineBreakEnumerator.Current;
if (!currentBreak.Required && currentBreak.PositionWrap != text.Length)
if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length)
{
breakOportunities.Enqueue(start + currentBreak.PositionMeasure);
breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure);
}
}
currentPosition += textRun.Length;
}
if (breakOportunities.Count == 0)
@ -78,9 +80,11 @@ namespace Avalonia.Media.TextFormatting
var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.WidthIncludingTrailingWhitespace);
var spacing = remainingSpace / breakOportunities.Count;
currentPosition = textLine.FirstTextSourceIndex;
foreach (var textRun in lineImpl.TextRuns)
{
var text = textRun.Text;
var text = textRun.CharacterBufferReference.CharacterBuffer;
if (text.IsEmpty)
{
@ -91,7 +95,6 @@ namespace Avalonia.Media.TextFormatting
{
var glyphRun = shapedText.GlyphRun;
var shapedBuffer = shapedText.ShapedBuffer;
var currentPosition = text.Start;
while (breakOportunities.Count > 0)
{
@ -110,6 +113,8 @@ namespace Avalonia.Media.TextFormatting
glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances;
}
currentPosition += textRun.Length;
}
}
}

20
src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs

@ -7,30 +7,26 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public sealed class ShapeableTextCharacters : TextRun
{
public ShapeableTextCharacters(ReadOnlySlice<char> text, TextRunProperties properties, sbyte biDiLevel)
public ShapeableTextCharacters(CharacterBufferReference characterBufferReference, int length,
TextRunProperties properties, sbyte biDiLevel)
{
TextSourceLength = text.Length;
Text = text;
CharacterBufferReference = characterBufferReference;
Length = length;
Properties = properties;
BidiLevel = biDiLevel;
}
public override int TextSourceLength { get; }
public override int Length { get; }
public override ReadOnlySlice<char> Text { get; }
public override CharacterBufferReference CharacterBufferReference { get; }
public override TextRunProperties Properties { get; }
public sbyte BidiLevel { get; }
public bool CanShapeTogether(ShapeableTextCharacters shapeableTextCharacters)
{
if (!Text.Buffer.Equals(shapeableTextCharacters.Text.Buffer))
{
return false;
}
if (Text.Start + Text.Length != shapeableTextCharacters.Text.Start)
if (!CharacterBufferReference.Equals(shapeableTextCharacters.CharacterBufferReference))
{
return false;
}

31
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@ -7,16 +7,16 @@ namespace Avalonia.Media.TextFormatting
public sealed class ShapedBuffer : IList<GlyphInfo>
{
private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters();
public ShapedBuffer(ReadOnlySlice<char> text, int length, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
: this(text, new GlyphInfo[length], glyphTypeface, fontRenderingEmSize, bidiLevel)
public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) :
this(characterBufferRange, new GlyphInfo[bufferLength], glyphTypeface, fontRenderingEmSize, bidiLevel)
{
}
internal ShapedBuffer(ReadOnlySlice<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
{
Text = text;
CharacterBufferRange = characterBufferRange;
GlyphInfos = glyphInfos;
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
@ -24,9 +24,7 @@ namespace Avalonia.Media.TextFormatting
}
internal ArraySlice<GlyphInfo> GlyphInfos { get; }
public ReadOnlySlice<char> Text { get; }
public int Length => GlyphInfos.Length;
public IGlyphTypeface GlyphTypeface { get; }
@ -45,6 +43,8 @@ namespace Avalonia.Media.TextFormatting
public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos);
public CharacterBufferRange CharacterBufferRange { get; }
/// <summary>
/// Finds a glyph index for given character index.
/// </summary>
@ -105,16 +105,23 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The split result.</returns>
internal SplitResult<ShapedBuffer> Split(int length)
{
if (Text.Length == length)
if (CharacterBufferRange.Length == length)
{
return new SplitResult<ShapedBuffer>(this, null);
}
var glyphCount = FindGlyphIndex(Text.Start + length);
var firstCluster = GlyphClusters[0];
var lastCluster = GlyphClusters[GlyphClusters.Count - 1];
var start = firstCluster < lastCluster ? firstCluster : lastCluster;
var glyphCount = FindGlyphIndex(start + length);
var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var first = new ShapedBuffer(CharacterBufferRange.Take(length),
GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var second = new ShapedBuffer(Text.Skip(length), GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var second = new ShapedBuffer(CharacterBufferRange.Skip(length),
GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
return new SplitResult<ShapedBuffer>(first, second);
}

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

@ -1,6 +1,5 @@
using System;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -14,10 +13,10 @@ namespace Avalonia.Media.TextFormatting
public ShapedTextCharacters(ShapedBuffer shapedBuffer, TextRunProperties properties)
{
ShapedBuffer = shapedBuffer;
Text = shapedBuffer.Text;
CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference;
Length = shapedBuffer.CharacterBufferRange.Length;
Properties = properties;
TextSourceLength = Text.Length;
TextMetrics = new TextMetrics(properties.Typeface, properties.FontRenderingEmSize);
TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize);
}
public bool IsReversed { get; private set; }
@ -27,13 +26,13 @@ namespace Avalonia.Media.TextFormatting
public ShapedBuffer ShapedBuffer { get; }
/// <inheritdoc/>
public override ReadOnlySlice<char> Text { get; }
public override CharacterBufferReference CharacterBufferReference { get; }
/// <inheritdoc/>
public override TextRunProperties Properties { get; }
/// <inheritdoc/>
public override int TextSourceLength { get; }
public override int Length { get; }
public TextMetrics TextMetrics { get; }
@ -176,12 +175,12 @@ namespace Avalonia.Media.TextFormatting
#if DEBUG
if (first.Text.Length != length)
if (first.Length != length)
{
throw new InvalidOperationException("Split length mismatch.");
}
#endif
#endif
var second = new ShapedTextCharacters(splitBuffer.Second!, Properties);
@ -193,7 +192,7 @@ namespace Avalonia.Media.TextFormatting
return new GlyphRun(
ShapedBuffer.GlyphTypeface,
ShapedBuffer.FontRenderingEmSize,
Text,
new CharacterBufferRange(CharacterBufferReference, Length),
ShapedBuffer.GlyphIndices,
ShapedBuffer.GlyphAdvances,
ShapedBuffer.GlyphOffsets,

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

@ -1,6 +1,6 @@
namespace Avalonia.Media.TextFormatting
{
internal readonly struct SplitResult<T>
public readonly struct SplitResult<T>
{
public SplitResult(T first, T? second)
{

129
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -10,26 +9,83 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public class TextCharacters : TextRun
{
public TextCharacters(ReadOnlySlice<char> text, TextRunProperties properties)
{
TextSourceLength = text.Length;
Text = text;
Properties = properties;
}
/// <summary>
/// Construct a run of text content from character array
/// </summary>
public TextCharacters(
char[] characterArray,
int offsetToFirstChar,
int length,
TextRunProperties textRunProperties
) :
this(
new CharacterBufferReference(characterArray, offsetToFirstChar),
length,
textRunProperties
)
{ }
public TextCharacters(ReadOnlySlice<char> text, int offsetToFirstCharacter, int length,
TextRunProperties properties)
/// <summary>
/// Construct a run for text content from string
/// </summary>
public TextCharacters(
string characterString,
TextRunProperties textRunProperties
) :
this(
characterString,
0, // offsetToFirstChar
(characterString == null) ? 0 : characterString.Length,
textRunProperties
)
{ }
/// <summary>
/// Construct a run for text content from string
/// </summary>
public TextCharacters(
string characterString,
int offsetToFirstChar,
int length,
TextRunProperties textRunProperties
) :
this(
new CharacterBufferReference(characterString, offsetToFirstChar),
length,
textRunProperties
)
{ }
/// <summary>
/// Internal constructor of TextContent
/// </summary>
public TextCharacters(
CharacterBufferReference characterBufferReference,
int length,
TextRunProperties textRunProperties
)
{
Text = text.Skip(offsetToFirstCharacter).Take(length);
TextSourceLength = length;
Properties = properties;
if (length <= 0)
{
throw new ArgumentOutOfRangeException("length", "ParameterMustBeGreaterThanZero");
}
if (textRunProperties.FontRenderingEmSize <= 0)
{
throw new ArgumentOutOfRangeException("textRunProperties.FontRenderingEmSize", "ParameterMustBeGreaterThanZero");
}
CharacterBufferReference = characterBufferReference;
Length = length;
Properties = textRunProperties;
}
/// <inheritdoc />
public override int TextSourceLength { get; }
public override int Length { get; }
/// <inheritdoc />
public override ReadOnlySlice<char> Text { get; }
public override CharacterBufferReference CharacterBufferReference { get; }
/// <inheritdoc />
public override TextRunProperties Properties { get; }
@ -38,18 +94,17 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of <see cref="ShapeableTextCharacters"/>.
/// </summary>
/// <returns>The shapeable text characters.</returns>
internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel,
ref TextRunProperties? previousProperties)
internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{
var shapeableCharacters = new List<ShapeableTextCharacters>(2);
while (!runText.IsEmpty)
while (characterBufferRange.Length > 0)
{
var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel, ref previousProperties);
var shapeableRun = CreateShapeableRun(characterBufferRange, Properties, biDiLevel, ref previousProperties);
shapeableCharacters.Add(shapeableRun);
runText = runText.Skip(shapeableRun.Text.Length);
characterBufferRange = characterBufferRange.Skip(shapeableRun.Length);
previousProperties = shapeableRun.Properties;
}
@ -60,45 +115,45 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Creates a shapeable text run with unique properties.
/// </summary>
/// <param name="text">The text to create text runs from.</param>
/// <param name="characterBufferRange">The character buffer range to create text runs from.</param>
/// <param name="defaultProperties">The default text run properties.</param>
/// <param name="biDiLevel">The bidi level of the run.</param>
/// <param name="previousProperties"></param>
/// <returns>A list of shapeable text runs.</returns>
private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text,
private static ShapeableTextCharacters CreateShapeableRun(CharacterBufferRange characterBufferRange,
TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{
var defaultTypeface = defaultProperties.Typeface;
var currentTypeface = defaultTypeface;
var previousTypeface = previousProperties?.Typeface;
if (TryGetShapeableLength(text, currentTypeface, null, out var count, out var script))
if (TryGetShapeableLength(characterBufferRange, currentTypeface, null, out var count, out var script))
{
if (script == Script.Common && previousTypeface is not null)
{
if (TryGetShapeableLength(text, previousTypeface.Value, null, out var fallbackCount, out _))
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _))
{
return new ShapeableTextCharacters(text.Take(fallbackCount),
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, fallbackCount,
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
if (previousTypeface is not null)
{
if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _))
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _))
{
return new ShapeableTextCharacters(text.Take(count),
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count,
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
var codepoint = Codepoint.ReplacementCodepoint;
var codepointEnumerator = new CodepointEnumerator(text.Skip(count));
var codepointEnumerator = new CodepointEnumerator(characterBufferRange.Skip(count));
while (codepointEnumerator.MoveNext())
{
@ -118,10 +173,10 @@ namespace Avalonia.Media.TextFormatting
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out currentTypeface);
if (matchFound && TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count, out _))
if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _))
{
//Fallback found
return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
@ -130,7 +185,7 @@ namespace Avalonia.Media.TextFormatting
var glyphTypeface = currentTypeface.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text);
var enumerator = new GraphemeEnumerator(characterBufferRange);
while (enumerator.MoveNext())
{
@ -144,20 +199,20 @@ namespace Avalonia.Media.TextFormatting
count += grapheme.Text.Length;
}
return new ShapeableTextCharacters(text.Take(count), defaultProperties, biDiLevel);
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel);
}
/// <summary>
/// Tries to get a shapeable length that is supported by the specified typeface.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="characterBufferRange">The character buffer range to shape.</param>
/// <param name="typeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultTypeface"></param>
/// <param name="length">The shapeable length.</param>
/// <param name="script"></param>
/// <returns></returns>
protected static bool TryGetShapeableLength(
ReadOnlySlice<char> text,
internal static bool TryGetShapeableLength(
CharacterBufferRange characterBufferRange,
Typeface typeface,
Typeface? defaultTypeface,
out int length,
@ -166,7 +221,7 @@ namespace Avalonia.Media.TextFormatting
length = 0;
script = Script.Unknown;
if (text.Length == 0)
if (characterBufferRange.Length == 0)
{
return false;
}
@ -174,7 +229,7 @@ namespace Avalonia.Media.TextFormatting
var font = typeface.GlyphTypeface;
var defaultFont = defaultTypeface?.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text);
var enumerator = new GraphemeEnumerator(characterBufferRange);
while (enumerator.MoveNext())
{

102
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@ -32,86 +32,88 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextCharacters shapedRun:
{
currentWidth += shapedRun.Size.Width;
if (currentWidth > availableWidth)
{
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
currentWidth += shapedRun.Size.Width;
if (currentWidth > availableWidth)
{
if (isWordEllipsis && measuredLength < textLine.Length)
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
var currentBreakPosition = 0;
if (isWordEllipsis && measuredLength < textLine.Length)
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
var text = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
var nextBreakPosition = lineBreaker.Current.PositionMeasure;
var lineBreaker = new LineBreakEnumerator(text);
if (nextBreakPosition == 0)
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
break;
}
var nextBreakPosition = lineBreaker.Current.PositionMeasure;
if (nextBreakPosition >= measuredLength)
{
break;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition >= measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
currentBreakPosition = nextBreakPosition;
measuredLength = currentBreakPosition;
}
measuredLength = currentBreakPosition;
}
}
collapsedLength += measuredLength;
collapsedLength += measuredLength;
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.Add(shapedSymbol);
collapsedRuns.Add(shapedSymbol);
return collapsedRuns;
}
return collapsedRuns;
}
availableWidth -= currentRun.Size.Width;
availableWidth -= currentRun.Size.Width;
break;
}
break;
}
case { } drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
if (collapsedLength > 0)
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
collapsedRuns.AddRange(splitResult.First);
}
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.Add(shapedSymbol);
collapsedRuns.Add(shapedSymbol);
return collapsedRuns;
}
return collapsedRuns;
break;
}
break;
}
}
collapsedLength += currentRun.TextSourceLength;
collapsedLength += currentRun.Length;
runIndex++;
}

4
src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs

@ -7,9 +7,9 @@
{
public TextEndOfLine(int textSourceLength = DefaultTextSourceLength)
{
TextSourceLength = textSourceLength;
Length = textSourceLength;
}
public override int TextSourceLength { get; }
public override int Length { get; }
}
}

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

@ -79,14 +79,14 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = textRuns[i];
if (currentLength + currentRun.TextSourceLength < length)
if (currentLength + currentRun.Length < length)
{
currentLength += currentRun.TextSourceLength;
currentLength += currentRun.Length;
continue;
}
var firstCount = currentRun.TextSourceLength >= 1 ? i + 1 : i;
var firstCount = currentRun.Length >= 1 ? i + 1 : i;
var first = new List<DrawableTextRun>(firstCount);
@ -100,13 +100,13 @@ namespace Avalonia.Media.TextFormatting
var secondCount = textRuns.Count - firstCount;
if (currentLength + currentRun.TextSourceLength == length)
if (currentLength + currentRun.Length == length)
{
var second = secondCount > 0 ? new List<DrawableTextRun>(secondCount) : null;
if (second != null)
{
var offset = currentRun.TextSourceLength >= 1 ? 1 : 0;
var offset = currentRun.Length >= 1 ? 1 : 0;
for (var j = 0; j < secondCount; j++)
{
@ -163,15 +163,17 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in textRuns)
{
if (textRun.Text.IsEmpty)
if (textRun.CharacterBufferReference.CharacterBuffer.Length == 0)
{
var text = new char[textRun.TextSourceLength];
var characterBuffer = new CharacterBufferReference(new char[textRun.Length]);
biDiData.Append(text);
biDiData.Append(new CharacterBufferRange(characterBuffer, textRun.Length));
}
else
{
biDiData.Append(textRun.Text);
var text = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
biDiData.Append(text);
}
}
@ -207,10 +209,9 @@ namespace Avalonia.Media.TextFormatting
case ShapeableTextCharacters shapeableRun:
{
var groupedRuns = new List<ShapeableTextCharacters>(2) { shapeableRun };
var text = currentRun.Text;
var start = currentRun.Text.Start;
var length = currentRun.Text.Length;
var bufferOffset = currentRun.Text.BufferOffset;
var characterBufferReference = currentRun.CharacterBufferReference;
var length = currentRun.Length;
var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar;
while (index + 1 < processedRuns.Count)
{
@ -223,19 +224,14 @@ namespace Avalonia.Media.TextFormatting
{
groupedRuns.Add(nextRun);
length += nextRun.Text.Length;
if (start > nextRun.Text.Start)
{
start = nextRun.Text.Start;
}
length += nextRun.Length;
if (bufferOffset > nextRun.Text.BufferOffset)
if (offsetToFirstCharacter > nextRun.CharacterBufferReference.OffsetToFirstChar)
{
bufferOffset = nextRun.Text.BufferOffset;
offsetToFirstCharacter = nextRun.CharacterBufferReference.OffsetToFirstChar;
}
text = new ReadOnlySlice<char>(text.Buffer, start, length, bufferOffset);
characterBufferReference = new CharacterBufferReference(characterBufferReference.CharacterBuffer, offsetToFirstCharacter);
index++;
@ -252,7 +248,7 @@ namespace Avalonia.Media.TextFormatting
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
break;
}
@ -263,17 +259,17 @@ namespace Avalonia.Media.TextFormatting
}
private static IReadOnlyList<ShapedTextCharacters> ShapeTogether(
IReadOnlyList<ShapeableTextCharacters> textRuns, ReadOnlySlice<char> text, TextShaperOptions options)
IReadOnlyList<ShapeableTextCharacters> textRuns, CharacterBufferReference text, int length, TextShaperOptions options)
{
var shapedRuns = new List<ShapedTextCharacters>(textRuns.Count);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
var shapedBuffer = TextShaper.Current.ShapeText(text, length, options);
for (var i = 0; i < textRuns.Count; i++)
{
var currentRun = textRuns[i];
var splitResult = shapedBuffer.Split(currentRun.Text.Length);
var splitResult = shapedBuffer.Split(currentRun.Length);
shapedRuns.Add(new ShapedTextCharacters(splitResult.First, currentRun.Properties));
@ -301,7 +297,7 @@ namespace Avalonia.Media.TextFormatting
TextRunProperties? previousProperties = null;
TextCharacters? currentRun = null;
var runText = ReadOnlySlice<char>.Empty;
CharacterBufferRange runText = default;
for (var i = 0; i < textCharacters.Count; i++)
{
@ -314,12 +310,12 @@ namespace Avalonia.Media.TextFormatting
yield return new[] { drawableRun };
levelIndex += drawableRun.TextSourceLength;
levelIndex += drawableRun.Length;
continue;
}
runText = currentRun.Text;
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
for (; j < runText.Length;)
{
@ -401,7 +397,7 @@ namespace Avalonia.Media.TextFormatting
{
endOfLine = textEndOfLine;
textSourceLength += textEndOfLine.TextSourceLength;
textSourceLength += textEndOfLine.Length;
textRuns.Add(textRun);
@ -414,7 +410,7 @@ namespace Avalonia.Media.TextFormatting
{
if (TryGetLineBreak(textCharacters, out var runLineBreak))
{
var splitResult = new TextCharacters(textCharacters.Text.Take(runLineBreak.PositionWrap),
var splitResult = new TextCharacters(textCharacters.CharacterBufferReference, runLineBreak.PositionWrap,
textCharacters.Properties);
textRuns.Add(splitResult);
@ -435,7 +431,7 @@ namespace Avalonia.Media.TextFormatting
}
}
textSourceLength += textRun.TextSourceLength;
textSourceLength += textRun.Length;
}
return textRuns;
@ -445,12 +441,14 @@ namespace Avalonia.Media.TextFormatting
{
lineBreak = default;
if (textRun.Text.IsEmpty)
if (textRun.CharacterBufferReference.CharacterBuffer.IsEmpty)
{
return false;
}
var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text);
var characterBufferRange = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
var lineBreakEnumerator = new LineBreakEnumerator(characterBufferRange);
while (lineBreakEnumerator.MoveNext())
{
@ -461,7 +459,7 @@ namespace Avalonia.Media.TextFormatting
lineBreak = lineBreakEnumerator.Current;
return lineBreak.PositionWrap >= textRun.Text.Length || true;
return lineBreak.PositionWrap >= textRun.Length || true;
}
return false;
@ -480,7 +478,7 @@ namespace Avalonia.Media.TextFormatting
{
if(shapedTextCharacters.ShapedBuffer.Length > 0)
{
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster;
var lastCluster = firstCluster;
for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
@ -498,7 +496,7 @@ namespace Avalonia.Media.TextFormatting
currentWidth += glyphInfo.GlyphAdvance;
}
measuredLength += currentRun.TextSourceLength;
measuredLength += currentRun.Length;
}
break;
@ -511,7 +509,7 @@ namespace Avalonia.Media.TextFormatting
goto found;
}
measuredLength += currentRun.TextSourceLength;
measuredLength += currentRun.Length;
currentWidth += currentRun.Size.Width;
break;
@ -533,11 +531,11 @@ namespace Avalonia.Media.TextFormatting
var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface;
var text = new ReadOnlySlice<char>(s_empty, firstTextSourceIndex, 1);
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
var characterBufferRange = new CharacterBufferRange(new CharacterBufferReference(s_empty), s_empty.Length);
var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection);
var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
@ -579,7 +577,9 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = textRuns[index];
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
var lineBreaker = new LineBreakEnumerator(runText);
var breakFound = false;
@ -612,7 +612,7 @@ namespace Avalonia.Media.TextFormatting
//Find next possible wrap position (overflow)
if (index < textRuns.Count - 1)
{
if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
if (lineBreaker.Current.PositionWrap != currentRun.Length)
{
//We already found the next possible wrap position.
breakFound = true;
@ -626,7 +626,7 @@ namespace Avalonia.Media.TextFormatting
{
currentPosition += lineBreaker.Current.PositionWrap;
if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
if (lineBreaker.Current.PositionWrap != currentRun.Length)
{
break;
}
@ -640,7 +640,9 @@ namespace Avalonia.Media.TextFormatting
currentRun = textRuns[index];
lineBreaker = new LineBreakEnumerator(currentRun.Text);
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
lineBreaker = new LineBreakEnumerator(runText);
}
}
else
@ -669,7 +671,7 @@ namespace Avalonia.Media.TextFormatting
if (!breakFound)
{
currentLength += currentRun.TextSourceLength;
currentLength += currentRun.Length;
continue;
}
@ -723,12 +725,12 @@ namespace Avalonia.Media.TextFormatting
return false;
}
if (Current.TextSourceLength == 0)
if (Current.Length == 0)
{
return false;
}
_pos += Current.TextSourceLength;
_pos += Current.Length;
return true;
}
@ -754,7 +756,9 @@ namespace Avalonia.Media.TextFormatting
var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo);
var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);
var characterBuffer = textRun.CharacterBufferReference;
var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions);
return new ShapedTextCharacters(shapedBuffer, textRun.Properties);
}

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

@ -55,7 +55,7 @@ namespace Avalonia.Media.TextFormatting
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, flowDirection, lineHeight, letterSpacing);
_textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
_textSource = new FormattedTextSource(text ?? "", _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
_textTrimming = textTrimming ?? TextTrimming.None;

5
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -19,7 +18,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="width">width in which collapsing is constrained to</param>
/// <param name="textRunProperties">text run properties of ellipsis symbol</param>
public TextLeadingPrefixCharacterEllipsis(
ReadOnlySlice<char> ellipsis,
string ellipsis,
int prefixLength,
double width,
TextRunProperties textRunProperties)
@ -129,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
if (suffixCount > 0)
{
var splitSuffix =
endShapedRun.Split(run.TextSourceLength - suffixCount);
endShapedRun.Split(run.Length - suffixCount);
collapsedRuns.Add(splitSuffix.Second!);
}

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

@ -56,7 +56,7 @@ namespace Avalonia.Media.TextFormatting
public override double Height => _textLineMetrics.Height;
/// <inheritdoc/>
public override int NewLineLength => _textLineMetrics.NewLineLength;
public override int NewLineLength => _textLineMetrics.NewlineLength;
/// <inheritdoc/>
public override double OverhangAfter => 0;
@ -180,7 +180,7 @@ namespace Avalonia.Media.TextFormatting
{
var lastRun = _textRuns[_textRuns.Count - 1];
return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.TextSourceLength, lastRun.Size.Width);
return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, lastRun.Size.Width);
}
// process hit that happens within the line
@ -195,18 +195,18 @@ namespace Avalonia.Media.TextFormatting
if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
{
var rightToLeftIndex = i;
currentPosition += currentRun.TextSourceLength;
currentPosition += currentRun.Length;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
{
var nextShaped = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters;
var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextCharacters;
if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight)
{
break;
}
currentPosition += nextShaped.TextSourceLength;
currentPosition += nextShaped.Length;
rightToLeftIndex++;
}
@ -223,27 +223,26 @@ namespace Avalonia.Media.TextFormatting
if (currentDistance + currentRun.Size.Width <= distance)
{
currentDistance += currentRun.Size.Width;
currentPosition -= currentRun.TextSourceLength;
currentPosition -= currentRun.Length;
continue;
}
characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
break;
return GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
}
}
if (currentDistance + currentRun.Size.Width < distance)
characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
if (i < _textRuns.Count - 1 && currentDistance + currentRun.Size.Width < distance)
{
currentDistance += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
currentPosition += currentRun.Length;
continue;
}
characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
break;
}
@ -264,10 +263,10 @@ namespace Avalonia.Media.TextFormatting
if (shapedRun.GlyphRun.IsLeftToRight)
{
offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster);
}
characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
characterHit = new CharacterHit(offset + characterHit.FirstCharacterIndex, characterHit.TrailingLength);
break;
}
@ -279,7 +278,7 @@ namespace Avalonia.Media.TextFormatting
}
else
{
characterHit = new CharacterHit(currentPosition, run.TextSourceLength);
characterHit = new CharacterHit(currentPosition, run.Length);
}
break;
}
@ -334,14 +333,14 @@ namespace Avalonia.Media.TextFormatting
rightToLeftWidth -= currentRun.Size.Width;
if (currentPosition + currentRun.TextSourceLength >= characterIndex)
if (currentPosition + currentRun.Length >= characterIndex)
{
break;
}
currentPosition += currentRun.TextSourceLength;
currentPosition += currentRun.Length;
remainingLength -= currentRun.TextSourceLength;
remainingLength -= currentRun.Length;
i--;
}
@ -350,7 +349,7 @@ namespace Avalonia.Media.TextFormatting
}
}
if (currentPosition + currentRun.TextSourceLength >= characterIndex &&
if (currentPosition + currentRun.Length >= characterIndex &&
TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _))
{
return Math.Max(0, currentDistance + distance);
@ -358,8 +357,8 @@ namespace Avalonia.Media.TextFormatting
//No hit hit found so we add the full width
currentDistance += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
remainingLength -= currentRun.TextSourceLength;
currentPosition += currentRun.Length;
remainingLength -= currentRun.Length;
}
}
else
@ -383,8 +382,8 @@ namespace Avalonia.Media.TextFormatting
//No hit hit found so we add the full width
currentDistance -= currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
remainingLength -= currentRun.TextSourceLength;
currentPosition += currentRun.Length;
remainingLength -= currentRun.Length;
}
}
@ -412,16 +411,16 @@ namespace Avalonia.Media.TextFormatting
{
currentGlyphRun = shapedTextCharacters.GlyphRun;
if (currentPosition + remainingLength <= currentPosition + currentRun.Text.Length)
if (currentPosition + remainingLength <= currentPosition + currentRun.Length)
{
characterHit = new CharacterHit(currentRun.Text.Start + remainingLength);
characterHit = new CharacterHit(currentPosition + remainingLength);
distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit);
return true;
}
if (currentPosition + remainingLength == currentPosition + currentRun.Text.Length && isTrailingHit)
if (currentPosition + remainingLength == currentPosition + currentRun.Length && isTrailingHit)
{
if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft)
{
@ -440,7 +439,7 @@ namespace Avalonia.Media.TextFormatting
return true;
}
if (characterIndex == currentPosition + currentRun.TextSourceLength)
if (characterIndex == currentPosition + currentRun.Length)
{
distance = currentRun.Size.Width;
@ -479,17 +478,22 @@ namespace Avalonia.Media.TextFormatting
{
case ShapedTextCharacters shapedRun:
{
characterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
break;
}
default:
{
characterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
break;
}
}
return characterHit;
if (characterHit.FirstCharacterIndex + characterHit.TrailingLength == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength)
{
return characterHit;
}
return nextCharacterHit;
}
/// <inheritdoc/>
@ -542,200 +546,182 @@ namespace Avalonia.Media.TextFormatting
var characterLength = 0;
var endX = startX;
var currentShapedRun = currentRun as ShapedTextCharacters;
TextRunBounds currentRunBounds;
double combinedWidth;
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
continue;
}
if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight)
if (currentRun is ShapedTextCharacters currentShapedRun)
{
var rightToLeftIndex = index;
var rightToLeftWidth = currentShapedRun.Size.Width;
var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun)
if (currentPosition + currentRun.Length <= firstTextSourceIndex)
{
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
{
break;
}
startX += currentRun.Size.Width;
rightToLeftIndex++;
currentPosition += currentRun.Length;
rightToLeftWidth += nextShapedRun.Size.Width;
if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength)
{
break;
}
currentShapedRun = nextShapedRun;
continue;
}
startX = startX + rightToLeftWidth;
if (currentShapedRun.ShapedBuffer.IsLeftToRight)
{
var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition);
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
double startOffset;
remainingLength -= currentRunBounds.Length;
currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
endX = currentRunBounds.Rectangle.Right;
startX = currentRunBounds.Rectangle.Left;
double endOffset;
var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
for (int i = rightToLeftIndex - 1; i >= index; i--)
{
currentShapedRun = TextRuns[i] as ShapedTextCharacters;
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
if(currentShapedRun == null)
{
continue;
}
startX += startOffset;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
endX += endOffset;
rightToLeftRunBounds.Insert(0, currentRunBounds);
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
remainingLength -= currentRunBounds.Length;
startX = currentRunBounds.Rectangle.Left;
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
currentPosition += currentRunBounds.Length;
characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
currentDirection = FlowDirection.LeftToRight;
}
else
{
var rightToLeftIndex = index;
var rightToLeftWidth = currentShapedRun.Size.Width;
combinedWidth = endX - startX;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun)
{
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
{
break;
}
currentRect = new Rect(startX, 0, combinedWidth, Height);
rightToLeftIndex++;
currentDirection = FlowDirection.RightToLeft;
rightToLeftWidth += nextShapedRun.Size.Width;
if (!MathUtilities.IsZero(combinedWidth))
{
result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
}
if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength)
{
break;
}
startX = endX;
}
else
{
if (currentShapedRun != null)
{
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
currentShapedRun = nextShapedRun;
}
currentPosition += offset;
startX += rightToLeftWidth;
var startIndex = currentRun.Text.Start + offset;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
double startOffset;
double endOffset;
remainingLength -= currentRunBounds.Length;
currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
endX = currentRunBounds.Rectangle.Right;
startX = currentRunBounds.Rectangle.Left;
if (currentShapedRun.ShapedBuffer.IsLeftToRight)
{
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
}
else
for (int i = rightToLeftIndex - 1; i >= index; i--)
{
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
if (currentPosition < startIndex)
{
startOffset = endOffset;
}
else
if (TextRuns[i] is not ShapedTextCharacters)
{
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
continue;
}
}
startX += startOffset;
currentShapedRun = (ShapedTextCharacters)TextRuns[i];
endX += endOffset;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
rightToLeftRunBounds.Insert(0, currentRunBounds);
characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
remainingLength -= currentRunBounds.Length;
startX = currentRunBounds.Rectangle.Left;
currentDirection = FlowDirection.LeftToRight;
}
else
{
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
startX += currentRun.Size.Width;
currentPosition += currentRunBounds.Length;
}
currentPosition += currentRun.TextSourceLength;
combinedWidth = endX - startX;
continue;
}
currentRect = new Rect(startX, 0, combinedWidth, Height);
currentDirection = FlowDirection.RightToLeft;
if (currentPosition < firstTextSourceIndex)
if (!MathUtilities.IsZero(combinedWidth))
{
startX += currentRun.Size.Width;
result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
}
if (currentPosition + currentRun.TextSourceLength <= characterIndex)
{
endX += currentRun.Size.Width;
startX = endX;
}
}
else
{
if (currentPosition + currentRun.Length <= firstTextSourceIndex)
{
startX += currentRun.Size.Width;
characterLength = currentRun.TextSourceLength;
}
currentPosition += currentRun.Length;
continue;
}
if (endX < startX)
if (currentPosition < firstTextSourceIndex)
{
(endX, startX) = (startX, endX);
startX += currentRun.Size.Width;
}
//Lines that only contain a linebreak need to be covered here
if (characterLength == 0)
if (currentPosition + currentRun.Length <= characterIndex)
{
characterLength = NewLineLength;
endX += currentRun.Size.Width;
characterLength = currentRun.Length;
}
}
combinedWidth = endX - startX;
if (endX < startX)
{
(endX, startX) = (startX, endX);
}
currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
//Lines that only contain a linebreak need to be covered here
if (characterLength == 0)
{
characterLength = NewLineLength;
}
currentPosition += characterLength;
combinedWidth = endX - startX;
remainingLength -= characterLength;
currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
startX = endX;
currentPosition += characterLength;
if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
{
if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
{
currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
remainingLength -= characterLength;
var textBounds = result[result.Count - 1];
startX = endX;
textBounds.Rectangle = currentRect;
if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
{
if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
{
currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
var textBounds = result[result.Count - 1];
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
textBounds.Rectangle = currentRect;
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
lastRunBounds = currentRunBounds;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
}
lastRunBounds = currentRunBounds;
currentWidth += combinedWidth;
if (remainingLength <= 0 || currentPosition >= characterIndex)
@ -771,11 +757,11 @@ namespace Avalonia.Media.TextFormatting
continue;
}
if (currentPosition + currentRun.TextSourceLength < firstTextSourceIndex)
if (currentPosition + currentRun.Length < firstTextSourceIndex)
{
startX -= currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
currentPosition += currentRun.Length;
continue;
}
@ -789,7 +775,7 @@ namespace Avalonia.Media.TextFormatting
currentPosition += offset;
var startIndex = currentRun.Text.Start + offset;
var startIndex = currentPosition;
double startOffset;
double endOffset;
@ -827,7 +813,7 @@ namespace Avalonia.Media.TextFormatting
}
else
{
if (currentPosition + currentRun.TextSourceLength <= characterIndex)
if (currentPosition + currentRun.Length <= characterIndex)
{
endX -= currentRun.Size.Width;
}
@ -836,7 +822,7 @@ namespace Avalonia.Media.TextFormatting
{
startX -= currentRun.Size.Width;
characterLength = currentRun.TextSourceLength;
characterLength = currentRun.Length;
}
}
@ -905,7 +891,7 @@ namespace Avalonia.Media.TextFormatting
currentPosition += offset;
var startIndex = currentRun.Text.Start + offset;
var startIndex = currentPosition;
double startOffset;
double endOffset;
@ -1172,12 +1158,12 @@ namespace Avalonia.Media.TextFormatting
return true;
}
var characterIndex = codepointIndex - shapedRun.Text.Start;
//var characterIndex = codepointIndex - shapedRun.Text.Start;
if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
{
foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
}
//if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
//{
// foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
//}
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
foundCharacterHit :
@ -1196,7 +1182,7 @@ namespace Avalonia.Media.TextFormatting
if (textPosition == currentPosition)
{
nextCharacterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
return true;
}
@ -1205,7 +1191,7 @@ namespace Avalonia.Media.TextFormatting
}
}
currentPosition += currentRun.TextSourceLength;
currentPosition += currentRun.Length;
runIndex++;
}
@ -1271,7 +1257,7 @@ namespace Avalonia.Media.TextFormatting
}
default:
{
if (characterIndex == currentPosition + currentRun.TextSourceLength)
if (characterIndex == currentPosition + currentRun.Length)
{
previousCharacterHit = new CharacterHit(currentPosition);
@ -1282,7 +1268,7 @@ namespace Avalonia.Media.TextFormatting
}
}
currentPosition -= currentRun.TextSourceLength;
currentPosition -= currentRun.Length;
runIndex--;
}
@ -1310,18 +1296,25 @@ namespace Avalonia.Media.TextFormatting
{
case ShapedTextCharacters shapedRun:
{
var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
if (firstCluster > codepointIndex)
{
break;
}
if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
{
if (shapedRun.ShapedBuffer.IsLeftToRight)
{
if (currentRun.Text.Start >= codepointIndex)
if (firstCluster >= codepointIndex)
{
return --runIndex;
}
}
else
{
if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length)
if (codepointIndex > firstCluster + currentRun.Length)
{
return --runIndex;
}
@ -1330,15 +1323,15 @@ namespace Avalonia.Media.TextFormatting
if (direction == LogicalDirection.Forward)
{
if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End)
if (codepointIndex >= firstCluster && codepointIndex <= firstCluster + currentRun.Length)
{
return runIndex;
}
}
else
{
if (codepointIndex > currentRun.Text.Start &&
codepointIndex <= currentRun.Text.Start + currentRun.Text.Length)
if (codepointIndex > firstCluster &&
codepointIndex <= firstCluster + currentRun.Length)
{
return runIndex;
}
@ -1349,6 +1342,8 @@ namespace Avalonia.Media.TextFormatting
return runIndex;
}
textPosition += currentRun.Length;
break;
}
@ -1364,13 +1359,14 @@ namespace Avalonia.Media.TextFormatting
return runIndex;
}
textPosition += currentRun.Length;
break;
}
}
runIndex++;
previousRun = currentRun;
textPosition += currentRun.TextSourceLength;
}
return runIndex;
@ -1401,7 +1397,7 @@ namespace Avalonia.Media.TextFormatting
case ShapedTextCharacters textRun:
{
var textMetrics =
new TextMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize);
if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
{
@ -1432,7 +1428,7 @@ namespace Avalonia.Media.TextFormatting
{
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
newLineLength = textRun.GlyphRun.Metrics.NewLineLength;
}
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;

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

@ -6,13 +6,13 @@
/// </summary>
public readonly struct TextLineMetrics
{
public TextLineMetrics(bool hasOverflowed, double height, int newLineLength, double start, double textBaseline,
public TextLineMetrics(bool hasOverflowed, double height, int newlineLength, double start, double textBaseline,
int trailingWhitespaceLength, double width,
double widthIncludingTrailingWhitespace)
{
HasOverflowed = hasOverflowed;
Height = height;
NewLineLength = newLineLength;
NewlineLength = newlineLength;
Start = start;
TextBaseline = textBaseline;
TrailingWhitespaceLength = trailingWhitespaceLength;
@ -33,7 +33,7 @@
/// <summary>
/// Gets the number of newline characters at the end of a line.
/// </summary>
public int NewLineLength { get; }
public int NewlineLength { get; }
/// <summary>
/// Gets the distance from the start of a paragraph to the starting point of a line.

4
src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs

@ -5,9 +5,9 @@
/// </summary>
public readonly struct TextMetrics
{
public TextMetrics(Typeface typeface, double fontRenderingEmSize)
public TextMetrics(IGlyphTypeface glyphTypeface, double fontRenderingEmSize)
{
var fontMetrics = typeface.GlyphTypeface.Metrics;
var fontMetrics = glyphTypeface.Metrics;
var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight;

11
src/Avalonia.Base/Media/TextFormatting/TextRun.cs

@ -1,5 +1,4 @@
using System.Diagnostics;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -14,12 +13,12 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Gets the text source length.
/// </summary>
public virtual int TextSourceLength => DefaultTextSourceLength;
public virtual int Length => DefaultTextSourceLength;
/// <summary>
/// Gets the text run's text.
/// </summary>
public virtual ReadOnlySlice<char> Text => default;
public virtual CharacterBufferReference CharacterBufferReference => default;
/// <summary>
/// A set of properties shared by every characters in the run
@ -41,9 +40,11 @@ namespace Avalonia.Media.TextFormatting
{
unsafe
{
fixed (char* charsPtr = _textRun.Text.Span)
var characterBuffer = _textRun.CharacterBufferReference.CharacterBuffer;
fixed (char* charsPtr = characterBuffer.Span)
{
return new string(charsPtr, 0, _textRun.Text.Length);
return new string(charsPtr, 0, characterBuffer.Span.Length);
}
}
}

11
src/Avalonia.Base/Media/TextFormatting/TextShaper.cs

@ -1,7 +1,5 @@
using System;
using System.Globalization;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -45,9 +43,14 @@ namespace Avalonia.Media.TextFormatting
}
/// <inheritdoc cref="ITextShaperImpl.ShapeText"/>
public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options = default)
{
return _platformImpl.ShapeText(text, options);
return _platformImpl.ShapeText(text, length, options);
}
public ShapedBuffer ShapeText(string text, TextShaperOptions options = default)
{
return ShapeText(new CharacterBufferReference(text), text.Length, options);
}
}
}

3
src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@ -1,5 +1,4 @@
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -15,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="ellipsis">Text used as collapsing symbol.</param>
/// <param name="width">Width in which collapsing is constrained to.</param>
/// <param name="textRunProperties">Text run properties of ellipsis symbol.</param>
public TextTrailingCharacterEllipsis(ReadOnlySlice<char> ellipsis, double width, TextRunProperties textRunProperties)
public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties)
{
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);

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

@ -16,7 +16,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="width">width in which collapsing is constrained to.</param>
/// <param name="textRunProperties">text run properties of ellipsis symbol.</param>
public TextTrailingWordEllipsis(
ReadOnlySlice<char> ellipsis,
string ellipsis,
double width,
TextRunProperties textRunProperties
)

3
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/
using System;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
@ -63,7 +64,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Appends text to the bidi data.
/// </summary>
/// <param name="text">The text to process.</param>
public void Append(ReadOnlySlice<char> text)
public void Append(CharacterBufferRange text)
{
_classes.Add(text.Length);
_pairedBracketTypes.Add(text.Length);

9
src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
{
@ -166,11 +165,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <param name="index">The index to read at.</param>
/// <param name="count">The count of character that were read.</param>
/// <returns></returns>
public static Codepoint ReadAt(ReadOnlySpan<char> text, int index, out int count)
public static Codepoint ReadAt(IReadOnlyList<char> text, int index, out int count)
{
count = 1;
if (index >= text.Length)
if (index >= text.Count)
{
return ReplacementCodepoint;
}
@ -184,7 +183,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
{
hi = code;
if (index + 1 == text.Length)
if (index + 1 == text.Count)
{
return ReplacementCodepoint;
}

7
src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@ -1,12 +1,13 @@
using Avalonia.Utilities;
using System;
namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct CodepointEnumerator
{
private ReadOnlySlice<char> _text;
private CharacterBufferRange _text;
private int _pos;
public CodepointEnumerator(ReadOnlySlice<char> text)
public CodepointEnumerator(CharacterBufferRange text)
{
_text = text;
Current = Codepoint.ReplacementCodepoint;

8
src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs

@ -1,13 +1,13 @@
using Avalonia.Utilities;
using System;
namespace Avalonia.Media.TextFormatting.Unicode
{
/// <summary>
/// Represents the smallest unit of a writing system of any given language.
/// </summary>
public readonly struct Grapheme
public readonly ref struct Grapheme
{
public Grapheme(Codepoint firstCodepoint, ReadOnlySlice<char> text)
public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
{
FirstCodepoint = firstCodepoint;
Text = text;
@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary>
/// The text that is representing the <see cref="Grapheme"/>.
/// </summary>
public ReadOnlySlice<char> Text { get; }
public ReadOnlySpan<char> Text { get; }
}
}

12
src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs

@ -3,16 +3,16 @@
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct GraphemeEnumerator
{
private ReadOnlySlice<char> _text;
private CharacterBufferRange _text;
public GraphemeEnumerator(ReadOnlySlice<char> text)
public GraphemeEnumerator(CharacterBufferRange text)
{
_text = text;
Current = default;
@ -187,7 +187,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
var text = _text.Take(processor.CurrentCodeUnitOffset);
Current = new Grapheme(firstCodepoint, text);
Current = new Grapheme(firstCodepoint, text.Span);
_text = _text.Skip(processor.CurrentCodeUnitOffset);
@ -197,10 +197,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
[StructLayout(LayoutKind.Auto)]
private ref struct Processor
{
private readonly ReadOnlySlice<char> _buffer;
private readonly CharacterBufferRange _buffer;
private int _codeUnitLengthOfCurrentScalar;
internal Processor(ReadOnlySlice<char> buffer)
internal Processor(CharacterBufferRange buffer)
{
_buffer = buffer;
_codeUnitLengthOfCurrentScalar = 0;

15
src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

@ -2,7 +2,8 @@
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting.Unicode
{
@ -12,7 +13,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary>
public ref struct LineBreakEnumerator
{
private readonly ReadOnlySlice<char> _text;
private readonly IReadOnlyList<char> _text;
private int _position;
private int _lastPosition;
private LineBreakClass _currentClass;
@ -28,7 +29,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
private int _lb30a;
private bool _lb31;
public LineBreakEnumerator(ReadOnlySlice<char> text)
public LineBreakEnumerator(IReadOnlyList<char> text)
: this()
{
_text = text;
@ -62,7 +63,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
_lb30a = 0;
}
while (_position < _text.Length)
while (_position < _text.Count)
{
_lastPosition = _position;
var lastClass = _nextClass;
@ -92,11 +93,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
}
}
if (_position >= _text.Length)
if (_position >= _text.Count)
{
if (_lastPosition < _text.Length)
if (_lastPosition < _text.Count)
{
_lastPosition = _text.Length;
_lastPosition = _text.Count;
var required = false;

11
src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs

@ -1,21 +1,16 @@
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Media
{
public sealed class TextLeadingPrefixTrimming : TextTrimming
{
private readonly ReadOnlySlice<char> _ellipsis;
private readonly string _ellipsis;
private readonly int _prefixLength;
public TextLeadingPrefixTrimming(char ellipsis, int prefixLength) : this(new[] { ellipsis }, prefixLength)
{
}
public TextLeadingPrefixTrimming(char[] ellipsis, int prefixLength)
public TextLeadingPrefixTrimming(string ellipsis, int prefixLength)
{
_prefixLength = prefixLength;
_ellipsis = new ReadOnlySlice<char>(ellipsis);
_ellipsis = ellipsis;
}
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)

11
src/Avalonia.Base/Media/TextTrailingTrimming.cs

@ -1,21 +1,16 @@
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Media
{
public sealed class TextTrailingTrimming : TextTrimming
{
private readonly ReadOnlySlice<char> _ellipsis;
private readonly string _ellipsis;
private readonly bool _isWordBased;
public TextTrailingTrimming(char ellipsis, bool isWordBased) : this(new[] {ellipsis}, isWordBased)
{
}
public TextTrailingTrimming(char[] ellipsis, bool isWordBased)
public TextTrailingTrimming(string ellipsis, bool isWordBased)
{
_isWordBased = isWordBased;
_ellipsis = new ReadOnlySlice<char>(ellipsis);
_ellipsis = ellipsis;
}
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)

2
src/Avalonia.Base/Media/TextTrimming.cs

@ -8,7 +8,7 @@ namespace Avalonia.Media
/// </summary>
public abstract class TextTrimming
{
internal const char DefaultEllipsisChar = '\u2026';
internal const string DefaultEllipsisChar = "\u2026";
/// <summary>
/// Text is not trimmed.

5
src/Avalonia.Base/Platform/ITextShaperImpl.cs

@ -1,6 +1,5 @@
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Platform
{
@ -13,9 +12,9 @@ namespace Avalonia.Platform
/// <summary>
/// Shapes the specified region within the text and returns a shaped buffer.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="text">The text buffer.</param>
/// <param name="options">Text shaper options to customize the shaping process.</param>
/// <returns>A shaped glyph run.</returns>
ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options);
ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options);
}
}

4
src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs

@ -175,6 +175,10 @@ namespace Avalonia.Rendering.Composition.Animations
public override void Activate()
{
if (_finished)
{
return;
}
TargetObject.Compositor.AddToClock(this);
base.Activate();
}

3
src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs

@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
@ -31,7 +32,7 @@ internal class FpsCounter
{
var s = new string((char)c, 1);
var glyph = typeface.GetGlyph((uint)(s[0]));
_runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice<char>(s.AsMemory()), new ushort[] { glyph });
_runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph });
}
}

67
src/Avalonia.Base/StyledElement.cs

@ -81,6 +81,7 @@ namespace Avalonia
private Styles? _styles;
private bool _stylesApplied;
private bool _themeApplied;
private bool _templatedParentThemeApplied;
private AvaloniaObject? _templatedParent;
private bool _dataContextUpdating;
private ControlTheme? _implicitTheme;
@ -375,6 +376,12 @@ namespace Avalonia
_themeApplied = true;
}
if (!_templatedParentThemeApplied)
{
ApplyTemplatedParentControlTheme();
_templatedParentThemeApplied = true;
}
if (!_stylesApplied)
{
ApplyStyles(this);
@ -613,26 +620,38 @@ namespace Avalonia
base.OnPropertyChanged(change);
if (change.Property == ThemeProperty)
{
OnControlThemeChanged();
_themeApplied = false;
}
}
private protected virtual void OnControlThemeChanged()
{
var values = GetValueStore();
values.BeginStyling();
try { values.RemoveFrames(FrameType.Theme); }
finally { values.EndStyling(); }
try
{
values.RemoveFrames(FrameType.Theme);
}
finally
{
values.EndStyling();
_themeApplied = false;
}
}
internal virtual void OnTemplatedParentControlThemeChanged()
{
var values = GetValueStore();
values.BeginStyling();
try { values.RemoveFrames(FrameType.TemplatedParentTheme); }
finally { values.EndStyling(); }
try
{
values.RemoveFrames(FrameType.TemplatedParentTheme);
}
finally
{
values.EndStyling();
_templatedParentThemeApplied = false;
}
}
internal ControlTheme? GetEffectiveTheme()
@ -743,13 +762,13 @@ namespace Avalonia
private void ApplyControlTheme()
{
var theme = GetEffectiveTheme();
if (theme is not null)
if (GetEffectiveTheme() is { } theme)
ApplyControlTheme(theme, FrameType.Theme);
}
if (TemplatedParent is StyledElement styleableParent &&
styleableParent.GetEffectiveTheme() is { } parentTheme)
private void ApplyTemplatedParentControlTheme()
{
if ((TemplatedParent as StyledElement)?.GetEffectiveTheme() is { } parentTheme)
{
ApplyControlTheme(parentTheme, FrameType.TemplatedParentTheme);
}
@ -793,6 +812,28 @@ namespace Avalonia
ApplyStyle(child, host, type);
}
private void ReevaluateImplicitTheme()
{
// We only need to check if the theme has changed when Theme isn't set (i.e. when we
// have an implicit theme).
if (Theme is not null)
return;
// Refetch the implicit theme.
var oldImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme;
_implicitTheme = null;
GetEffectiveTheme();
var newImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme;
// If the implicit theme has changed, detach the existing theme.
if (newImplicitTheme != oldImplicitTheme)
{
OnControlThemeChanged();
_themeApplied = false;
}
}
private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e)
{
if (this.GetLogicalParent() == null && !(this is ILogicalRoot))
@ -811,6 +852,7 @@ namespace Avalonia
{
_logicalRoot = e.Root;
ReevaluateImplicitTheme();
ApplyStyling();
NotifyResourcesChanged(propagate: false);
@ -835,7 +877,6 @@ namespace Avalonia
if (_logicalRoot != null)
{
_logicalRoot = null;
_implicitTheme = null;
InvalidateStyles(recurse: false);
OnDetachedFromLogicalTree(e);
DetachedFromLogicalTree?.Invoke(this, e);

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

@ -70,6 +70,9 @@ namespace Avalonia.Styling
_animationTrigger ??= new Subject<bool>();
foreach (var animation in _animations)
animation.Apply(animatable, null, _animationTrigger);
if (_activator is null)
_animationTrigger.OnNext(true);
}
}

8
src/Avalonia.Base/Utilities/ArraySlice.cs

@ -111,14 +111,6 @@ namespace Avalonia.Utilities
}
}
/// <summary>
/// Defines an implicit conversion of a <see cref="ArraySlice{T}"/> to a <see cref="ReadOnlySlice{T}"/>
/// </summary>
public static implicit operator ReadOnlySlice<T>(ArraySlice<T> slice)
{
return new ReadOnlySlice<T>(slice._data, 0, slice.Length, slice.Start);
}
/// <summary>
/// Defines an implicit conversion of an array to a <see cref="ArraySlice{T}"/>
/// </summary>

239
src/Avalonia.Base/Utilities/ReadOnlySlice.cs

@ -1,239 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Avalonia.Utilities
{
/// <summary>
/// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region.
/// </summary>
/// <typeparam name="T">The type of elements in the slice.</typeparam>
[DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))]
public readonly struct ReadOnlySlice<T> : IReadOnlyList<T> where T : struct
{
private readonly int _bufferOffset;
/// <summary>
/// Gets an empty <see cref="ReadOnlySlice{T}"/>
/// </summary>
public static ReadOnlySlice<T> Empty => new ReadOnlySlice<T>(Array.Empty<T>());
private readonly ReadOnlyMemory<T> _buffer;
public ReadOnlySlice(ReadOnlyMemory<T> buffer) : this(buffer, 0, buffer.Length) { }
public ReadOnlySlice(ReadOnlyMemory<T> buffer, int start, int length, int bufferOffset = 0)
{
#if DEBUG
if (start.CompareTo(0) < 0)
{
throw new ArgumentOutOfRangeException(nameof (start));
}
if (length.CompareTo(buffer.Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof (length));
}
#endif
_buffer = buffer;
Start = start;
Length = length;
_bufferOffset = bufferOffset;
}
/// <summary>
/// Gets the start.
/// </summary>
/// <value>
/// The start.
/// </value>
public int Start { get; }
/// <summary>
/// Gets the end.
/// </summary>
/// <value>
/// The end.
/// </value>
public int End => Start + Length - 1;
/// <summary>
/// Gets the length.
/// </summary>
/// <value>
/// The length.
/// </value>
public int Length { get; }
/// <summary>
/// Gets a value that indicates whether this instance of <see cref="ReadOnlySlice{T}"/> is Empty.
/// </summary>
public bool IsEmpty => Length == 0;
/// <summary>
/// Get the underlying span.
/// </summary>
public ReadOnlySpan<T> Span => _buffer.Span.Slice(_bufferOffset, Length);
/// <summary>
/// Get the buffer offset.
/// </summary>
public int BufferOffset => _bufferOffset;
/// <summary>
/// Get the underlying buffer.
/// </summary>
public ReadOnlyMemory<T> Buffer => _buffer;
/// <summary>
/// Returns a value to specified element of the slice.
/// </summary>
/// <param name="index">The index of the element to return.</param>
/// <returns>The <typeparamref name="T"/>.</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
/// </exception>
public T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
#if DEBUG
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof (index));
}
#endif
return Span[index];
}
}
/// <summary>
/// Returns a sub slice of elements that start at the specified index and has the specified number of elements.
/// </summary>
/// <param name="start">The start of the sub slice.</param>
/// <param name="length">The length of the sub slice.</param>
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the specified start.</returns>
public ReadOnlySlice<T> AsSlice(int start, int length)
{
if (IsEmpty)
{
return this;
}
if (length == 0)
{
return Empty;
}
if (start < 0 || _bufferOffset + start > _buffer.Length - 1)
{
throw new ArgumentOutOfRangeException(nameof(start));
}
if (_bufferOffset + start + length > _buffer.Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(_buffer, start, length, _bufferOffset);
}
/// <summary>
/// Returns a specified number of contiguous elements from the start of the slice.
/// </summary>
/// <param name="length">The number of elements to return.</param>
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns>
public ReadOnlySlice<T> Take(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(_buffer, Start, length, _bufferOffset);
}
/// <summary>
/// Bypasses a specified number of elements in the slice and then returns the remaining elements.
/// </summary>
/// <param name="length">The number of elements to skip before returning the remaining elements.</param>
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns>
public ReadOnlySlice<T> Skip(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(_buffer, Start + length, Length - length, _bufferOffset + length);
}
/// <summary>
/// Returns an enumerator for the slice.
/// </summary>
public ImmutableReadOnlyListStructEnumerator<T> GetEnumerator()
{
return new ImmutableReadOnlyListStructEnumerator<T>(this);
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
int IReadOnlyCollection<T>.Count => Length;
T IReadOnlyList<T>.this[int index] => this[index];
public static implicit operator ReadOnlySlice<T>(T[] array)
{
return new ReadOnlySlice<T>(array);
}
public static implicit operator ReadOnlySlice<T>(ReadOnlyMemory<T> memory)
{
return new ReadOnlySlice<T>(memory);
}
public static implicit operator ReadOnlySpan<T>(ReadOnlySlice<T> slice) => slice.Span;
internal class ReadOnlySliceDebugView
{
private readonly ReadOnlySlice<T> _readOnlySlice;
public ReadOnlySliceDebugView(ReadOnlySlice<T> readOnlySlice)
{
_readOnlySlice = readOnlySlice;
}
public int Start => _readOnlySlice.Start;
public int End => _readOnlySlice.End;
public int Length => _readOnlySlice.Length;
public bool IsEmpty => _readOnlySlice.IsEmpty;
public ReadOnlySpan<T> Items => _readOnlySlice.Span;
}
}
}

90
src/Avalonia.Base/Visual.cs

@ -89,6 +89,14 @@ namespace Avalonia
public static readonly StyledProperty<RelativePoint> RenderTransformOriginProperty =
AvaloniaProperty.Register<Visual, RelativePoint>(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center);
/// <summary>
/// Defines the <see cref="FlowDirection"/> property.
/// </summary>
public static readonly AttachedProperty<FlowDirection> FlowDirectionProperty =
AvaloniaProperty.RegisterAttached<Visual, Visual, FlowDirection>(
nameof(FlowDirection),
inherits: true);
/// <summary>
/// Defines the <see cref="VisualParent"/> property.
/// </summary>
@ -263,6 +271,15 @@ namespace Avalonia
set { SetValue(RenderTransformOriginProperty, value); }
}
/// <summary>
/// Gets or sets the text flow direction.
/// </summary>
public FlowDirection FlowDirection
{
get => GetValue(FlowDirectionProperty);
set => SetValue(FlowDirectionProperty, value);
}
/// <summary>
/// Gets or sets the Z index of the control.
/// </summary>
@ -306,6 +323,36 @@ namespace Avalonia
/// </summary>
internal Visual? VisualParent => _visualParent;
/// <summary>
/// Gets a value indicating whether control bypass FlowDirecton policies.
/// </summary>
/// <remarks>
/// Related to FlowDirection system and returns false as default, so if
/// <see cref="FlowDirection"/> is RTL then control will get a mirror presentation.
/// For controls that want to avoid this behavior, override this property and return true.
/// </remarks>
protected virtual bool BypassFlowDirectionPolicies => false;
/// <summary>
/// Gets the value of the attached <see cref="FlowDirectionProperty"/> on a control.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The flow direction.</returns>
public static FlowDirection GetFlowDirection(Visual visual)
{
return visual.GetValue(FlowDirectionProperty);
}
/// <summary>
/// Sets the value of the attached <see cref="FlowDirectionProperty"/> on a control.
/// </summary>
/// <param name="visual">The control.</param>
/// <param name="value">The property value to set.</param>
public static void SetFlowDirection(Visual visual, FlowDirection value)
{
visual.SetValue(FlowDirectionProperty, value);
}
/// <summary>
/// Invalidates the visual and queues a repaint.
/// </summary>
@ -387,6 +434,22 @@ namespace Avalonia
}
}
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == FlowDirectionProperty)
{
InvalidateMirrorTransform();
foreach (var child in VisualChildren)
{
child.InvalidateMirrorTransform();
}
}
}
protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
base.LogicalChildrenCollectionChanged(sender, e);
@ -682,5 +745,32 @@ namespace Avalonia
visual.SetVisualParent(parent);
}
}
/// <summary>
/// Computes the <see cref="HasMirrorTransform"/> value according to the
/// <see cref="FlowDirection"/> and <see cref="BypassFlowDirectionPolicies"/>
/// </summary>
public virtual void InvalidateMirrorTransform()
{
var flowDirection = this.FlowDirection;
var parentFlowDirection = FlowDirection.LeftToRight;
bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies;
bool parentBypassFlowDirectionPolicies = false;
var parent = VisualParent;
if (parent != null)
{
parentFlowDirection = parent.FlowDirection;
parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies;
}
bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies;
bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies;
bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored;
HasMirrorTransform = shouldApplyMirrorTransform;
}
}
}

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

@ -64,9 +64,11 @@ namespace Avalonia.Controls
protected override Control GenerateElement(DataGridCell cell, object dataItem)
{
if(CellTemplate != null)
if (CellTemplate != null)
{
return CellTemplate.Build(dataItem);
return (CellTemplate is IRecyclingDataTemplate recyclingDataTemplate)
? recyclingDataTemplate.Build(dataItem, cell.Content as Control)
: CellTemplate.Build(dataItem);
}
if (Design.IsDesignMode)
{

5
src/Avalonia.Controls/ComboBox.cs

@ -454,10 +454,9 @@ namespace Avalonia.Controls
{
if (SelectionBoxItem is Rectangle rectangle)
{
if ((rectangle.Fill as VisualBrush)?.Visual is Control content)
if ((rectangle.Fill as VisualBrush)?.Visual is Visual content)
{
var flowDirection = (((Visual)content!).VisualParent as Control)?.FlowDirection ??
FlowDirection.LeftToRight;
var flowDirection = content.VisualParent?.FlowDirection ?? FlowDirection.LeftToRight;
rectangle.FlowDirection = flowDirection;
}
}

85
src/Avalonia.Controls/Control.cs

@ -91,13 +91,6 @@ namespace Avalonia.Controls
RoutedEvent.Register<Control, SizeChangedEventArgs>(
nameof(SizeChanged), RoutingStrategies.Direct);
/// <summary>
/// Defines the <see cref="FlowDirection"/> property.
/// </summary>
public static readonly AttachedProperty<FlowDirection> FlowDirectionProperty =
AvaloniaProperty.RegisterAttached<Control, Control, FlowDirection>(
nameof(FlowDirection),
inherits: true);
// Note the following:
// _loadedQueue :
@ -170,15 +163,6 @@ namespace Avalonia.Controls
get => GetValue(TagProperty);
set => SetValue(TagProperty, value);
}
/// <summary>
/// Gets or sets the text flow direction.
/// </summary>
public FlowDirection FlowDirection
{
get => GetValue(FlowDirectionProperty);
set => SetValue(FlowDirectionProperty, value);
}
/// <summary>
/// Occurs when the user has completed a context input gesture, such as a right-click.
@ -229,39 +213,9 @@ namespace Avalonia.Controls
public new Control? Parent => (Control?)base.Parent;
/// <summary>
/// Gets the value of the attached <see cref="FlowDirectionProperty"/> on a control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>The flow direction.</returns>
public static FlowDirection GetFlowDirection(Control control)
{
return control.GetValue(FlowDirectionProperty);
}
/// <summary>
/// Sets the value of the attached <see cref="FlowDirectionProperty"/> on a control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="value">The property value to set.</param>
public static void SetFlowDirection(Control control, FlowDirection value)
{
control.SetValue(FlowDirectionProperty, value);
}
/// <inheritdoc/>
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
/// <summary>
/// Gets a value indicating whether control bypass FlowDirecton policies.
/// </summary>
/// <remarks>
/// Related to FlowDirection system and returns false as default, so if
/// <see cref="FlowDirection"/> is RTL then control will get a mirror presentation.
/// For controls that want to avoid this behavior, override this property and return true.
/// </remarks>
protected virtual bool BypassFlowDirectionPolicies => false;
/// <inheritdoc/>
void ISetterValue.Initialize(ISetter setter)
{
@ -571,45 +525,6 @@ namespace Avalonia.Controls
RaiseEvent(sizeChangedEventArgs);
}
}
else if (change.Property == FlowDirectionProperty)
{
InvalidateMirrorTransform();
foreach (var visual in VisualChildren)
{
if (visual is Control child)
{
child.InvalidateMirrorTransform();
}
}
}
}
/// <summary>
/// Computes the <see cref="Visual.HasMirrorTransform"/> value according to the
/// <see cref="FlowDirection"/> and <see cref="BypassFlowDirectionPolicies"/>
/// </summary>
public virtual void InvalidateMirrorTransform()
{
var flowDirection = this.FlowDirection;
var parentFlowDirection = FlowDirection.LeftToRight;
bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies;
bool parentBypassFlowDirectionPolicies = false;
var parent = this.VisualParent as Control;
if (parent != null)
{
parentFlowDirection = parent.FlowDirection;
parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies;
}
bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies;
bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies;
bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored;
HasMirrorTransform = shouldApplyMirrorTransform;
}
}
}

4
src/Avalonia.Controls/Documents/LineBreak.cs

@ -4,7 +4,7 @@ using System.Text;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
namespace Avalonia.Controls.Documents
namespace Avalonia.Controls.Documents
{
/// <summary>
/// LineBreak element that forces a line breaking.
@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents
internal override void BuildTextRun(IList<TextRun> textRuns)
{
var text = Environment.NewLine.AsMemory();
var text = Environment.NewLine;
var textRunProperties = CreateTextRunProperties();

2
src/Avalonia.Controls/Documents/Run.cs

@ -52,7 +52,7 @@ namespace Avalonia.Controls.Documents
internal override void BuildTextRun(IList<TextRun> textRuns)
{
var text = (Text ?? "").AsMemory();
var text = Text ?? "";
var textRunProperties = CreateTextRunProperties();

36
src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs

@ -0,0 +1,36 @@
using System;
using System.Threading;
namespace Avalonia.Controls
{
/// <summary>
/// Deferral class for notify that a work done in RefreshRequested event is done.
/// </summary>
public class RefreshCompletionDeferral
{
private Action _deferredAction;
private int _deferCount;
public RefreshCompletionDeferral(Action deferredAction)
{
_deferredAction = deferredAction;
}
public void Complete()
{
Interlocked.Decrement(ref _deferCount);
if (_deferCount == 0)
{
_deferredAction?.Invoke();
}
}
public RefreshCompletionDeferral Get()
{
Interlocked.Increment(ref _deferCount);
return this;
}
}
}

252
src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs

@ -0,0 +1,252 @@
using System;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.PullToRefresh;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a container control that provides a <see cref="RefreshVisualizer"/> and pull-to-refresh functionality for scrollable content.
/// </summary>
public class RefreshContainer : ContentControl
{
internal const int DefaultPullDimensionSize = 100;
private bool _hasDefaultRefreshInfoProviderAdapter;
private ScrollViewerIRefreshInfoProviderAdapter? _refreshInfoProviderAdapter;
private RefreshInfoProvider? _refreshInfoProvider;
private IDisposable? _visualizerSizeSubscription;
private Grid? _visualizerPresenter;
private RefreshVisualizer? _refreshVisualizer;
private bool _hasDefaultRefreshVisualizer;
/// <summary>
/// Defines the <see cref="RefreshRequested"/> event.
/// </summary>
public static readonly RoutedEvent<RefreshRequestedEventArgs> RefreshRequestedEvent =
RoutedEvent.Register<RefreshContainer, RefreshRequestedEventArgs>(nameof(RefreshRequested), RoutingStrategies.Bubble);
internal static readonly DirectProperty<RefreshContainer, ScrollViewerIRefreshInfoProviderAdapter?> RefreshInfoProviderAdapterProperty =
AvaloniaProperty.RegisterDirect<RefreshContainer, ScrollViewerIRefreshInfoProviderAdapter?>(nameof(RefreshInfoProviderAdapter),
(s) => s.RefreshInfoProviderAdapter, (s, o) => s.RefreshInfoProviderAdapter = o);
/// <summary>
/// Defines the <see cref="Visualizer"/> event.
/// </summary>
public static readonly DirectProperty<RefreshContainer, RefreshVisualizer?> VisualizerProperty =
AvaloniaProperty.RegisterDirect<RefreshContainer, RefreshVisualizer?>(nameof(Visualizer),
s => s.Visualizer, (s, o) => s.Visualizer = o);
/// <summary>
/// Defines the <see cref="PullDirection"/> event.
/// </summary>
public static readonly StyledProperty<PullDirection> PullDirectionProperty =
AvaloniaProperty.Register<RefreshContainer, PullDirection>(nameof(PullDirection), PullDirection.TopToBottom);
internal ScrollViewerIRefreshInfoProviderAdapter? RefreshInfoProviderAdapter
{
get => _refreshInfoProviderAdapter; set
{
_hasDefaultRefreshInfoProviderAdapter = false;
SetAndRaise(RefreshInfoProviderAdapterProperty, ref _refreshInfoProviderAdapter, value);
}
}
/// <summary>
/// Gets or sets the <see cref="RefreshVisualizer"/> for this container.
/// </summary>
public RefreshVisualizer? Visualizer
{
get => _refreshVisualizer; set
{
if (_refreshVisualizer != null)
{
_visualizerSizeSubscription?.Dispose();
_refreshVisualizer.RefreshRequested -= Visualizer_RefreshRequested;
}
SetAndRaise(VisualizerProperty, ref _refreshVisualizer, value);
}
}
/// <summary>
/// Gets or sets a value that specifies the direction to pull to initiate a refresh.
/// </summary>
public PullDirection PullDirection
{
get => GetValue(PullDirectionProperty);
set => SetValue(PullDirectionProperty, value);
}
/// <summary>
/// Occurs when an update of the content has been initiated.
/// </summary>
public event EventHandler<RefreshRequestedEventArgs>? RefreshRequested
{
add => AddHandler(RefreshRequestedEvent, value);
remove => RemoveHandler(RefreshRequestedEvent, value);
}
public RefreshContainer()
{
_hasDefaultRefreshInfoProviderAdapter = true;
_refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_visualizerPresenter = e.NameScope.Find<Grid>("PART_RefreshVisualizerPresenter");
if (_refreshVisualizer == null)
{
_hasDefaultRefreshVisualizer = true;
Visualizer = new RefreshVisualizer();
}
else
{
_hasDefaultRefreshVisualizer = false;
RaisePropertyChanged(VisualizerProperty, default, _refreshVisualizer);
}
OnPullDirectionChanged();
}
private void OnVisualizerSizeChanged(Rect obj)
{
if (_hasDefaultRefreshInfoProviderAdapter)
{
_refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
}
}
private void Visualizer_RefreshRequested(object? sender, RefreshRequestedEventArgs e)
{
var ev = new RefreshRequestedEventArgs(e.GetDeferral(), RefreshRequestedEvent);
RaiseEvent(ev);
ev.DecrementCount();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == RefreshInfoProviderAdapterProperty)
{
if (_refreshVisualizer != null)
{
if (_refreshInfoProvider != null)
{
_refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider;
}
else
{
if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null)
{
_refreshInfoProvider = RefreshInfoProviderAdapter?.AdaptFromTree(this, _refreshVisualizer.Bounds.Size);
if (_refreshInfoProvider != null)
{
_refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider;
RefreshInfoProviderAdapter?.SetAnimations(_refreshVisualizer);
}
}
}
}
}
else if (change.Property == VisualizerProperty)
{
if (_visualizerPresenter != null)
{
_visualizerPresenter.Children.Clear();
if (_refreshVisualizer != null)
{
_visualizerPresenter.Children.Add(_refreshVisualizer);
}
}
if (_refreshVisualizer != null)
{
_refreshVisualizer.RefreshRequested += Visualizer_RefreshRequested;
_visualizerSizeSubscription = _refreshVisualizer.GetObservable(Control.BoundsProperty).Subscribe(OnVisualizerSizeChanged);
}
}
else if (change.Property == PullDirectionProperty)
{
OnPullDirectionChanged();
}
}
private void OnPullDirectionChanged()
{
if (_visualizerPresenter != null && _refreshVisualizer != null)
{
switch (PullDirection)
{
case PullDirection.TopToBottom:
_visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Top;
_visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch;
if (_hasDefaultRefreshVisualizer)
{
_refreshVisualizer.PullDirection = PullDirection.TopToBottom;
_refreshVisualizer.Height = DefaultPullDimensionSize;
_refreshVisualizer.Width = double.NaN;
}
break;
case PullDirection.BottomToTop:
_visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Bottom;
_visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch;
if (_hasDefaultRefreshVisualizer)
{
_refreshVisualizer.PullDirection = PullDirection.BottomToTop;
_refreshVisualizer.Height = DefaultPullDimensionSize;
_refreshVisualizer.Width = double.NaN;
}
break;
case PullDirection.LeftToRight:
_visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch;
_visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Left;
if (_hasDefaultRefreshVisualizer)
{
_refreshVisualizer.PullDirection = PullDirection.LeftToRight;
_refreshVisualizer.Width = DefaultPullDimensionSize;
_refreshVisualizer.Height = double.NaN;
}
break;
case PullDirection.RightToLeft:
_visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch;
_visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Right;
if (_hasDefaultRefreshVisualizer)
{
_refreshVisualizer.PullDirection = PullDirection.RightToLeft;
_refreshVisualizer.Width = DefaultPullDimensionSize;
_refreshVisualizer.Height = double.NaN;
}
break;
}
if (_hasDefaultRefreshInfoProviderAdapter &&
_hasDefaultRefreshVisualizer &&
_refreshVisualizer.Bounds.Height == DefaultPullDimensionSize &&
_refreshVisualizer.Bounds.Width == DefaultPullDimensionSize)
{
_refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
}
}
}
/// <summary>
/// Initiates an update of the content.
/// </summary>
public void RequestRefresh()
{
_refreshVisualizer?.RequestRefresh();
}
}
}

141
src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs

@ -0,0 +1,141 @@
using System;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Rendering.Composition;
namespace Avalonia.Controls.PullToRefresh
{
internal class RefreshInfoProvider : Interactive
{
internal const double DefaultExecutionRatio = 0.8;
private readonly PullDirection _refreshPullDirection;
private readonly Size _refreshVisualizerSize;
private readonly CompositionVisual? _visual;
private bool _isInteractingForRefresh;
private double _interactionRatio;
private bool _entered;
public DirectProperty<RefreshInfoProvider, bool> IsInteractingForRefreshProperty =
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, bool>(nameof(IsInteractingForRefresh),
s => s.IsInteractingForRefresh, (s, o) => s.IsInteractingForRefresh = o);
public DirectProperty<RefreshInfoProvider, double> ExecutionRatioProperty =
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, double>(nameof(ExecutionRatio),
s => s.ExecutionRatio);
public DirectProperty<RefreshInfoProvider, double> InteractionRatioProperty =
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, double>(nameof(InteractionRatio),
s => s.InteractionRatio, (s, o) => s.InteractionRatio = o);
/// <summary>
/// Defines the <see cref="RefreshStarted"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> RefreshStartedEvent =
RoutedEvent.Register<RefreshInfoProvider, RoutedEventArgs>(nameof(RefreshStarted), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="RefreshCompleted"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> RefreshCompletedEvent =
RoutedEvent.Register<RefreshInfoProvider, RoutedEventArgs>(nameof(RefreshCompleted), RoutingStrategies.Bubble);
public bool PeekingMode { get; internal set; }
public bool IsInteractingForRefresh
{
get => _isInteractingForRefresh;
internal set
{
var isInteractingForRefresh = value && !PeekingMode;
if (isInteractingForRefresh != _isInteractingForRefresh)
{
SetAndRaise(IsInteractingForRefreshProperty, ref _isInteractingForRefresh, isInteractingForRefresh);
}
}
}
public double InteractionRatio
{
get => _interactionRatio;
set
{
SetAndRaise(InteractionRatioProperty, ref _interactionRatio, value);
}
}
public double ExecutionRatio
{
get => DefaultExecutionRatio;
}
internal CompositionVisual? Visual => _visual;
public event EventHandler<RoutedEventArgs> RefreshStarted
{
add => AddHandler(RefreshStartedEvent, value);
remove => RemoveHandler(RefreshStartedEvent, value);
}
public event EventHandler<RoutedEventArgs> RefreshCompleted
{
add => AddHandler(RefreshCompletedEvent, value);
remove => RemoveHandler(RefreshCompletedEvent, value);
}
internal void InteractingStateEntered(object? sender, PullGestureEventArgs e)
{
if (!_entered)
{
IsInteractingForRefresh = true;
_entered = true;
}
ValuesChanged(e.Delta);
}
internal void InteractingStateExited(object? sender, PullGestureEndedEventArgs e)
{
IsInteractingForRefresh = false;
_entered = false;
ValuesChanged(default);
}
public RefreshInfoProvider(PullDirection refreshPullDirection, Size? refreshVIsualizerSize, CompositionVisual? visual)
{
_refreshPullDirection = refreshPullDirection;
_refreshVisualizerSize = refreshVIsualizerSize ?? default;
_visual = visual;
}
public void OnRefreshStarted()
{
RaiseEvent(new RoutedEventArgs(RefreshStartedEvent));
}
public void OnRefreshCompleted()
{
RaiseEvent(new RoutedEventArgs(RefreshCompletedEvent));
}
internal void ValuesChanged(Vector value)
{
switch (_refreshPullDirection)
{
case PullDirection.TopToBottom:
case PullDirection.BottomToTop:
InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.Y / _refreshVisualizerSize.Height);
break;
case PullDirection.LeftToRight:
case PullDirection.RightToLeft:
InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.X / _refreshVisualizerSize.Width);
break;
}
}
}
}

42
src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs

@ -0,0 +1,42 @@
using System;
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
/// <summary>
/// Provides event data for RefreshRequested events.
/// </summary>
public class RefreshRequestedEventArgs : RoutedEventArgs
{
private RefreshCompletionDeferral _refreshCompletionDeferral;
/// <summary>
/// Gets a deferral object for managing the work done in the RefreshRequested event handler.
/// </summary>
/// <returns>A <see cref="RefreshCompletionDeferral"/> object</returns>
public RefreshCompletionDeferral GetDeferral()
{
return _refreshCompletionDeferral.Get();
}
public RefreshRequestedEventArgs(Action deferredAction, RoutedEvent? routedEvent) : base(routedEvent)
{
_refreshCompletionDeferral = new RefreshCompletionDeferral(deferredAction);
}
public RefreshRequestedEventArgs(RefreshCompletionDeferral completionDeferral, RoutedEvent? routedEvent) : base(routedEvent)
{
_refreshCompletionDeferral = completionDeferral;
}
internal void IncrementCount()
{
_refreshCompletionDeferral?.Get();
}
internal void DecrementCount()
{
_refreshCompletionDeferral?.Complete();
}
}
}

553
src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs

@ -0,0 +1,553 @@
using System;
using System.Numerics;
using System.Reactive.Linq;
using Avalonia.Animation.Easings;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.PullToRefresh;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Animations;
namespace Avalonia.Controls
{
public class RefreshVisualizer : ContentControl
{
private const int DefaultIndicatorSize = 24;
private const float MinimumIndicatorOpacity = 0.4f;
private const float ParallaxPositionRatio = 0.5f;
private double _executingRatio = 0.8;
private RefreshVisualizerState _refreshVisualizerState;
private RefreshInfoProvider? _refreshInfoProvider;
private IDisposable? _isInteractingSubscription;
private IDisposable? _interactionRatioSubscription;
private bool _isInteractingForRefresh;
private Grid? _root;
private Control? _content;
private RefreshVisualizerOrientation _orientation;
private float _startingRotationAngle;
private double _interactionRatio;
private bool _played;
private ScalarKeyFrameAnimation? _rotateAnimation;
private bool IsPullDirectionVertical => PullDirection == PullDirection.TopToBottom || PullDirection == PullDirection.BottomToTop;
private bool IsPullDirectionFar => PullDirection == PullDirection.BottomToTop || PullDirection == PullDirection.RightToLeft;
/// <summary>
/// Defines the <see cref="PullDirection"/> property.
/// </summary>
public static readonly StyledProperty<PullDirection> PullDirectionProperty =
AvaloniaProperty.Register<RefreshVisualizer, PullDirection>(nameof(PullDirection), PullDirection.TopToBottom);
/// <summary>
/// Defines the <see cref="RefreshRequested"/> event.
/// </summary>
public static readonly RoutedEvent<RefreshRequestedEventArgs> RefreshRequestedEvent =
RoutedEvent.Register<RefreshVisualizer, RefreshRequestedEventArgs>(nameof(RefreshRequested), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="RefreshVisualizerState"/> property.
/// </summary>
public static readonly DirectProperty<RefreshVisualizer, RefreshVisualizerState> RefreshVisualizerStateProperty =
AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshVisualizerState>(nameof(RefreshVisualizerState),
s => s.RefreshVisualizerState);
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly DirectProperty<RefreshVisualizer, RefreshVisualizerOrientation> OrientationProperty =
AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshVisualizerOrientation>(nameof(Orientation),
s => s.Orientation, (s, o) => s.Orientation = o);
/// <summary>
/// Defines the <see cref="RefreshInfoProvider"/> property.
/// </summary>
internal DirectProperty<RefreshVisualizer, RefreshInfoProvider?> RefreshInfoProviderProperty =
AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshInfoProvider?>(nameof(RefreshInfoProvider),
s => s.RefreshInfoProvider, (s, o) => s.RefreshInfoProvider = o);
/// <summary>
/// Gets or sets a value that indicates the refresh state of the visualizer.
/// </summary>
protected RefreshVisualizerState RefreshVisualizerState
{
get
{
return _refreshVisualizerState;
}
private set
{
SetAndRaise(RefreshVisualizerStateProperty, ref _refreshVisualizerState, value);
UpdateContent();
}
}
/// <summary>
/// Gets or sets a value that indicates the orientation of the visualizer.
/// </summary>
public RefreshVisualizerOrientation Orientation
{
get
{
return _orientation;
}
set
{
SetAndRaise(OrientationProperty, ref _orientation, value);
}
}
internal PullDirection PullDirection
{
get => GetValue(PullDirectionProperty);
set => SetValue(PullDirectionProperty, value);
}
internal RefreshInfoProvider? RefreshInfoProvider
{
get => _refreshInfoProvider;
set
{
if (_refreshInfoProvider != null)
{
_refreshInfoProvider.RenderTransform = null;
}
SetAndRaise(RefreshInfoProviderProperty, ref _refreshInfoProvider, value);
}
}
/// <summary>
/// Occurs when an update of the content has been initiated.
/// </summary>
public event EventHandler<RefreshRequestedEventArgs>? RefreshRequested
{
add => AddHandler(RefreshRequestedEvent, value);
remove => RemoveHandler(RefreshRequestedEvent, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
this.ClipToBounds = false;
_root = e.NameScope.Find<Grid>("PART_Root");
if (_root != null)
{
_content = Content as Control;
if (_content == null)
{
_content = new PathIcon()
{
Height = DefaultIndicatorSize,
Width = DefaultIndicatorSize,
Name = "PART_Icon"
};
_content.Loaded += (s, e) =>
{
var composition = ElementComposition.GetElementVisual(_content);
var compositor = composition!.Compositor;
composition.Opacity = 0;
var smoothRotationAnimation
= compositor.CreateScalarKeyFrameAnimation();
smoothRotationAnimation.Target = "RotationAngle";
smoothRotationAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing());
smoothRotationAnimation.Duration = TimeSpan.FromMilliseconds(100);
var opacityAnimation
= compositor.CreateScalarKeyFrameAnimation();
opacityAnimation.Target = "Opacity";
opacityAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing());
opacityAnimation.Duration = TimeSpan.FromMilliseconds(100);
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing());
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150);
var scaleAnimation
= compositor.CreateVector3KeyFrameAnimation();
scaleAnimation.Target = "Scale";
scaleAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing());
scaleAnimation.Duration = TimeSpan.FromMilliseconds(100);
var animation = compositor.CreateImplicitAnimationCollection();
animation["RotationAngle"] = smoothRotationAnimation;
animation["Offset"] = offsetAnimation;
animation["Scale"] = scaleAnimation;
animation["Opacity"] = opacityAnimation;
composition.ImplicitAnimations = animation;
UpdateContent();
};
Content = _content;
}
else
{
RaisePropertyChanged(ContentProperty, null, Content, Data.BindingPriority.Style, false);
}
}
OnOrientationChanged();
UpdateContent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
UpdateContent();
}
private void UpdateContent()
{
if (_content != null && _root != null)
{
var root = _root;
var visual = _refreshInfoProvider?.Visual;
var contentVisual = ElementComposition.GetElementVisual(_content);
var visualizerVisual = ElementComposition.GetElementVisual(this);
if (visual != null && contentVisual != null && visualizerVisual != null)
{
contentVisual.CenterPoint = new Vector3((float)(_content.Bounds.Width / 2), (float)(_content.Bounds.Height / 2), 0);
switch (RefreshVisualizerState)
{
case RefreshVisualizerState.Idle:
_played = false;
if(_rotateAnimation != null)
{
_rotateAnimation.IterationBehavior = AnimationIterationBehavior.Count;
_rotateAnimation = null;
}
contentVisual.Opacity = MinimumIndicatorOpacity;
contentVisual.RotationAngle = _startingRotationAngle;
visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, 0, 0) :
new Vector3(0, visualizerVisual.Offset.Y, 0);
visual.Offset = default;
_content.InvalidateMeasure();
break;
case RefreshVisualizerState.Interacting:
_played = false;
contentVisual.Opacity = MinimumIndicatorOpacity;
contentVisual.RotationAngle = (float)(_startingRotationAngle + _interactionRatio * 2 * Math.PI);
Vector3 offset = default;
if (IsPullDirectionVertical)
{
offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
}
else
{
offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
}
visual.Offset = offset;
visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) :
new Vector3(offset.X, visualizerVisual.Offset.Y, 0);
break;
case RefreshVisualizerState.Pending:
contentVisual.Opacity = 1;
contentVisual.RotationAngle = _startingRotationAngle + (float)(2 * Math.PI);
if (IsPullDirectionVertical)
{
offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
}
else
{
offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
}
visual.Offset = offset;
visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) :
new Vector3(offset.X, visualizerVisual.Offset.Y, 0);
if (!_played)
{
_played = true;
var scaleAnimation = contentVisual.Compositor!.CreateVector3KeyFrameAnimation();
scaleAnimation.Target = "Scale";
scaleAnimation.InsertKeyFrame(0.5f, new Vector3(1.5f, 1.5f, 1));
scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1));
scaleAnimation.Duration = TimeSpan.FromSeconds(0.3);
contentVisual.StartAnimation("Scale", scaleAnimation);
}
break;
case RefreshVisualizerState.Refreshing:
_rotateAnimation = contentVisual.Compositor!.CreateScalarKeyFrameAnimation();
_rotateAnimation.Target = "RotationAngle";
_rotateAnimation.InsertKeyFrame(0, _startingRotationAngle, new LinearEasing());
_rotateAnimation.InsertKeyFrame(1, _startingRotationAngle + (float)(2 * Math.PI), new LinearEasing());
_rotateAnimation.IterationBehavior = AnimationIterationBehavior.Forever;
_rotateAnimation.StopBehavior = AnimationStopBehavior.LeaveCurrentValue;
_rotateAnimation.Duration = TimeSpan.FromSeconds(0.5);
contentVisual.StartAnimation("RotationAngle", _rotateAnimation);
contentVisual.Opacity = 1;
float translationRatio = (float)(_refreshInfoProvider != null ? (1.0f - _refreshInfoProvider.ExecutionRatio) * ParallaxPositionRatio : 1.0f)
* (IsPullDirectionFar ? -1f : 1f);
if (IsPullDirectionVertical)
{
offset = new Vector3(0, (float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
}
else
{
offset = new Vector3((float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
}
visual.Offset = offset;
contentVisual.Offset += IsPullDirectionVertical ? new Vector3(0, (float)(translationRatio * root.Bounds.Height), 0) :
new Vector3((float)(translationRatio * root.Bounds.Width), 0, 0);
visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) :
new Vector3(offset.X, visualizerVisual.Offset.Y, 0);
break;
case RefreshVisualizerState.Peeking:
contentVisual.Opacity = 1;
contentVisual.RotationAngle = _startingRotationAngle;
break;
}
}
}
}
/// <summary>
/// Initiates an update of the content.
/// </summary>
public void RequestRefresh()
{
RefreshVisualizerState = RefreshVisualizerState.Refreshing;
RefreshInfoProvider?.OnRefreshStarted();
RaiseRefreshRequested();
}
private void RefreshCompleted()
{
RefreshVisualizerState = RefreshVisualizerState.Idle;
RefreshInfoProvider?.OnRefreshCompleted();
}
private void RaiseRefreshRequested()
{
var refreshArgs = new RefreshRequestedEventArgs(RefreshCompleted, RefreshRequestedEvent);
refreshArgs.IncrementCount();
RaiseEvent(refreshArgs);
refreshArgs.DecrementCount();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == RefreshInfoProviderProperty)
{
OnRefreshInfoProviderChanged();
}
else if (change.Property == ContentProperty)
{
if (_root != null && _content != null)
{
_root.Children.Insert(0, _content);
_content.VerticalAlignment = Layout.VerticalAlignment.Center;
_content.HorizontalAlignment = Layout.HorizontalAlignment.Center;
}
UpdateContent();
}
else if (change.Property == OrientationProperty)
{
OnOrientationChanged();
UpdateContent();
}
else if (change.Property == BoundsProperty)
{
switch (PullDirection)
{
case PullDirection.TopToBottom:
RenderTransform = new TranslateTransform(0, -Bounds.Height);
break;
case PullDirection.BottomToTop:
RenderTransform = new TranslateTransform(0, Bounds.Height);
break;
case PullDirection.LeftToRight:
RenderTransform = new TranslateTransform(-Bounds.Width, 0);
break;
case PullDirection.RightToLeft:
RenderTransform = new TranslateTransform(Bounds.Width, 0);
break;
}
UpdateContent();
}
else if(change.Property == PullDirectionProperty)
{
OnOrientationChanged();
UpdateContent();
}
}
private void OnOrientationChanged()
{
switch (_orientation)
{
case RefreshVisualizerOrientation.Auto:
switch (PullDirection)
{
case PullDirection.TopToBottom:
case PullDirection.BottomToTop:
_startingRotationAngle = 0.0f;
break;
case PullDirection.LeftToRight:
_startingRotationAngle = (float)(-Math.PI / 2);
break;
case PullDirection.RightToLeft:
_startingRotationAngle = (float)(Math.PI / 2);
break;
}
break;
case RefreshVisualizerOrientation.Normal:
_startingRotationAngle = 0.0f;
break;
case RefreshVisualizerOrientation.Rotate90DegreesCounterclockwise:
_startingRotationAngle = (float)(Math.PI / 2);
break;
case RefreshVisualizerOrientation.Rotate270DegreesCounterclockwise:
_startingRotationAngle = (float)(-Math.PI / 2);
break;
}
}
private void OnRefreshInfoProviderChanged()
{
_isInteractingSubscription?.Dispose();
_isInteractingSubscription = null;
_interactionRatioSubscription?.Dispose();
_interactionRatioSubscription = null;
if (RefreshInfoProvider != null)
{
_isInteractingSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty)
.Subscribe(InteractingForRefreshObserver);
_interactionRatioSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty)
.Subscribe(InteractionRatioObserver);
var visual = RefreshInfoProvider.Visual;
_executingRatio = RefreshInfoProvider.ExecutionRatio;
}
else
{
_executingRatio = 1;
}
}
private void InteractionRatioObserver(double obj)
{
var wasAtZero = _interactionRatio == 0.0;
_interactionRatio = obj;
if (_isInteractingForRefresh)
{
if (RefreshVisualizerState == RefreshVisualizerState.Idle)
{
if (wasAtZero)
{
if (_interactionRatio > _executingRatio)
{
RefreshVisualizerState = RefreshVisualizerState.Pending;
}
else if (_interactionRatio > 0)
{
RefreshVisualizerState = RefreshVisualizerState.Interacting;
}
}
else if (_interactionRatio > 0)
{
RefreshVisualizerState = RefreshVisualizerState.Peeking;
}
}
else if (RefreshVisualizerState == RefreshVisualizerState.Interacting)
{
if (_interactionRatio <= 0)
{
RefreshVisualizerState = RefreshVisualizerState.Idle;
}
else if (_interactionRatio > _executingRatio)
{
RefreshVisualizerState = RefreshVisualizerState.Pending;
}
else
{
UpdateContent();
}
}
else if (RefreshVisualizerState == RefreshVisualizerState.Pending)
{
if (_interactionRatio <= _executingRatio)
{
RefreshVisualizerState = RefreshVisualizerState.Interacting;
}
else if (_interactionRatio <= 0)
{
RefreshVisualizerState = RefreshVisualizerState.Idle;
}
else
{
UpdateContent();
}
}
}
else
{
if (RefreshVisualizerState != RefreshVisualizerState.Refreshing)
{
if (_interactionRatio > 0)
{
RefreshVisualizerState = RefreshVisualizerState.Peeking;
}
else
{
RefreshVisualizerState = RefreshVisualizerState.Idle;
}
}
}
}
private void InteractingForRefreshObserver(bool obj)
{
_isInteractingForRefresh = obj;
if (!_isInteractingForRefresh)
{
switch (_refreshVisualizerState)
{
case RefreshVisualizerState.Pending:
RequestRefresh();
break;
case RefreshVisualizerState.Refreshing:
// We don't want to interrupt a currently executing refresh.
break;
default:
RefreshVisualizerState = RefreshVisualizerState.Idle;
break;
}
}
}
}
}

13
src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs

@ -0,0 +1,13 @@
namespace Avalonia.Controls
{
/// <summary>
/// Defines constants that specify the orientation of a RefreshVisualizer.
/// </summary>
public enum RefreshVisualizerOrientation
{
Auto,
Normal,
Rotate90DegreesCounterclockwise,
Rotate270DegreesCounterclockwise
}
}

14
src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs

@ -0,0 +1,14 @@
namespace Avalonia.Controls
{
/// <summary>
/// Defines constants that specify the state of a RefreshVisualizer
/// </summary>
public enum RefreshVisualizerState
{
Idle,
Peeking,
Interacting,
Pending,
Refreshing
}
}

274
src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs

@ -0,0 +1,274 @@
using System;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Rendering.Composition;
using Avalonia.VisualTree;
namespace Avalonia.Controls.PullToRefresh
{
internal class ScrollViewerIRefreshInfoProviderAdapter
{
private const int MaxSearchDepth = 10;
private const int InitialOffsetThreshold = 1;
private PullDirection _refreshPullDirection;
private ScrollViewer? _scrollViewer;
private RefreshInfoProvider? _refreshInfoProvider;
private PullGestureRecognizer? _pullGestureRecognizer;
private InputElement? _interactionSource;
private bool _isVisualizerInteractionSourceAttached;
public ScrollViewerIRefreshInfoProviderAdapter(PullDirection pullDirection)
{
_refreshPullDirection = pullDirection;
}
public RefreshInfoProvider? AdaptFromTree(Visual root, Size? refreshVIsualizerSize)
{
if (root is ScrollViewer scrollViewer)
{
return Adapt(scrollViewer, refreshVIsualizerSize);
}
else
{
int depth = 0;
while (depth < MaxSearchDepth)
{
var scroll = AdaptFromTreeRecursiveHelper(root, depth);
if (scroll != null)
{
return Adapt(scroll, refreshVIsualizerSize);
}
depth++;
}
}
ScrollViewer? AdaptFromTreeRecursiveHelper(Visual root, int depth)
{
if (depth == 0)
{
foreach (var child in root.VisualChildren)
{
if (child is ScrollViewer viewer)
{
return viewer;
}
}
}
else
{
foreach (var child in root.VisualChildren)
{
var viewer = AdaptFromTreeRecursiveHelper(child, depth - 1);
if (viewer != null)
{
return viewer;
}
}
}
return null;
}
return null;
}
public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size? refreshVIsualizerSize)
{
if (adaptee == null)
{
throw new ArgumentNullException(nameof(adaptee), "Adaptee cannot be null");
}
if (_scrollViewer != null)
{
CleanUpScrollViewer();
}
if (_refreshInfoProvider != null && _interactionSource != null)
{
_interactionSource.RemoveHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered);
_interactionSource.RemoveHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited);
}
_refreshInfoProvider = null;
_scrollViewer = adaptee;
if (_scrollViewer.Content == null)
{
throw new ArgumentException(nameof(adaptee), "Adaptee's content property cannot be null.");
}
var content = adaptee.Content as Visual;
if (content == null)
{
throw new ArgumentException(nameof(adaptee), "Adaptee's content property must be a Visual");
}
if (content.GetVisualParent() == null)
{
_scrollViewer.Loaded += ScrollViewer_Loaded;
}
else
{
ScrollViewer_Loaded(null, null);
if (content.Parent is not InputElement)
{
throw new ArgumentException(nameof(adaptee), "Adaptee's content's parent must be a InputElement");
}
}
_refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, ElementComposition.GetElementVisual(content));
_pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection);
if (_interactionSource != null)
{
_interactionSource.GestureRecognizers.Add(_pullGestureRecognizer);
_interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered);
_interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited);
_isVisualizerInteractionSourceAttached = true;
}
_scrollViewer.PointerPressed += ScrollViewer_PointerPressed;
_scrollViewer.PointerReleased += ScrollViewer_PointerReleased;
_scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
return _refreshInfoProvider;
}
private void ScrollViewer_ScrollChanged(object? sender, ScrollChangedEventArgs e)
{
if (_isVisualizerInteractionSourceAttached && _refreshInfoProvider != null && _refreshInfoProvider.IsInteractingForRefresh)
{
if (!IsWithinOffsetThreashold())
{
_refreshInfoProvider.IsInteractingForRefresh = false;
}
}
}
public void SetAnimations(RefreshVisualizer refreshVisualizer)
{
var visualizerComposition = ElementComposition.GetElementVisual(refreshVisualizer);
if (visualizerComposition != null)
{
var compositor = visualizerComposition.Compositor;
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150);
var animation = compositor.CreateImplicitAnimationCollection();
animation["Offset"] = offsetAnimation;
visualizerComposition.ImplicitAnimations = animation;
}
if(_scrollViewer != null && _scrollViewer.Content is Visual visual)
{
var scollContentComposition = ElementComposition.GetElementVisual(visual);
if(scollContentComposition != null)
{
var compositor = scollContentComposition.Compositor;
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150);
var animation = compositor.CreateImplicitAnimationCollection();
animation["Offset"] = offsetAnimation;
scollContentComposition.ImplicitAnimations = animation;
}
}
}
private void ScrollViewer_Loaded(object? sender, RoutedEventArgs? e)
{
var content = _scrollViewer?.Content as Visual;
if (content == null)
{
throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual");
}
if (content.Parent is not InputElement)
{
throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content parent must be an InputElement");
}
MakeInteractionSource(content.Parent as InputElement);
if (_scrollViewer != null)
{
_scrollViewer.Loaded -= ScrollViewer_Loaded;
}
}
private void MakeInteractionSource(InputElement? element)
{
_interactionSource = element;
if (_pullGestureRecognizer != null && _refreshInfoProvider != null)
{
element?.GestureRecognizers.Add(_pullGestureRecognizer);
_interactionSource?.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered);
_interactionSource?.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited);
_isVisualizerInteractionSourceAttached = true;
}
}
private void ScrollViewer_PointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (_refreshInfoProvider != null)
{
_refreshInfoProvider.IsInteractingForRefresh = false;
}
}
private void ScrollViewer_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_refreshInfoProvider != null)
{
_refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold();
}
}
private bool IsWithinOffsetThreashold()
{
if (_scrollViewer != null)
{
var offset = _scrollViewer.Offset;
switch (_refreshPullDirection)
{
case PullDirection.TopToBottom:
return offset.Y < InitialOffsetThreshold;
case PullDirection.LeftToRight:
return offset.X < InitialOffsetThreshold;
case PullDirection.RightToLeft:
return offset.X > _scrollViewer.Extent.Width - _scrollViewer.Viewport.Width - InitialOffsetThreshold;
case PullDirection.BottomToTop:
return offset.Y > _scrollViewer.Extent.Height - _scrollViewer.Viewport.Height - InitialOffsetThreshold;
}
}
return false;
}
private void CleanUpScrollViewer()
{
if (_scrollViewer != null)
{
_scrollViewer.PointerPressed -= ScrollViewer_PointerPressed;
_scrollViewer.PointerReleased -= ScrollViewer_PointerReleased;
_scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
}
}
}
}

27
src/Avalonia.Controls/TextBlock.cs

@ -630,7 +630,7 @@ namespace Avalonia.Controls
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
textSource = new SimpleTextSource(text ?? "", defaultProperties);
}
return new TextLayout(
@ -829,12 +829,12 @@ namespace Avalonia.Controls
protected readonly struct SimpleTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
private readonly CharacterBufferRange _text;
private readonly TextRunProperties _defaultProperties;
public SimpleTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties)
public SimpleTextSource(string text, TextRunProperties defaultProperties)
{
_text = text;
_text = new CharacterBufferRange(new CharacterBufferReference(text), text.Length);
_defaultProperties = defaultProperties;
}
@ -852,7 +852,7 @@ namespace Avalonia.Controls
return new TextEndOfParagraph();
}
return new TextCharacters(runText, _defaultProperties);
return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultProperties);
}
}
@ -873,21 +873,28 @@ namespace Avalonia.Controls
foreach (var textRun in _textRuns)
{
if (textRun.TextSourceLength == 0)
if (textRun.Length == 0)
{
continue;
}
if (textSourceIndex >= currentPosition + textRun.TextSourceLength)
if (textSourceIndex >= currentPosition + textRun.Length)
{
currentPosition += textRun.TextSourceLength;
currentPosition += textRun.Length;
continue;
}
if (textRun is TextCharacters)
if (textRun is TextCharacters)
{
return new TextCharacters(textRun.Text.Skip(Math.Max(0, textSourceIndex - currentPosition)), textRun.Properties!);
var characterBufferReference = textRun.CharacterBufferReference;
var skip = Math.Max(0, textSourceIndex - currentPosition);
return new TextCharacters(
new CharacterBufferReference(characterBufferReference.CharacterBuffer, characterBufferReference.OffsetToFirstChar + skip),
textRun.Length - skip,
textRun.Properties!);
}
return textRun;

4
src/Avalonia.Controls/TextBox.cs

@ -961,7 +961,9 @@ namespace Avalonia.Controls
var length = 0;
var graphemeEnumerator = new GraphemeEnumerator(input.AsMemory());
var inputRange = new CharacterBufferRange(new CharacterBufferReference(input), input.Length);
var graphemeEnumerator = new GraphemeEnumerator(inputRange);
while (graphemeEnumerator.MoveNext())
{

8
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -77,12 +77,14 @@ namespace Avalonia.Controls
foreach (var run in textLine.TextRuns)
{
if(run.Text.Length > 0)
if(run.Length > 0)
{
var characterBufferRange = new CharacterBufferRange(run.CharacterBufferReference, run.Length);
#if NET6_0
builder.Append(run.Text.Span);
builder.Append(characterBufferRange.Span);
#else
builder.Append(run.Text.Span.ToArray());
builder.Append(characterBufferRange.Span.ToArray());
#endif
}
}

6
src/Avalonia.Controls/TopLevel.cs

@ -347,12 +347,6 @@ namespace Avalonia.Controls
/// </summary>
protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this);
public override void InvalidateMirrorTransform()
{
}
protected override bool BypassFlowDirectionPolicies => true;
/// <summary>
/// Handles a paint notification from <see cref="ITopLevelImpl.Resized"/>.
/// </summary>

20
src/Avalonia.Controls/TreeView.cs

@ -179,6 +179,26 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Collapse the specified <see cref="TreeViewItem"/> all descendent <see cref="TreeViewItem"/> s.
/// </summary>
/// <param name="item">The item to collapse.</param>
public void CollapseSubTree(TreeViewItem item)
{
item.IsExpanded = false;
if (item.Presenter?.Panel != null)
{
foreach (var child in item.Presenter.Panel.Children)
{
if (child is TreeViewItem treeViewItem)
{
CollapseSubTree(treeViewItem);
}
}
}
}
/// <summary>
/// Selects all items in the <see cref="TreeView"/>.
/// </summary>

124
src/Avalonia.Controls/TreeViewItem.cs

@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Metadata;
@ -166,30 +168,94 @@ namespace Avalonia.Controls
{
if (!e.Handled)
{
switch (e.Key)
Func<TreeViewItem, bool>? handler =
e.Key switch
{
Key.Left => ApplyToItemOrRecursivelyIfCtrl(FocusAwareCollapseItem, e.KeyModifiers),
Key.Right => ApplyToItemOrRecursivelyIfCtrl(ExpandItem, e.KeyModifiers),
Key.Enter or Key.Space => ApplyToItemOrRecursivelyIfCtrl(IsExpanded ? CollapseItem : ExpandItem, e.KeyModifiers),
// do not handle CTRL with numpad keys
Key.Subtract => FocusAwareCollapseItem,
Key.Add => ExpandItem,
Key.Divide => ApplyToSubtree(CollapseItem),
Key.Multiply => ApplyToSubtree(ExpandItem),
_ => null,
};
if (handler is not null)
{
e.Handled = handler(this);
}
// NOTE: these local functions do not use the TreeView.Expand/CollapseSubtree
// function because we want to know if any items were in fact expanded to set the
// event handled status. Also the handling here avoids a potential infinite recursion/stack overflow.
static Func<TreeViewItem, bool> ApplyToSubtree(Func<TreeViewItem, bool> f)
{
// Calling toList enumerates all items before applying functions. This avoids a
// potential infinite loop if there is an infinite tree (the control catalog is
// lazily infinite). But also means a lazily loaded tree will not be expanded completely.
return t => SubTree(t)
.ToList()
.Select(treeViewItem => f(treeViewItem))
.Aggregate(false, (p, c) => p || c);
}
static Func<TreeViewItem, bool> ApplyToItemOrRecursivelyIfCtrl(Func<TreeViewItem,bool> f, KeyModifiers keyModifiers)
{
if (keyModifiers.HasAllFlags(KeyModifiers.Control))
{
return ApplyToSubtree(f);
}
return f;
}
static bool ExpandItem(TreeViewItem treeViewItem)
{
if (treeViewItem.ItemCount > 0 && !treeViewItem.IsExpanded)
{
treeViewItem.IsExpanded = true;
return true;
}
return false;
}
static bool CollapseItem(TreeViewItem treeViewItem)
{
case Key.Right:
if (Items != null && Items.Cast<object>().Any() && !IsExpanded)
if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded)
{
treeViewItem.IsExpanded = false;
return true;
}
return false;
}
static bool FocusAwareCollapseItem(TreeViewItem treeViewItem)
{
if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded)
{
if (treeViewItem.IsFocused)
{
IsExpanded = true;
e.Handled = true;
treeViewItem.IsExpanded = false;
}
break;
case Key.Left:
if (Items is not null && Items.Cast<object>().Any() && IsExpanded)
else
{
if (IsFocused)
{
IsExpanded = false;
}
else
{
FocusManager.Instance?.Focus(this, NavigationMethod.Directional);
}
e.Handled = true;
FocusManager.Instance?.Focus(treeViewItem, NavigationMethod.Directional);
}
break;
return true;
}
return false;
}
static IEnumerable<TreeViewItem> SubTree(TreeViewItem treeViewItem)
{
return new[] { treeViewItem }.Concat(treeViewItem.LogicalChildren.OfType<TreeViewItem>().SelectMany(child => SubTree(child)));
}
}
@ -198,8 +264,19 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_header is InputElement previousInputMethod)
{
previousInputMethod.DoubleTapped -= HeaderDoubleTapped;
}
_header = e.NameScope.Find<Control>("PART_Header");
_templateApplied = true;
if (_header is InputElement im)
{
im.DoubleTapped += HeaderDoubleTapped;
}
if (_deferredBringIntoViewFlag)
{
_deferredBringIntoViewFlag = false;
@ -220,6 +297,15 @@ namespace Avalonia.Controls
return logical != null ? result : @default;
}
private void HeaderDoubleTapped(object? sender, TappedEventArgs e)
{
if (ItemCount > 0)
{
IsExpanded = !IsExpanded;
e.Handled = true;
}
}
private void OnParentChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!((ILogical)this).IsAttachedToLogicalTree && e.NewValue is null)

267
src/Avalonia.Controls/Window.cs

@ -176,6 +176,8 @@ namespace Avalonia.Controls
private object? _dialogResult;
private readonly Size _maxPlatformClientSize;
private WindowStartupLocation _windowStartupLocation;
private bool _shown;
private bool _showingAsDialog;
/// <summary>
/// Initializes static members of the <see cref="Window"/> class.
@ -508,6 +510,8 @@ namespace Avalonia.Controls
Owner = null;
PlatformImpl?.Dispose();
_showingAsDialog = false;
}
private bool ShouldCancelClose(CancelEventArgs? args = null)
@ -563,29 +567,33 @@ namespace Avalonia.Controls
/// </summary>
public override void Hide()
{
if (!IsVisible)
using (FreezeVisibilityChangeHandling())
{
return;
}
if (!_shown)
{
return;
}
Renderer?.Stop();
Renderer?.Stop();
if (Owner is Window owner)
{
owner.RemoveChild(this);
}
if (Owner is Window owner)
{
owner.RemoveChild(this);
}
if (_children.Count > 0)
{
foreach (var child in _children.ToArray())
if (_children.Count > 0)
{
child.child.Hide();
foreach (var child in _children.ToArray())
{
child.child.Hide();
}
}
}
Owner = null;
PlatformImpl?.Hide();
IsVisible = false;
Owner = null;
PlatformImpl?.Hide();
IsVisible = false;
_shown = false;
}
}
/// <summary>
@ -599,81 +607,124 @@ namespace Avalonia.Controls
ShowCore(null);
}
protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!IgnoreVisibilityChanges)
{
var isVisible = e.GetNewValue<bool>();
if (_shown != isVisible)
{
if(!_shown)
{
Show();
}
else
{
if (_showingAsDialog)
{
Close(false);
}
else
{
Hide();
}
}
}
}
}
/// <summary>
/// Shows the window as a child of <paramref name="parent"/>.
/// Shows the window as a child of <paramref name="owner"/>.
/// </summary>
/// <param name="parent">Window that will be a parent of the shown window.</param>
/// <param name="owner">Window that will be the owner of the shown window.</param>
/// <exception cref="InvalidOperationException">
/// The window has already been closed.
/// </exception>
public void Show(Window parent)
public void Show(Window owner)
{
if (parent is null)
if (owner is null)
{
throw new ArgumentNullException(nameof(parent), "Showing a child window requires valid parent.");
throw new ArgumentNullException(nameof(owner), "Showing a child window requires valid parent.");
}
ShowCore(parent);
ShowCore(owner);
}
private void ShowCore(Window? parent)
private void EnsureStateBeforeShow()
{
if (PlatformImpl == null)
{
throw new InvalidOperationException("Cannot re-show a closed window.");
}
}
private void EnsureParentStateBeforeShow(Window owner)
{
if (owner.PlatformImpl == null)
{
throw new InvalidOperationException("Cannot show a window with a closed owner.");
}
if (parent != null)
if (owner == this)
{
if (parent.PlatformImpl == null)
{
throw new InvalidOperationException("Cannot show a window with a closed parent.");
}
else if (parent == this)
{
throw new InvalidOperationException("A Window cannot be its own parent.");
}
else if (!parent.IsVisible)
{
throw new InvalidOperationException("Cannot show window with non-visible parent.");
}
throw new InvalidOperationException("A Window cannot be its own owner.");
}
if (IsVisible)
if (!owner.IsVisible)
{
return;
throw new InvalidOperationException("Cannot show window with non-visible owner.");
}
}
RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
private void ShowCore(Window? owner)
{
using (FreezeVisibilityChangeHandling())
{
EnsureStateBeforeShow();
EnsureInitialized();
ApplyStyling();
IsVisible = true;
if (owner != null)
{
EnsureParentStateBeforeShow(owner);
}
var initialSize = new Size(
double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width,
double.IsNaN(Height) ? Math.Max(MinHeight, ClientSize.Height) : Height);
if (_shown)
{
return;
}
if (initialSize != ClientSize)
{
PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout);
}
RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
LayoutManager.ExecuteInitialLayoutPass();
EnsureInitialized();
ApplyStyling();
_shown = true;
IsVisible = true;
if (PlatformImpl != null && parent?.PlatformImpl is not null)
{
PlatformImpl.SetParent(parent.PlatformImpl);
}
var initialSize = new Size(
double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width,
double.IsNaN(Height) ? Math.Max(MinHeight, ClientSize.Height) : Height);
Owner = parent;
parent?.AddChild(this, false);
if (initialSize != ClientSize)
{
PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout);
}
SetWindowStartupLocation(parent?.PlatformImpl);
LayoutManager.ExecuteInitialLayoutPass();
PlatformImpl?.Show(ShowActivated, false);
Renderer?.Start();
OnOpened(EventArgs.Empty);
if (PlatformImpl != null && owner?.PlatformImpl is not null)
{
PlatformImpl.SetParent(owner.PlatformImpl);
}
Owner = owner;
owner?.AddChild(this, false);
SetWindowStartupLocation(owner?.PlatformImpl);
PlatformImpl?.Show(ShowActivated, false);
Renderer?.Start();
OnOpened(EventArgs.Empty);
}
}
/// <summary>
@ -703,68 +754,66 @@ namespace Avalonia.Controls
/// </returns>
public Task<TResult> ShowDialog<TResult>(Window owner)
{
if (owner == null)
{
throw new ArgumentNullException(nameof(owner));
}
else if (owner.PlatformImpl == null)
{
throw new InvalidOperationException("Cannot show a window with a closed owner.");
}
else if (owner == this)
using (FreezeVisibilityChangeHandling())
{
throw new InvalidOperationException("A Window cannot be its own owner.");
}
else if (IsVisible)
{
throw new InvalidOperationException("The window is already being shown.");
}
else if (!owner.IsVisible)
{
throw new InvalidOperationException("Cannot show window with non-visible parent.");
}
EnsureStateBeforeShow();
RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
if (owner == null)
{
throw new ArgumentNullException(nameof(owner));
}
EnsureInitialized();
ApplyStyling();
IsVisible = true;
EnsureParentStateBeforeShow(owner);
var initialSize = new Size(
double.IsNaN(Width) ? ClientSize.Width : Width,
double.IsNaN(Height) ? ClientSize.Height : Height);
if (_shown)
{
throw new InvalidOperationException("The window is already being shown.");
}
if (initialSize != ClientSize)
{
PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout);
}
RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
LayoutManager.ExecuteInitialLayoutPass();
EnsureInitialized();
ApplyStyling();
_shown = true;
_showingAsDialog = true;
IsVisible = true;
var result = new TaskCompletionSource<TResult>();
var initialSize = new Size(
double.IsNaN(Width) ? ClientSize.Width : Width,
double.IsNaN(Height) ? ClientSize.Height : Height);
PlatformImpl?.SetParent(owner.PlatformImpl);
Owner = owner;
owner.AddChild(this, true);
if (initialSize != ClientSize)
{
PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout);
}
SetWindowStartupLocation(owner.PlatformImpl);
LayoutManager.ExecuteInitialLayoutPass();
PlatformImpl?.Show(ShowActivated, true);
var result = new TaskCompletionSource<TResult>();
Renderer?.Start();
PlatformImpl?.SetParent(owner.PlatformImpl!);
Owner = owner;
owner.AddChild(this, true);
Observable.FromEventPattern<EventHandler, EventArgs>(
x => Closed += x,
x => Closed -= x)
.Take(1)
.Subscribe(_ =>
{
owner.Activate();
result.SetResult((TResult)(_dialogResult ?? default(TResult)!));
});
SetWindowStartupLocation(owner.PlatformImpl);
OnOpened(EventArgs.Empty);
return result.Task;
PlatformImpl?.Show(ShowActivated, true);
Renderer?.Start();
Observable.FromEventPattern<EventHandler, EventArgs>(
x => Closed += x,
x => Closed -= x)
.Take(1)
.Subscribe(_ =>
{
owner.Activate();
result.SetResult((TResult)(_dialogResult ?? default(TResult)!));
});
OnOpened(EventArgs.Empty);
return result.Task;
}
}
private void UpdateEnabled()

58
src/Avalonia.Controls/WindowBase.cs

@ -42,9 +42,11 @@ namespace Avalonia.Controls
private bool _hasExecutedInitialLayoutPass;
private bool _isActive;
private bool _ignoreVisibilityChange;
private int _ignoreVisibilityChanges;
private WindowBase? _owner;
protected bool IgnoreVisibilityChanges => _ignoreVisibilityChanges > 0;
static WindowBase()
{
IsVisibleProperty.OverrideDefaultValue<WindowBase>(false);
@ -66,6 +68,11 @@ namespace Avalonia.Controls
impl.PositionChanged = HandlePositionChanged;
}
protected IDisposable FreezeVisibilityChangeHandling()
{
return new IgnoreVisibilityChangesDisposable(this);
}
/// <summary>
/// Fired when the window is activated.
/// </summary>
@ -125,18 +132,12 @@ namespace Avalonia.Controls
/// </summary>
public virtual void Hide()
{
_ignoreVisibilityChange = true;
try
using (FreezeVisibilityChangeHandling())
{
Renderer?.Stop();
PlatformImpl?.Hide();
IsVisible = false;
}
finally
{
_ignoreVisibilityChange = false;
}
}
/// <summary>
@ -144,9 +145,7 @@ namespace Avalonia.Controls
/// </summary>
public virtual void Show()
{
_ignoreVisibilityChange = true;
try
using (FreezeVisibilityChangeHandling())
{
EnsureInitialized();
ApplyStyling();
@ -157,14 +156,11 @@ namespace Avalonia.Controls
LayoutManager.ExecuteInitialLayoutPass();
_hasExecutedInitialLayoutPass = true;
}
PlatformImpl?.Show(true, false);
Renderer?.Start();
OnOpened(EventArgs.Empty);
}
finally
{
_ignoreVisibilityChange = false;
}
}
/// <summary>
@ -202,23 +198,17 @@ namespace Avalonia.Controls
protected override void HandleClosed()
{
_ignoreVisibilityChange = true;
try
using (FreezeVisibilityChangeHandling())
{
IsVisible = false;
if (this is IFocusScope scope)
{
FocusManager.Instance?.RemoveFocusScope(scope);
}
base.HandleClosed();
}
finally
{
_ignoreVisibilityChange = false;
}
}
/// <summary>
@ -318,9 +308,9 @@ namespace Avalonia.Controls
Deactivated?.Invoke(this, EventArgs.Empty);
}
private void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e)
protected virtual void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_ignoreVisibilityChange)
if (_ignoreVisibilityChanges == 0)
{
if ((bool)e.NewValue!)
{
@ -332,5 +322,21 @@ namespace Avalonia.Controls
}
}
}
private readonly struct IgnoreVisibilityChangesDisposable : IDisposable
{
private readonly WindowBase _windowBase;
public IgnoreVisibilityChangesDisposable(WindowBase windowBase)
{
_windowBase = windowBase;
_windowBase._ignoreVisibilityChanges++;
}
public void Dispose()
{
_windowBase._ignoreVisibilityChanges--;
}
}
}
}

6
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -145,13 +145,15 @@ namespace Avalonia.Headless
class HeadlessTextShaperStub : ITextShaperImpl
{
public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options)
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
var characterBufferRange = new CharacterBufferRange(text, length);
return new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel);
}
}

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

@ -636,4 +636,8 @@
<!-- BaseResources for ScrollViewer.xaml -->
<SolidColorBrush x:Key="ScrollViewerScrollBarsSeparatorBackground" Color="{StaticResource SystemChromeMediumColor}" Opacity="0.9" />
<!-- BaseResources for RefreshVisualizer.xaml -->
<SolidColorBrush x:Key="RefreshVisualizerForeground" Color="White"/>
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent"/>
</ResourceDictionary>

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

@ -631,4 +631,8 @@
<!-- Resources for ScrollViewer.xaml -->
<SolidColorBrush x:Key="ScrollViewerScrollBarsSeparatorBackground" Color="{StaticResource SystemChromeMediumColor}" Opacity="0.9" />
</ResourceDictionary>
<!-- BaseResources for RefreshVisualizer.xaml -->
<SolidColorBrush x:Key="RefreshVisualizerForeground" Color="Black"/>
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent"/>
</ResourceDictionary>

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

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

24
src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml

@ -0,0 +1,24 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTheme x:Key="{x:Type RefreshContainer}"
TargetType="RefreshContainer">
<Setter Property="Template">
<ControlTemplate>
<Grid>
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}">
</ContentPresenter>
<Grid Name="PART_RefreshVisualizerPresenter"/>
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

29
src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml

@ -0,0 +1,29 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTheme x:Key="{x:Type RefreshVisualizer}"
TargetType="RefreshVisualizer">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="IsHitTestVisible" Value="False"/>
<Setter Property="Height"
Value="100"/>
<Setter Property="Background"
Value="{DynamicResource RefreshVisualizerBackground}"/>
<Setter Property="Foreground"
Value="{DynamicResource RefreshVisualizerForeground}"/>
<Setter Property="Template">
<ControlTemplate>
<Grid Name="PART_Root"
MinHeight="80"
Background="{TemplateBinding Background}">
<Grid.Styles>
<Style Selector="PathIcon#PART_Icon">
<Setter Property="Data"
Value="M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z">
</Setter>
</Style>
</Grid.Styles>
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

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

@ -75,6 +75,7 @@
MinHeight="{TemplateBinding MinHeight}"
TemplatedControl.IsTemplateFocusTarget="True">
<Grid Name="PART_Header"
Background="Transparent"
ColumnDefinitions="Auto, *"
Margin="{TemplateBinding Level, Mode=OneWay, Converter={StaticResource TreeViewItemLeftMarginConverter}}">
<Panel Name="PART_ExpandCollapseChevronContainer"

3
src/Avalonia.Themes.Simple/Accents/BaseDark.xaml

@ -31,5 +31,8 @@
<SolidColorBrush x:Key="ThemeControlHighlightHighBrush" Color="{StaticResource ThemeControlHighlightHighColor}" />
<SolidColorBrush x:Key="ThemeForegroundBrush" Color="{StaticResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="HighlightBrush" Color="{StaticResource HighlightColor}" />
<SolidColorBrush x:Key="RefreshVisualizerForeground" Color="White"/>
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent"/>
</ResourceDictionary>

4
src/Avalonia.Themes.Simple/Accents/BaseLight.xaml

@ -31,5 +31,7 @@
<SolidColorBrush x:Key="ThemeControlHighlightHighBrush" Color="{StaticResource ThemeControlHighlightHighColor}" />
<SolidColorBrush x:Key="ThemeForegroundBrush" Color="{StaticResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="HighlightBrush" Color="{StaticResource HighlightColor}" />
<SolidColorBrush x:Key="HighlightBrush" Color="{StaticResource HighlightColor}" />
<SolidColorBrush x:Key="RefreshVisualizerForeground" Color="Black"/>
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent"/>
</ResourceDictionary>

24
src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml

@ -0,0 +1,24 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTheme x:Key="{x:Type RefreshContainer}"
TargetType="RefreshContainer">
<Setter Property="Template">
<ControlTemplate>
<Grid>
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}">
</ContentPresenter>
<Grid Name="PART_RefreshVisualizerPresenter"/>
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

31
src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml

@ -0,0 +1,31 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTheme x:Key="{x:Type RefreshVisualizer}"
TargetType="RefreshVisualizer">
<Setter Property="IsTabStop"
Value="False"/>
<Setter Property="IsHitTestVisible"
Value="False"/>
<Setter Property="Height"
Value="100"/>
<Setter Property="Background"
Value="{DynamicResource RefreshVisualizerBackground}"/>
<Setter Property="Foreground"
Value="{DynamicResource RefreshVisualizerForeground}"/>
<Setter Property="Template">
<ControlTemplate>
<Grid Name="PART_Root"
MinHeight="80"
Background="{TemplateBinding Background}">
<Grid.Styles>
<Style Selector="PathIcon#PART_Icon">
<Setter Property="Data"
Value="M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z">
</Setter>
</Style>
</Grid.Styles>
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

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

@ -65,6 +65,8 @@
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/SplitButton.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/RefreshContainer.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>

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

Loading…
Cancel
Save