Browse Source

Merge branch 'master' into patch-1

pull/11422/head
Dan Walmsley 3 years ago
committed by GitHub
parent
commit
51d4aa3799
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .ncrunch/AppWithoutLifetime.v3.ncrunchproject
  2. 5
      .ncrunch/Avalonia.Headless.NUnit.netstandard2.0.v3.ncrunchproject
  3. 5
      .ncrunch/Avalonia.Headless.XUnit.netstandard2.0.v3.ncrunchproject
  4. 11
      Avalonia.Desktop.slnf
  5. 2
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  6. 29
      samples/ControlCatalog/Pages/TabControlPage.xaml
  7. 3
      samples/IntegrationTestApp/MainWindow.axaml
  8. 112
      src/Android/Avalonia.Android/Platform/Input/AndroidKeyboardDevice.cs
  9. 2
      src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
  10. 2
      src/Avalonia.Base/AvaloniaProperty.cs
  11. 2
      src/Avalonia.Base/AvaloniaProperty`1.cs
  12. 2
      src/Avalonia.Base/Collections/AvaloniaListConverter.cs
  13. 4
      src/Avalonia.Base/Data/BindingValue.cs
  14. 2
      src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
  15. 2
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  16. 0
      src/Avalonia.Base/Data/TemplateBinding.cs
  17. 8
      src/Avalonia.Base/Diagnostics/TrimmingMessages.cs
  18. 3
      src/Avalonia.Base/Input/IAccessKeyHandler.cs
  19. 5
      src/Avalonia.Base/Input/IInputRoot.cs
  20. 5
      src/Avalonia.Base/Input/IMainMenu.cs
  21. 14
      src/Avalonia.Base/Input/KeyboardNavigationHandler.cs
  22. 11
      src/Avalonia.Base/Input/PointerEventArgs.cs
  23. 1
      src/Avalonia.Base/Media/CharacterHit.cs
  24. 74
      src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs
  25. 10
      src/Avalonia.Base/Media/TextFormatting/IndexedTextRun.cs
  26. 2
      src/Avalonia.Base/Media/TextFormatting/TextBounds.cs
  27. 18
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  28. 594
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  29. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  30. 2
      src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs
  31. 5
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  32. 2
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
  33. 4
      src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs
  34. 5
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs
  35. 2
      src/Avalonia.Base/StyledProperty.cs
  36. 4
      src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
  37. 2
      src/Avalonia.Base/Styling/Setter.cs
  38. 10
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  39. 2
      src/Avalonia.Base/Visual.cs
  40. 6
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
  41. 22
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  42. 2
      src/Avalonia.Controls/Avalonia.Controls.csproj
  43. 90
      src/Avalonia.Controls/Border.cs
  44. 4
      src/Avalonia.Controls/Button.cs
  45. 2
      src/Avalonia.Controls/Calendar/CalendarButton.cs
  46. 2
      src/Avalonia.Controls/Calendar/CalendarDayButton.cs
  47. 4
      src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs
  48. 20
      src/Avalonia.Controls/ComboBox.cs
  49. 21
      src/Avalonia.Controls/ContentControl.cs
  50. 6
      src/Avalonia.Controls/Control.cs
  51. 3
      src/Avalonia.Controls/GridSplitter.cs
  52. 3
      src/Avalonia.Controls/IContentControl.cs
  53. 2
      src/Avalonia.Controls/IHeadered.cs
  54. 3
      src/Avalonia.Controls/IMenu.cs
  55. 3
      src/Avalonia.Controls/IMenuElement.cs
  56. 3
      src/Avalonia.Controls/IMenuItem.cs
  57. 2
      src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs
  58. 24
      src/Avalonia.Controls/ItemsControl.cs
  59. 70
      src/Avalonia.Controls/ListBox.cs
  60. 67
      src/Avalonia.Controls/ListBoxItem.cs
  61. 2
      src/Avalonia.Controls/Menu.cs
  62. 2
      src/Avalonia.Controls/MenuItemAccessKeyHandler.cs
  63. 2
      src/Avalonia.Controls/NativeMenuItemSeparator.cs
  64. 2
      src/Avalonia.Controls/Panel.cs
  65. 459
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  66. 58
      src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs
  67. 4
      src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs
  68. 13
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  69. 23
      src/Avalonia.Controls/Presenters/IContentPresenter.cs
  70. 9
      src/Avalonia.Controls/Presenters/IContentPresenterHost.cs
  71. 20
      src/Avalonia.Controls/Presenters/IPresenter.cs
  72. 2
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  73. 2
      src/Avalonia.Controls/Primitives/AccessText.cs
  74. 4
      src/Avalonia.Controls/Primitives/HeaderedContentControl.cs
  75. 8
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  76. 8
      src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs
  77. 2
      src/Avalonia.Controls/Primitives/IPopupHost.cs
  78. 1
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  79. 37
      src/Avalonia.Controls/Primitives/RangeBase.cs
  80. 47
      src/Avalonia.Controls/Primitives/RangeBaseValueChangedEventArgs.cs
  81. 71
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  82. 2
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
  83. 2
      src/Avalonia.Controls/ScrollViewer.cs
  84. 32
      src/Avalonia.Controls/Shapes/Shape.cs
  85. 18
      src/Avalonia.Controls/Slider.cs
  86. 6
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  87. 20
      src/Avalonia.Controls/SplitView/SplitView.cs
  88. 72
      src/Avalonia.Controls/TabControl.cs
  89. 42
      src/Avalonia.Controls/TabItem.cs
  90. 9
      src/Avalonia.Controls/TextBlock.cs
  91. 93
      src/Avalonia.Controls/TextBox.cs
  92. 11
      src/Avalonia.Controls/TextChangedEventArgs.cs
  93. 11
      src/Avalonia.Controls/TextChangingEventArgs.cs
  94. 8
      src/Avalonia.Controls/ToggleSwitch.cs
  95. 25
      src/Avalonia.Controls/TopLevel.cs
  96. 2
      src/Avalonia.Controls/TransitioningContentControl.cs
  97. 120
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  98. 4
      src/Avalonia.Controls/Window.cs
  99. 12
      src/Avalonia.Controls/WindowBase.cs
  100. 2
      src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs

5
.ncrunch/AppWithoutLifetime.v3.ncrunchproject

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

5
.ncrunch/Avalonia.Headless.NUnit.netstandard2.0.v3.ncrunchproject

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

5
.ncrunch/Avalonia.Headless.XUnit.netstandard2.0.v3.ncrunchproject

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

11
Avalonia.Desktop.slnf

@ -39,14 +39,13 @@
"src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj",
"src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj",
"src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj",
"src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj",
"src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"src\\tools\\Avalonia.Analyzers\\Avalonia.Analyzers.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj",
"tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",
@ -66,4 +65,4 @@
"tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj"
]
}
}
}

2
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -307,7 +307,7 @@ namespace ControlCatalog.Pages
Content:
";
resultText += await ReadTextFromFile(file, 10000);
resultText += await ReadTextFromFile(file, 500);
}
openedFileContent.Text = resultText;

29
samples/ControlCatalog/Pages/TabControlPage.xaml

@ -4,7 +4,27 @@
xmlns="https://github.com/avaloniaui"
xmlns:viewModels="using:ControlCatalog.ViewModels"
x:DataType="viewModels:TabControlPageViewModel">
<DockPanel>
<DockPanel Classes.WithContentTemplates="{Binding IsChecked, ElementName=UseContentTemplates}">
<DockPanel.Styles>
<Style Selector="DockPanel.WithContentTemplates">
<Style Selector="^ TabItem">
<Setter Property="ContentTemplate">
<DataTemplate x:CompileBindings="False">
<Border BorderBrush="Red" BorderThickness="10">
<ContentPresenter Content="{Binding}"/>
</Border>
</DataTemplate>
</Setter>
</Style>
<Style Selector="^ TabControl">
<Setter Property="ContentTemplate">
<DataTemplate>
<TextBlock Text="This template should be overriden by each TabItem's template."/>
</DataTemplate>
</Setter>
</Style>
</Style>
</DockPanel.Styles>
<TextBlock
DockPanel.Dock="Top"
Classes="h2"
@ -55,14 +75,14 @@
Margin="0 16"
DisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}"
TabStripPlacement="{Binding TabPlacement}">
<TabControl.ContentTemplate>
<TabControl.DataTemplates>
<DataTemplate x:DataType="viewModels:TabControlPageViewModelItem">
<StackPanel Orientation="Vertical" Spacing="8">
<TextBlock Text="{Binding Text}"/>
<Image Source="{Binding Image}" Width="300"/>
</StackPanel>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl.DataTemplates>
<TabControl.Styles>
<Style Selector="TabItem" x:DataType="viewModels:TabControlPageViewModelItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
@ -78,12 +98,13 @@
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Tab Placement:</TextBlock>
<ComboBox SelectedIndex="{Binding TabPlacement, Mode=TwoWay}">
<ComboBox SelectedIndex="{Binding TabPlacement, Mode=TwoWay}" Width="100">
<ComboBoxItem>Left</ComboBoxItem>
<ComboBoxItem>Bottom</ComboBoxItem>
<ComboBoxItem>Right</ComboBoxItem>
<ComboBoxItem>Top</ComboBoxItem>
</ComboBox>
<CheckBox Name="UseContentTemplates">Set TabItem.ContentTemplate</CheckBox>
</StackPanel>
</Grid>
</DockPanel>

3
samples/IntegrationTestApp/MainWindow.axaml

@ -47,6 +47,9 @@
<Button Name="DisabledButton" IsEnabled="False">
Disabled Button
</Button>
<Button Name="EffectivelyDisabledButton" Command="{ReflectionBinding DoesntExist}">
Effectively Disabled Button
</Button>
<Button Name="BasicButton">
Basic Button
</Button>

112
src/Android/Avalonia.Android/Platform/Input/AndroidKeyboardDevice.cs

@ -5,8 +5,9 @@ using Avalonia.Input;
namespace Avalonia.Android.Platform.Input
{
internal class AndroidKeyboardDevice : KeyboardDevice, IKeyboardDevice {
private static readonly Dictionary<Keycode, Key> KeyDic = new Dictionary<Keycode, Key>
internal class AndroidKeyboardDevice : KeyboardDevice, IKeyboardDevice
{
private static readonly Dictionary<Keycode, Key> KeyDic = new Dictionary<Keycode, Key>
{
// { Keycode.Cancel?, Key.Cancel },
{ Keycode.Del, Key.Back },
@ -15,7 +16,7 @@ namespace Avalonia.Android.Platform.Input
{ Keycode.Clear, Key.Clear },
{ Keycode.Enter, Key.Return },
{ Keycode.MediaPause, Key.Pause },
//{ Keycode.?, Key.CapsLock }
{ Keycode.CapsLock, Key.CapsLock },
//{ Keycode.?, Key.HangulMode }
//{ Keycode.?, Key.JunjaMode }
//{ Keycode.?, Key.FinalMode }
@ -28,8 +29,8 @@ namespace Avalonia.Android.Platform.Input
{ Keycode.Space, Key.Space },
{ Keycode.PageUp, Key.Prior },
{ Keycode.PageDown, Key.PageDown },
// { Keycode.end?, Key.End },
{ Keycode.Home, Key.Home },
{ Keycode.MoveEnd, Key.End },
{ Keycode.MoveHome, Key.Home },
{ Keycode.DpadLeft, Key.Left },
{ Keycode.DpadUp, Key.Up },
{ Keycode.DpadRight, Key.Right },
@ -37,20 +38,20 @@ namespace Avalonia.Android.Platform.Input
// { Keycode.ButtonSelect?, Key.Select },
// { Keycode.print?, Key.Print },
//{ Keycode.execute?, Key.Execute },
// { Keycode.snap, Key.Snapshot }
//{ Keycode.snap?, Key.Snapshot }
{ Keycode.Insert, Key.Insert },
{ Keycode.ForwardDel, Key.Delete },
//{ Keycode.help, Key.Help },
//{ Keycode.?, Key.D0 }
//{ Keycode.?, Key.D1 }
//{ Keycode.?, Key.D2 }
//{ Keycode.?, Key.D3 }
//{ Keycode.?, Key.D4 }
//{ Keycode.?, Key.D5 }
//{ Keycode.?, Key.D6 }
//{ Keycode.?, Key.D7 }
//{ Keycode.?, Key.D8 }
//{ Keycode.?, Key.D9 }
{ Keycode.Help, Key.Help },
{ Keycode.Num0, Key.D0 },
{ Keycode.Num1, Key.D1 },
{ Keycode.Num2, Key.D2 },
{ Keycode.Num3, Key.D3 },
{ Keycode.Num4, Key.D4 },
{ Keycode.Num5, Key.D5 },
{ Keycode.Num6, Key.D6 },
{ Keycode.Num7, Key.D7 },
{ Keycode.Num8, Key.D8 },
{ Keycode.Num9, Key.D9 },
{ Keycode.A, Key.A },
{ Keycode.B, Key.B },
{ Keycode.C, Key.C },
@ -106,22 +107,22 @@ namespace Avalonia.Android.Platform.Input
//{ Keycode.?, Key.LWin }
//{ Keycode.?, Key.RWin }
//{ Keycode.?, Key.Apps }
//{ Keycode.?, Key.Sleep }
//{ Keycode.?, Key.NumPad0 }
//{ Keycode.?, Key.NumPad1 }
//{ Keycode.?, Key.NumPad2 }
//{ Keycode.?, Key.NumPad3 }
//{ Keycode.?, Key.NumPad4 }
//{ Keycode.?, Key.NumPad5 }
//{ Keycode.?, Key.NumPad6 }
//{ Keycode.?, Key.NumPad7 }
//{ Keycode.?, Key.NumPad8 }
//{ Keycode.?, Key.NumPad9 }
{ Keycode.Sleep, Key.Sleep },
{ Keycode.Numpad0, Key.NumPad0 },
{ Keycode.Numpad1, Key.NumPad1 },
{ Keycode.Numpad2, Key.NumPad2 },
{ Keycode.Numpad3, Key.NumPad3 },
{ Keycode.Numpad4, Key.NumPad4 },
{ Keycode.Numpad5, Key.NumPad5 },
{ Keycode.Numpad6, Key.NumPad6 },
{ Keycode.Numpad7, Key.NumPad7 },
{ Keycode.Numpad8, Key.NumPad8 },
{ Keycode.Numpad9, Key.NumPad9 },
{ Keycode.NumpadMultiply, Key.Multiply },
{ Keycode.NumpadAdd, Key.Add },
{ Keycode.NumpadComma, Key.Separator },
{ Keycode.NumpadSubtract, Key.Subtract },
//{ Keycode.numpaddecimal?, Key.Decimal }
{ Keycode.NumpadDot, Key.Decimal },
{ Keycode.NumpadDivide, Key.Divide },
{ Keycode.F1, Key.F1 },
{ Keycode.F2, Key.F2 },
@ -147,14 +148,14 @@ namespace Avalonia.Android.Platform.Input
//{ Keycode.R2, Key.F22 },
//{ Keycode.F23, Key.F23 },
//{ Keycode.R4, Key.F24 },
// { Keycode.numpad, Key.NumLock }
{ Keycode.NumLock, Key.NumLock },
{ Keycode.ScrollLock, Key.Scroll },
{ Keycode.ShiftLeft, Key.LeftShift },
//{ Keycode.?, Key.RightShift }
//{ Keycode.?, Key.LeftCtrl }
//{ Keycode.?, Key.RightCtrl }
//{ Keycode.?, Key.LeftAlt }
//{ Keycode.?, Key.RightAlt }
{ Keycode.ShiftRight, Key.RightShift },
{ Keycode.CtrlLeft, Key.LeftCtrl },
{ Keycode.CtrlRight, Key.RightCtrl },
{ Keycode.AltLeft, Key.LeftAlt },
{ Keycode.AltRight, Key.RightAlt },
//{ Keycode.?, Key.BrowserBack }
//{ Keycode.?, Key.BrowserForward }
//{ Keycode.?, Key.BrowserRefresh }
@ -163,28 +164,30 @@ namespace Avalonia.Android.Platform.Input
//{ Keycode.?, Key.BrowserFavorites }
//{ Keycode.?, Key.BrowserHome }
//{ Keycode.?, Key.VolumeMute }
//{ Keycode.?, Key.VolumeDown }
//{ Keycode.?, Key.VolumeUp }
//{ Keycode.?, Key.MediaNextTrack }
//{ Keycode.?, Key.MediaPreviousTrack }
//{ Keycode.?, Key.MediaStop }
//{ Keycode.?, Key.MediaPlayPause }
{ Keycode.VolumeDown, Key.VolumeDown },
{ Keycode.VolumeUp, Key.VolumeUp },
{ Keycode.MediaNext, Key.MediaNextTrack },
{ Keycode.MediaPrevious, Key.MediaPreviousTrack },
{ Keycode.MediaStop, Key.MediaStop },
{ Keycode.MediaPlayPause, Key.MediaPlayPause },
//{ Keycode.?, Key.LaunchMail }
//{ Keycode.?, Key.SelectMedia }
//{ Keycode.?, Key.LaunchApplication1 }
//{ Keycode.?, Key.LaunchApplication2 }
//{ Keycode.?, Key.OemSemicolon }
//{ Keycode.?, Key.OemPlus }
//{ Keycode.?, Key.OemComma }
//{ Keycode.?, Key.OemMinus }
//{ Keycode.?, Key.OemPeriod }
{ Keycode.Semicolon, Key.OemSemicolon },
{ Keycode.Plus, Key.OemPlus },
{ Keycode.Comma, Key.OemComma },
{ Keycode.Minus, Key.OemMinus },
{ Keycode.Period, Key.OemPeriod },
//{ Keycode.?, Key.Oem2 }
//{ Keycode.?, Key.OemTilde }
{ Keycode.Grave, Key.OemTilde },
//{ Keycode.?, Key.AbntC1 }
//{ Keycode.?, Key.AbntC2 }
//{ Keycode.?, Key.Oem4 }
//{ Keycode.?, Key.OemPipe }
//{ Keycode.?, Key.OemCloseBrackets }
{ Keycode.Apostrophe, Key.OemQuotes },
{ Keycode.Slash, Key.OemQuestion },
{ Keycode.LeftBracket, Key.OemOpenBrackets },
{ Keycode.RightBracket, Key.OemCloseBrackets },
//{ Keycode.?, Key.Oem7 }
//{ Keycode.?, Key.Oem8 }
//{ Keycode.?, Key.Oem102 }
@ -200,17 +203,18 @@ namespace Avalonia.Android.Platform.Input
//{ Keycode.?, Key.DbeEnterWordRegisterMode }
//{ Keycode.?, Key.DbeEnterImeConfigureMode }
//{ Keycode.?, Key.EraseEof }
//{ Keycode.?, Key.Play }
{ Keycode.MediaPlay, Key.Play },
//{ Keycode.?, Key.Zoom }
//{ Keycode.?, Key.NoName }
//{ Keycode.?, Key.DbeEnterDialogConversionMode }
//{ Keycode.?, Key.OemClear }
//{ Keycode.?, Key.DeadCharProcessed }
{ Keycode.Backslash, Key.OemBackslash }
};
internal static Key ConvertKey(Keycode key) {
Key result;
return KeyDic.TryGetValue(key, out result) ? result : Key.None;
internal static Key ConvertKey(Keycode key)
{
return KeyDic.TryGetValue(key, out var result) ? result : Key.None;
}
}
}
}

2
src/Avalonia.Base/Animation/AnimatorKeyFrame.cs

@ -67,7 +67,7 @@ namespace Avalonia.Animation
}
}
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]
public T GetTypedValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
{
var typeConv = TypeDescriptor.GetConverter(typeof(T));

2
src/Avalonia.Base/AvaloniaProperty.cs

@ -476,7 +476,7 @@ namespace Avalonia
/// </summary>
/// <param name="value">The value.</param>
/// <returns>True if the value is valid, otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConversionRequiresUnreferencedCodeMessage)]
public bool IsValidValue(object? value)
{
return TypeUtilities.TryConvertImplicit(PropertyType, value, out _);

2
src/Avalonia.Base/AvaloniaProperty`1.cs

@ -70,7 +70,7 @@ namespace Avalonia
private protected override IObservable<AvaloniaPropertyChangedEventArgs> GetChanged() => Changed;
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConversionSupressWarningMessage)]
private protected BindingValue<object?> TryConvert(object? value)
{
if (value == UnsetValue)

2
src/Avalonia.Base/Collections/AvaloniaListConverter.cs

@ -9,7 +9,7 @@ namespace Avalonia.Collections
/// <summary>
/// Creates an <see cref="AvaloniaList{T}"/> from a string representation.
/// </summary>
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]
public class AvaloniaListConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)

4
src/Avalonia.Base/Data/BindingValue.cs

@ -237,7 +237,7 @@ namespace Avalonia.Data
/// </summary>
/// <param name="value">The untyped value.</param>
/// <returns>The typed binding value.</returns>
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConversionRequiresUnreferencedCodeMessage)]
public static BindingValue<T> FromUntyped(object? value)
{
return FromUntyped(value, typeof(T));
@ -251,7 +251,7 @@ namespace Avalonia.Data
/// <param name="value">The untyped value.</param>
/// <param name="targetType">The runtime target type.</param>
/// <returns>The typed binding value.</returns>
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConversionRequiresUnreferencedCodeMessage)]
public static BindingValue<T> FromUntyped(object? value, Type targetType)
{
if (value == AvaloniaProperty.UnsetValue)

2
src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs

@ -10,7 +10,7 @@ namespace Avalonia.Data.Converters
/// Provides a default set of value conversions for bindings that do not specify a value
/// converter.
/// </summary>
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]
public class DefaultValueConverter : IValueConverter
{
/// <summary>

2
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -12,7 +12,7 @@ namespace Avalonia.Data.Core
/// Binds to an expression on an object using a type value converter to convert the values
/// that are sent and received.
/// </summary>
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]
internal class BindingExpression : LightweightObservableBase<object?>, IAvaloniaSubject<object?>, IDescription
{
private readonly ExpressionObserver _inner;

0
src/Markup/Avalonia.Markup/Data/TemplateBinding.cs → src/Avalonia.Base/Data/TemplateBinding.cs

8
src/Avalonia.Base/Diagnostics/TrimmingMessages.cs

@ -2,11 +2,11 @@
internal static class TrimmingMessages
{
public const string ImplicitTypeConvertionSupressWarningMessage = "Implicit convertion methods might be removed by the linker. We don't have a reliable way to prevent it, except converting everything in compile time when possible.";
public const string ImplicitTypeConvertionRequiresUnreferencedCodeMessage = "Implicit convertion methods are required for type conversion.";
public const string ImplicitTypeConversionSupressWarningMessage = "Implicit conversion methods might be removed by the linker. We don't have a reliable way to prevent it, except converting everything in compile time when possible.";
public const string ImplicitTypeConversionRequiresUnreferencedCodeMessage = "Implicit conversion methods are required for type conversion.";
public const string TypeConvertionSupressWarningMessage = "Convertion methods might be removed by the linker. We don't have a reliable way to prevent it, except converting everything in compile time when possible.";
public const string TypeConvertionRequiresUnreferencedCodeMessage = "Convertion methods are required for type conversion, including op_Implicit, op_Explicit, Parse and TypeConverter.";
public const string TypeConversionSupressWarningMessage = "Conversion methods might be removed by the linker. We don't have a reliable way to prevent it, except converting everything in compile time when possible.";
public const string TypeConversionRequiresUnreferencedCodeMessage = "Conversion methods are required for type conversion, including op_Implicit, op_Explicit, Parse and TypeConverter.";
public const string ReflectionBindingRequiresUnreferencedCodeMessage = "BindingExpression and ReflectionBinding heavily use reflection. Consider using CompiledBindings instead.";
public const string ReflectionBindingSupressWarningMessage = "BindingExpression and ReflectionBinding internal heavily use reflection.";

3
src/Avalonia.Base/Input/IAccessKeyHandler.cs

@ -5,8 +5,7 @@ namespace Avalonia.Input
/// <summary>
/// Defines the interface for classes that handle access keys for a window.
/// </summary>
[Unstable]
public interface IAccessKeyHandler
internal interface IAccessKeyHandler
{
/// <summary>
/// Gets or sets the window's main menu.

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

@ -8,11 +8,6 @@ namespace Avalonia.Input
[NotClientImplementable]
public interface IInputRoot : IInputElement
{
/// <summary>
/// Gets or sets the access key handler.
/// </summary>
IAccessKeyHandler AccessKeyHandler { get; }
/// <summary>
/// Gets or sets the keyboard navigation handler.
/// </summary>

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

@ -1,15 +1,12 @@
using System;
using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
/// <summary>
/// Defines the interface for a window's main menu.
/// </summary>
[NotClientImplementable]
public interface IMainMenu
internal interface IMainMenu
{
/// <summary>
/// Gets a value indicating whether the menu is open.

14
src/Avalonia.Base/Input/KeyboardNavigationHandler.cs

@ -77,13 +77,16 @@ namespace Avalonia.Input
/// <param name="direction">The direction to move.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
public void Move(
IInputElement element,
IInputElement? element,
NavigationDirection direction,
KeyModifiers keyModifiers = KeyModifiers.None)
{
element = element ?? throw new ArgumentNullException(nameof(element));
if (element is null && _owner is null)
{
return;
}
var next = GetNext(element, direction);
var next = GetNext(element ?? _owner!, direction);
if (next != null)
{
@ -101,10 +104,9 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param>
protected virtual void OnKeyDown(object? sender, KeyEventArgs e)
{
var current = FocusManager.GetFocusManager(e.Source as IInputElement)?.GetFocusedElement();
if (current != null && e.Key == Key.Tab)
if (e.Key == Key.Tab)
{
var current = FocusManager.GetFocusManager(e.Source as IInputElement)?.GetFocusedElement();
var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ?
NavigationDirection.Next : NavigationDirection.Previous;
Move(current, direction, e.KeyModifiers);

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

@ -70,6 +70,17 @@ namespace Avalonia.Input
if (relativeTo == null)
return pt;
// If the visual the user passed in, is not connected to the same visual root
// (i.e. they called it for a control inside a popup.
if (!ReferenceEquals(_rootVisual, relativeTo.VisualRoot) && relativeTo.VisualRoot is { })
{
// Convert to absolute screen coordinates.
var screenPt = _rootVisual.PointToScreen(pt);
// Convert to client co-ordinates of the visual inside the other visual root.
return relativeTo.PointToClient(screenPt);
}
return pt * _rootVisual.TransformToVisual(relativeTo) ?? default;
}

1
src/Avalonia.Base/Media/CharacterHit.cs

@ -19,6 +19,7 @@ namespace Avalonia.Media
/// <param name="firstCharacterIndex">Index of the first character that got hit.</param>
/// <param name="trailingLength">In the case of a leading edge, this value is 0. In the case of a trailing edge,
/// this value is the number of code points until the next valid caret position.</param>
[DebuggerStepThrough]
public CharacterHit(int firstCharacterIndex, int trailingLength = 0)
{
FirstCharacterIndex = firstCharacterIndex;

74
src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs

@ -18,14 +18,14 @@ namespace Avalonia.Media.TextFormatting
public static BidiReorderer Instance
=> t_instance ??= new();
public void BidiReorder(Span<TextRun> textRuns, FlowDirection flowDirection)
public IndexedTextRun[] BidiReorder(Span<TextRun> textRuns, FlowDirection flowDirection, int firstTextSourceIndex)
{
Debug.Assert(_runs.Length == 0);
Debug.Assert(_ranges.Length == 0);
if (textRuns.IsEmpty)
{
return;
return Array.Empty<IndexedTextRun>();
}
try
@ -46,6 +46,22 @@ namespace Avalonia.Media.TextFormatting
// Reorder them into visual order.
var firstIndex = LinearReorder();
var indexedTextRuns = new IndexedTextRun[textRuns.Length];
for (var i = 0; i < textRuns.Length; i++)
{
var currentRun = textRuns[i];
indexedTextRuns[i] = new IndexedTextRun
{
TextRun = currentRun,
TextSourceCharacterIndex = firstTextSourceIndex,
RunIndex = i,
NextRunIndex = i + 1
};
firstTextSourceIndex += currentRun.Length;
}
// Now perform a recursive reversal of each run.
// From the highest level found in the text to the lowest odd level on each line, including intermediate levels
@ -76,7 +92,7 @@ namespace Avalonia.Media.TextFormatting
if (max == 0 || (min == max && (max & 1) == 0))
{
// Nothing to reverse.
return;
return indexedTextRuns;
}
// Now apply the reversal and replace the original contents.
@ -107,13 +123,25 @@ namespace Avalonia.Media.TextFormatting
var index = 0;
currentIndex = firstIndex;
while (currentIndex >= 0)
{
ref var current = ref _runs[currentIndex];
textRuns[index++] = current.Run;
textRuns[index] = current.Run;
var indexedRun = indexedTextRuns[index];
indexedRun.RunIndex = current.RunIndex;
indexedRun.NextRunIndex = current.NextRunIndex;
index++;
currentIndex = current.NextRunIndex;
}
return indexedTextRuns;
}
finally
{
@ -227,25 +255,6 @@ namespace Avalonia.Media.TextFormatting
return previousIndex;
}
private struct OrderedBidiRun
{
public OrderedBidiRun(int runIndex, TextRun run, sbyte level)
{
RunIndex = runIndex;
Run = run;
Level = level;
NextRunIndex = -1;
}
public int RunIndex { get; }
public sbyte Level { get; }
public TextRun Run { get; }
public int NextRunIndex { get; set; } // -1 if none
}
private struct BidiRange
{
public BidiRange(sbyte level, int leftRunIndex, int rightRunIndex, int previousRangeIndex)
@ -265,4 +274,23 @@ namespace Avalonia.Media.TextFormatting
public int PreviousRangeIndex { get; } // -1 if none
}
}
internal struct OrderedBidiRun
{
public OrderedBidiRun(int runIndex, TextRun run, sbyte level)
{
RunIndex = runIndex;
Run = run;
Level = level;
NextRunIndex = -1;
}
public int RunIndex { get; }
public sbyte Level { get; }
public TextRun Run { get; }
public int NextRunIndex { get; set; } // -1 if none
}
}

10
src/Avalonia.Base/Media/TextFormatting/IndexedTextRun.cs

@ -0,0 +1,10 @@
namespace Avalonia.Media.TextFormatting
{
internal class IndexedTextRun
{
public int TextSourceCharacterIndex { get; init; }
public int RunIndex { get; set; }
public int NextRunIndex { get; set; }
public TextRun? TextRun { get; init; }
}
}

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

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Diagnostics;
namespace Avalonia.Media.TextFormatting
{
@ -10,6 +11,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Constructing TextBounds object
/// </summary>
[DebuggerStepThrough]
internal TextBounds(Rect bounds, FlowDirection flowDirection, IList<TextRunBounds> runBounds)
{
Rectangle = bounds;

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

@ -60,14 +60,10 @@ namespace Avalonia.Media.TextFormatting
_textTrimming = textTrimming ?? TextTrimming.None;
LineHeight = lineHeight;
MaxWidth = maxWidth;
MaxHeight = maxHeight;
LetterSpacing = letterSpacing;
MaxLines = maxLines;
_textLines = CreateTextLines();
@ -81,8 +77,6 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textTrimming">The text trimming.</param>
/// <param name="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <param name="maxLines">The maximum number of text lines.</param>
public TextLayout(
ITextSource textSource,
@ -90,8 +84,6 @@ namespace Avalonia.Media.TextFormatting
TextTrimming? textTrimming = null,
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
double letterSpacing = 0,
int maxLines = 0)
{
_textSource = textSource;
@ -100,14 +92,10 @@ namespace Avalonia.Media.TextFormatting
_textTrimming = textTrimming ?? TextTrimming.None;
LineHeight = lineHeight;
MaxWidth = maxWidth;
MaxHeight = maxHeight;
LetterSpacing = letterSpacing;
MaxLines = maxLines;
_textLines = CreateTextLines();
@ -120,7 +108,7 @@ namespace Avalonia.Media.TextFormatting
/// A value of NaN (equivalent to an attribute value of "Auto") indicates that the line height
/// is determined automatically from the current font characteristics. The default is NaN.
/// </remarks>
public double LineHeight { get; }
public double LineHeight => _paragraphProperties.LineHeight;
/// <summary>
/// Gets the maximum width.
@ -140,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Gets the text spacing.
/// </summary>
public double LetterSpacing { get; }
public double LetterSpacing => _paragraphProperties.LetterSpacing;
/// <summary>
/// Gets the text lines.
@ -495,7 +483,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <returns></returns>
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
internal static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping,
TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight,
double letterSpacing)

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

@ -4,8 +4,12 @@ using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal sealed class TextLineImpl : TextLine
internal class TextLineImpl : TextLine
{
internal static Comparer<TextBounds> TextBoundsComparer { get; } =
Comparer<TextBounds>.Create((x, y) => x.Rectangle.Left.CompareTo(y.Rectangle.Left));
private IReadOnlyList<IndexedTextRun>? _indexedTextRuns;
private readonly TextRun[] _textRuns;
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
@ -338,184 +342,169 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var flowDirection = _paragraphProperties.FlowDirection;
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var currentPosition = FirstTextSourceIndex;
var remainingLength = characterIndex - FirstTextSourceIndex;
var currentDistance = Start;
if (flowDirection == FlowDirection.LeftToRight)
if (_indexedTextRuns is null || _indexedTextRuns.Count == 0)
{
for (var index = 0; index < _textRuns.Length; index++)
{
var currentRun = _textRuns[index];
if (currentRun is ShapedTextRun shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
{
var i = index;
var rightToLeftWidth = shapedRun.Size.Width;
while (i + 1 <= _textRuns.Length - 1)
{
var nextRun = _textRuns[i + 1];
if (nextRun is ShapedTextRun nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight)
{
i++;
return Start;
}
rightToLeftWidth += nextShapedRun.Size.Width;
var characterIndex = Math.Min(
characterHit.FirstCharacterIndex + characterHit.TrailingLength,
FirstTextSourceIndex + Length);
continue;
}
var currentPosition = FirstTextSourceIndex;
break;
}
static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection)
{
if (textRun is ShapedTextRun shapedTextRun)
{
return shapedTextRun.ShapedBuffer.IsLeftToRight ?
FlowDirection.LeftToRight :
FlowDirection.RightToLeft;
}
if (i > index)
{
while (i >= index)
{
currentRun = _textRuns[i];
return currentDirection;
}
if (currentRun is DrawableTextRun drawable)
{
rightToLeftWidth -= drawable.Size.Width;
}
IndexedTextRun FindIndexedRun()
{
var i = 0;
if (currentPosition + currentRun.Length >= characterIndex)
{
break;
}
IndexedTextRun currentIndexedRun = _indexedTextRuns[i];
currentPosition += currentRun.Length;
while(currentIndexedRun.TextSourceCharacterIndex != currentPosition)
{
if(i + 1 < _indexedTextRuns.Count)
{
i++;
remainingLength -= currentRun.Length;
currentIndexedRun = _indexedTextRuns[i];
}
}
i--;
}
return currentIndexedRun;
}
currentDistance += rightToLeftWidth;
}
}
double GetPreceedingDistance(int firstIndex)
{
var distance = 0.0;
if (currentPosition + currentRun.Length >= characterIndex &&
TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _))
{
return Math.Max(0, currentDistance + distance);
}
for (var i = 0; i < firstIndex; i++)
{
var currentRun = _textRuns[i];
if (currentRun is DrawableTextRun drawableTextRun)
{
currentDistance += drawableTextRun.Size.Width;
distance += drawableTextRun.Size.Width;
}
//No hit hit found so we add the full width
currentPosition += currentRun.Length;
remainingLength -= currentRun.Length;
}
return distance;
}
else
TextRun? currentTextRun = null;
var currentIndexedRun = FindIndexedRun();
while (currentPosition < FirstTextSourceIndex + Length)
{
currentDistance += WidthIncludingTrailingWhitespace;
currentTextRun = currentIndexedRun.TextRun;
for (var index = _textRuns.Length - 1; index >= 0; index--)
if (currentTextRun == null)
{
var currentRun = _textRuns[index];
break;
}
if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength,
flowDirection, out var distance, out var currentGlyphRun))
if (currentIndexedRun.TextSourceCharacterIndex + currentTextRun.Length <= characterHit.FirstCharacterIndex)
{
if (currentPosition + currentTextRun.Length < FirstTextSourceIndex + Length)
{
if (currentGlyphRun != null)
{
currentDistance -= currentGlyphRun.Bounds.Width;
}
currentPosition += currentTextRun.Length;
return currentDistance + distance;
}
currentIndexedRun = FindIndexedRun();
if (currentRun is DrawableTextRun drawableTextRun)
{
currentDistance -= drawableTextRun.Size.Width;
continue;
}
//No hit hit found so we add the full width
currentPosition += currentRun.Length;
remainingLength -= currentRun.Length;
}
break;
}
return Math.Max(0, currentDistance);
}
if (currentTextRun == null)
{
return 0;
}
private static bool TryGetDistanceFromCharacterHit(
TextRun currentRun,
CharacterHit characterHit,
int currentPosition,
int remainingLength,
FlowDirection flowDirection,
out double distance,
out GlyphRun? currentGlyphRun)
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var isTrailingHit = characterHit.TrailingLength > 0;
var directionalWidth = 0.0;
var firstRunIndex = currentIndexedRun.RunIndex;
var lastRunIndex = firstRunIndex;
distance = 0;
currentGlyphRun = null;
var currentDirection = GetDirection(currentTextRun, _resolvedFlowDirection);
switch (currentRun)
var currentX = Start + GetPreceedingDistance(currentIndexedRun.RunIndex);
if (currentTextRun is DrawableTextRun currentDrawable)
{
case ShapedTextRun shapedTextCharacters:
{
currentGlyphRun = shapedTextCharacters.GlyphRun;
directionalWidth = currentDrawable.Size.Width;
}
if (currentPosition + remainingLength <= currentPosition + currentRun.Length)
{
characterHit = new CharacterHit(currentPosition + remainingLength);
if (currentTextRun is not TextEndOfLine)
{
if (currentDirection == FlowDirection.LeftToRight)
{
// Find consecutive runs of same direction
for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++)
{
var nextRun = _textRuns[lastRunIndex + 1];
distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit);
var nextDirection = GetDirection(nextRun, currentDirection);
return true;
if (currentDirection != nextDirection)
{
break;
}
if (currentPosition + remainingLength == currentPosition + currentRun.Length && isTrailingHit)
if (nextRun is DrawableTextRun nextDrawable)
{
if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft)
{
distance = currentGlyphRun.Bounds.Width;
}
return true;
directionalWidth += nextDrawable.Size.Width;
}
break;
}
case DrawableTextRun drawableTextRun:
}
else
{
// Find consecutive runs of same direction
for (; firstRunIndex - 1 > 0; firstRunIndex--)
{
if (characterIndex == currentPosition)
var previousRun = _textRuns[firstRunIndex - 1];
var previousDirection = GetDirection(previousRun, currentDirection);
if (currentDirection != previousDirection)
{
return true;
break;
}
if (characterIndex == currentPosition + currentRun.Length)
if (previousRun is DrawableTextRun previousDrawable)
{
distance = drawableTextRun.Size.Width;
return true;
directionalWidth += previousDrawable.Size.Width;
currentX -= previousDrawable.Size.Width;
}
}
}
}
break;
switch (currentDirection)
{
case FlowDirection.RightToLeft:
{
return GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, characterIndex,
currentPosition, 1, out _, out _).Rectangle.Right;
}
default:
{
return false;
return GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, characterIndex,
currentPosition, 1, out _, out _).Rectangle.Left;
}
}
return false;
}
/// <inheritdoc/>
@ -585,7 +574,7 @@ namespace Avalonia.Media.TextFormatting
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
{
if (_textRuns.Length == 0)
if (_indexedTextRuns is null || _indexedTextRuns.Count == 0)
{
return Array.Empty<TextBounds>();
}
@ -607,303 +596,154 @@ namespace Avalonia.Media.TextFormatting
return currentDirection;
}
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
IndexedTextRun FindIndexedRun()
{
var currentX = Start;
var i = 0;
for (int i = 0; i < _textRuns.Length; i++)
{
var currentRun = _textRuns[i];
IndexedTextRun currentIndexedRun = _indexedTextRuns[i];
var firstRunIndex = i;
var lastRunIndex = firstRunIndex;
var currentDirection = GetDirection(currentRun, FlowDirection.LeftToRight);
var directionalWidth = 0.0;
if (currentRun is DrawableTextRun currentDrawable)
while (currentIndexedRun.TextSourceCharacterIndex != currentPosition)
{
if (i + 1 < _indexedTextRuns.Count)
{
directionalWidth = currentDrawable.Size.Width;
}
i++;
// Find consecutive runs of same direction
for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++)
{
var nextRun = _textRuns[lastRunIndex + 1];
currentIndexedRun = _indexedTextRuns[i];
}
}
var nextDirection = GetDirection(nextRun, currentDirection);
return currentIndexedRun;
}
if (currentDirection != nextDirection)
{
break;
}
double GetPreceedingDistance(int firstIndex)
{
var distance = 0.0;
if (nextRun is DrawableTextRun nextDrawable)
{
directionalWidth += nextDrawable.Size.Width;
}
}
for (var i = 0; i < firstIndex; i++)
{
var currentRun = _textRuns[i];
//Skip runs that are not part of the hit test range
switch (currentDirection)
if (currentRun is DrawableTextRun drawableTextRun)
{
case FlowDirection.RightToLeft:
{
for (; lastRunIndex >= firstRunIndex; lastRunIndex--)
{
currentRun = _textRuns[lastRunIndex];
distance += drawableTextRun.Size.Width;
}
}
if (currentPosition + currentRun.Length > firstTextSourceIndex)
{
break;
}
return distance;
}
currentPosition += currentRun.Length;
while (remainingLength > 0 && currentPosition < FirstTextSourceIndex + Length)
{
var currentIndexedRun = FindIndexedRun();
if (currentRun is DrawableTextRun drawableTextRun)
{
directionalWidth -= drawableTextRun.Size.Width;
currentX += drawableTextRun.Size.Width;
}
if (currentIndexedRun == null)
{
break;
}
if (lastRunIndex - 1 < 0)
{
break;
}
}
var directionalWidth = 0.0;
var firstRunIndex = currentIndexedRun.RunIndex;
var lastRunIndex = firstRunIndex;
var currentTextRun = currentIndexedRun.TextRun;
break;
}
default:
{
for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
{
currentRun = _textRuns[firstRunIndex];
if (currentPosition + currentRun.Length > firstTextSourceIndex)
{
break;
}
if (currentTextRun == null)
{
break;
}
currentPosition += currentRun.Length;
var currentDirection = GetDirection(currentTextRun, _resolvedFlowDirection);
if (currentRun is DrawableTextRun drawableTextRun)
{
currentX += drawableTextRun.Size.Width;
directionalWidth -= drawableTextRun.Size.Width;
}
if (currentIndexedRun.TextSourceCharacterIndex + currentTextRun.Length <= firstTextSourceIndex)
{
currentPosition += currentTextRun.Length;
if (firstRunIndex + 1 == _textRuns.Length)
{
break;
}
}
continue;
}
break;
}
}
var currentX = Start + GetPreceedingDistance(currentIndexedRun.RunIndex);
i = lastRunIndex;
if (currentTextRun is DrawableTextRun currentDrawable)
{
directionalWidth = currentDrawable.Size.Width;
}
//Possible overlap at runs of different direction
if (directionalWidth == 0 && i < _textRuns.Length - 1)
if (currentTextRun is not TextEndOfLine)
{
if (currentDirection == FlowDirection.LeftToRight)
{
//In case a run only contains a linebreak we don't want to skip it.
if (currentRun is ShapedTextRun shaped)
{
if (currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0)
{
continue;
}
}
else
// Find consecutive runs of same direction
for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++)
{
continue;
}
}
var nextRun = _textRuns[lastRunIndex + 1];
int coveredLength;
TextBounds? textBounds;
var nextDirection = GetDirection(nextRun, currentDirection);
switch (currentDirection)
{
case FlowDirection.RightToLeft:
if (currentDirection != nextDirection)
{
textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
currentX += directionalWidth;
break;
}
default:
{
textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
currentX = textBounds.Rectangle.Right;
break;
if (nextRun is DrawableTextRun nextDrawable)
{
directionalWidth += nextDrawable.Size.Width;
}
}
}
if (coveredLength > 0)
{
result.Add(textBounds);
remainingLength -= coveredLength;
}
if (remainingLength <= 0)
{
break;
}
}
}
else
{
var currentX = Start + WidthIncludingTrailingWhitespace;
for (int i = _textRuns.Length - 1; i >= 0; i--)
{
var currentRun = _textRuns[i];
var firstRunIndex = i;
var lastRunIndex = firstRunIndex;
var currentDirection = GetDirection(currentRun, FlowDirection.RightToLeft);
var directionalWidth = 0.0;
if (currentRun is DrawableTextRun currentDrawable)
{
directionalWidth = currentDrawable.Size.Width;
}
// Find consecutive runs of same direction
for (; firstRunIndex - 1 > 0; firstRunIndex--)
else
{
var previousRun = _textRuns[firstRunIndex - 1];
var previousDirection = GetDirection(previousRun, currentDirection);
if (currentDirection != previousDirection)
// Find consecutive runs of same direction
for (; firstRunIndex - 1 > 0; firstRunIndex--)
{
break;
}
var previousRun = _textRuns[firstRunIndex - 1];
if (currentRun is DrawableTextRun previousDrawable)
{
directionalWidth += previousDrawable.Size.Width;
}
}
var previousDirection = GetDirection(previousRun, currentDirection);
//Skip runs that are not part of the hit test range
switch (currentDirection)
{
case FlowDirection.RightToLeft:
if (currentDirection != previousDirection)
{
for (; lastRunIndex >= firstRunIndex; lastRunIndex--)
{
currentRun = _textRuns[lastRunIndex];
if (currentPosition + currentRun.Length <= firstTextSourceIndex)
{
currentPosition += currentRun.Length;
if (currentRun is DrawableTextRun drawableTextRun)
{
currentX -= drawableTextRun.Size.Width;
directionalWidth -= drawableTextRun.Size.Width;
}
continue;
}
break;
}
break;
}
default:
{
for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
{
currentRun = _textRuns[firstRunIndex];
if (currentPosition + currentRun.Length <= firstTextSourceIndex)
{
currentPosition += currentRun.Length;
if (currentRun is DrawableTextRun drawableTextRun)
{
currentX += drawableTextRun.Size.Width;
directionalWidth -= drawableTextRun.Size.Width;
}
continue;
}
break;
}
if (previousRun is DrawableTextRun previousDrawable)
{
directionalWidth += previousDrawable.Size.Width;
break;
currentX -= previousDrawable.Size.Width;
}
}
}
}
i = firstRunIndex;
int coveredLength;
TextBounds? textBounds;
//Possible overlap at runs of different direction
if (directionalWidth == 0 && i > 0)
{
//In case a run only contains a linebreak we don't want to skip it.
if (currentRun is ShapedTextRun shaped)
switch (currentDirection)
{
case FlowDirection.RightToLeft:
{
if (currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0)
{
continue;
}
textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
break;
}
else
default:
{
continue;
}
}
int coveredLength;
TextBounds? textBounds;
switch (currentDirection)
{
case FlowDirection.LeftToRight:
{
textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX - directionalWidth, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
currentX -= directionalWidth;
break;
}
default:
{
textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
currentX = textBounds.Rectangle.Left;
textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
break;
}
}
break;
}
}
//Visual order is always left to right so we need to insert
result.Insert(0, textBounds);
if (coveredLength > 0)
{
result.Add(textBounds);
remainingLength -= coveredLength;
if (remainingLength <= 0)
{
break;
}
}
}
result.Sort(TextBoundsComparer);
return result;
}
@ -1164,7 +1004,7 @@ namespace Avalonia.Media.TextFormatting
_textLineBreak = new TextLineBreak(textEndOfLine);
}
BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection);
_indexedTextRuns = BidiReorderer.Instance.BidiReorder(_textRuns, _paragraphProperties.FlowDirection, FirstTextSourceIndex);
}
/// <summary>
@ -1211,13 +1051,6 @@ namespace Avalonia.Media.TextFormatting
return true;
}
//var characterIndex = codepointIndex - shapedRun.Text.Start;
//if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
//{
// foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
//}
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
foundCharacterHit :
new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
@ -1556,8 +1389,8 @@ namespace Avalonia.Media.TextFormatting
TrailingWhitespaceLength = trailingWhitespaceLength,
Width = width,
WidthIncludingTrailingWhitespace = widthIncludingWhitespace,
OverhangLeading= overhangLeading,
OverhangTrailing= overhangTrailing,
OverhangLeading = overhangLeading,
OverhangTrailing = overhangTrailing,
OverhangAfter = overhangAfter
};
}
@ -1615,8 +1448,7 @@ namespace Avalonia.Media.TextFormatting
return Math.Max(0, start);
case TextAlignment.Right:
return Math.Max(0, _paragraphWidth - width);
return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);
default:
return 0;
}

1
src/Avalonia.Base/Properties/AssemblyInfo.cs

@ -6,6 +6,7 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Easings")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")]

2
src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs

@ -5,7 +5,7 @@ using Avalonia.Threading;
namespace Avalonia.PropertyStore
{
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConversionSupressWarningMessage)]
internal class DirectUntypedBindingObserver<T> : IObserver<object?>,
IDisposable
{

5
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@ -208,7 +208,7 @@ namespace Avalonia.PropertyStore
IsOverridenCurrentValue = isOverriddenCurrentValue;
IsCoercedDefaultValue = isCoercedDefaultValue;
if (_uncommon?._coerce is { } coerce)
if (!isCoercedDefaultValue && _uncommon?._coerce is { } coerce)
v = coerce(owner.Owner, value);
if (priority <= Priority)
@ -262,7 +262,8 @@ namespace Avalonia.PropertyStore
if (_uncommon?._coerce is { } coerce)
{
v = coerce(owner.Owner, value);
bv = coerce(owner.Owner, baseValue);
if (priority != basePriority)
bv = coerce(owner.Owner, baseValue);
}
if (!EqualityComparer<T>.Default.Equals(Value, v))

2
src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs

@ -14,7 +14,7 @@ namespace Avalonia.PropertyStore
public void Start(IObservable<object?> source) => _subscription = source.Subscribe(this);
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConversionSupressWarningMessage)]
public void OnNext(object? value)
{
if (value == BindingOperations.DoNothing)

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

@ -7,7 +7,7 @@ namespace Avalonia.PropertyStore
{
internal static class UntypedValueUtils
{
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConversionSupressWarningMessage)]
public static BindingValue<T> ConvertAndValidate<T>(
object? value,
Type targetType,
@ -24,7 +24,7 @@ namespace Avalonia.PropertyStore
return v;
}
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConversionSupressWarningMessage)]
public static bool TryConvertAndValidate<T>(
StyledProperty<T> property,
object? value,

5
src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs

@ -177,7 +177,10 @@ internal class RenderDataDrawingContext : DrawingContext
});
}
public override void Custom(ICustomDrawOperation custom) => Add(new RenderDataCustomNode());
public override void Custom(ICustomDrawOperation custom) => Add(new RenderDataCustomNode
{
Operation = custom
});
public override void DrawGlyphRun(IBrush? foreground, GlyphRun? glyphRun)
{

2
src/Avalonia.Base/StyledProperty.cs

@ -221,7 +221,7 @@ namespace Avalonia
return target.Bind<TValue>(this, source, priority);
}
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConversionSupressWarningMessage)]
private bool ShouldSetValue(AvaloniaObject target, object? value, [NotNullWhen(true)] out TValue? converted)
{
if (value == BindingOperations.DoNothing)

4
src/Avalonia.Base/Styling/PropertyEqualsSelector.cs

@ -91,8 +91,8 @@ namespace Avalonia.Styling
private protected override Selector? MovePrevious() => _previous;
private protected override Selector? MovePreviousOrParent() => _previous;
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.TypeConvertionSupressWarningMessage)]
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = TrimmingMessages.TypeConvertionSupressWarningMessage)]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.TypeConversionSupressWarningMessage)]
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = TrimmingMessages.TypeConversionSupressWarningMessage)]
internal static bool Compare(Type propertyType, object? propertyValue, object? value)
{
if (propertyType == typeof(object) &&

2
src/Avalonia.Base/Styling/Setter.cs

@ -65,7 +65,7 @@ namespace Avalonia.Styling
void IValueEntry.Unsubscribe() { }
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConversionSupressWarningMessage)]
internal override ISetterInstance Instance(IStyleInstance instance, StyledElement target)
{
if (target is not AvaloniaObject ao)

10
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -126,7 +126,7 @@ namespace Avalonia.Utilities
/// <param name="culture">The culture to use.</param>
/// <param name="result">If successful, contains the convert value.</param>
/// <returns>True if the cast was successful, otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]
public static bool TryConvert(Type to, object? value, CultureInfo? culture, out object? result)
{
if (value == null)
@ -246,7 +246,7 @@ namespace Avalonia.Utilities
/// <param name="value">The value to convert.</param>
/// <param name="result">If successful, contains the converted value.</param>
/// <returns>True if the convert was successful, otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConversionRequiresUnreferencedCodeMessage)]
public static bool TryConvertImplicit(Type to, object? value, out object? result)
{
if (value == null)
@ -309,7 +309,7 @@ namespace Avalonia.Utilities
/// <param name="type">The type to convert to..</param>
/// <param name="culture">The culture to use.</param>
/// <returns>A value of <paramref name="type"/>.</returns>
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]
public static object? ConvertOrDefault(object? value, Type type, CultureInfo culture)
{
return TryConvert(type, value, culture, out var result) ? result : Default(type);
@ -322,13 +322,13 @@ namespace Avalonia.Utilities
/// <param name="value">The value to convert.</param>
/// <param name="type">The type to convert to.</param>
/// <returns>A value of <paramref name="type"/>.</returns>
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConversionRequiresUnreferencedCodeMessage)]
public static object? ConvertImplicitOrDefault(object? value, Type type)
{
return TryConvertImplicit(type, value, out var result) ? result : Default(type);
}
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConversionRequiresUnreferencedCodeMessage)]
public static T ConvertImplicit<T>(object? value)
{
if (TryConvertImplicit(typeof(T), value, out var result))

2
src/Avalonia.Base/Visual.cs

@ -774,7 +774,7 @@ namespace Avalonia
/// Computes the <see cref="HasMirrorTransform"/> value according to the
/// <see cref="FlowDirection"/> and <see cref="BypassFlowDirectionPolicies"/>
/// </summary>
public virtual void InvalidateMirrorTransform()
protected internal virtual void InvalidateMirrorTransform()
{
var flowDirection = this.FlowDirection;
var parentFlowDirection = FlowDirection.LeftToRight;

6
src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs

@ -98,10 +98,10 @@ namespace Avalonia.Controls.Primitives
int pixelWidth;
int pixelHeight;
if (base._track != null)
if (base.Track != null)
{
pixelWidth = Convert.ToInt32(base._track.Bounds.Width * scale);
pixelHeight = Convert.ToInt32(base._track.Bounds.Height * scale);
pixelWidth = Convert.ToInt32(base.Track.Bounds.Width * scale);
pixelHeight = Convert.ToInt32(base.Track.Bounds.Height * scale);
}
else
{

22
src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs

@ -31,11 +31,31 @@ namespace Avalonia.Automation.Peers
return CreatePeerForElement(element);
}
/// <summary>
/// Gets the <see cref="AutomationPeer"/> for a <see cref="Control"/>, creating it if
/// necessary.
/// </summary>
/// <param name="element">The control.</param>
/// <returns>The automation peer.</returns>
/// <remarks>
/// Despite the name (which comes from the analogous WPF API), this method does not create
/// a new peer if one already exists: instead it returns the existing peer.
/// </remarks>
public static AutomationPeer CreatePeerForElement(Control element)
{
return element.GetOrCreateAutomationPeer();
}
/// <summary>
/// Gets an existing <see cref="AutomationPeer"/> for a <see cref="Control"/>.
/// </summary>
/// <param name="element">The control.</param>
/// <returns>The automation peer if already created; otherwise null.</returns>
/// <remarks>
/// To ensure that a peer is created, use <see cref="CreatePeerForElement(Control)"/>.
/// </remarks>
public static AutomationPeer? FromElement(Control element) => element.GetAutomationPeer();
protected override void BringIntoViewCore() => Owner.BringIntoView();
protected override IReadOnlyList<AutomationPeer> GetOrCreateChildrenCore()
@ -151,7 +171,7 @@ namespace Avalonia.Automation.Peers
protected override bool HasKeyboardFocusCore() => Owner.IsFocused;
protected override bool IsContentElementCore() => true;
protected override bool IsControlElementCore() => true;
protected override bool IsEnabledCore() => Owner.IsEnabled;
protected override bool IsEnabledCore() => Owner.IsEffectivelyEnabled;
protected override bool IsKeyboardFocusableCore() => Owner.Focusable;
protected override void SetFocusCore() => Owner.Focus();

2
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -14,6 +14,7 @@
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Controls.ItemsRepeater, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.DesignerSupport, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Diagnostics, PublicKey=$(AvaloniaPublicKey)"/>
<InternalsVisibleTo Include="Avalonia.LeakTests, PublicKey=$(AvaloniaPublicKey)" />
@ -22,5 +23,6 @@
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.DesignerSupport.Remote, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Browser, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
</ItemGroup>
</Project>

90
src/Avalonia.Controls/Border.cs

@ -47,30 +47,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
AvaloniaProperty.Register<Border, BoxShadows>(nameof(BoxShadow));
/// <summary>
/// Defines the <see cref="BorderDashOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> BorderDashOffsetProperty =
AvaloniaProperty.Register<Border, double>(nameof(BorderDashOffset));
/// <summary>
/// Defines the <see cref="BorderDashArray"/> property.
/// </summary>
public static readonly StyledProperty<AvaloniaList<double>?> BorderDashArrayProperty =
AvaloniaProperty.Register<Border, AvaloniaList<double>?>(nameof(BorderDashArray));
/// <summary>
/// Defines the <see cref="BorderLineCap"/> property.
/// </summary>
public static readonly StyledProperty<PenLineCap> BorderLineCapProperty =
AvaloniaProperty.Register<Border, PenLineCap>(nameof(BorderLineCap), PenLineCap.Flat);
/// <summary>
/// Defines the <see cref="BorderLineJoin"/> property.
/// </summary>
public static readonly StyledProperty<PenLineJoin> BorderLineJoinProperty =
AvaloniaProperty.Register<Border, PenLineJoin>(nameof(BorderLineJoin), PenLineJoin.Miter);
private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
private Thickness? _layoutThickness;
private double _scale;
@ -86,10 +62,6 @@ namespace Avalonia.Controls
BorderBrushProperty,
BorderThicknessProperty,
CornerRadiusProperty,
BorderDashArrayProperty,
BorderLineCapProperty,
BorderLineJoinProperty,
BorderDashOffsetProperty,
BoxShadowProperty);
AffectsMeasure<Border>(BorderThicknessProperty);
}
@ -115,8 +87,8 @@ namespace Avalonia.Controls
/// </summary>
public IBrush? Background
{
get { return GetValue(BackgroundProperty); }
set { SetValue(BackgroundProperty, value); }
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
/// <summary>
@ -124,17 +96,8 @@ namespace Avalonia.Controls
/// </summary>
public IBrush? BorderBrush
{
get { return GetValue(BorderBrushProperty); }
set { SetValue(BorderBrushProperty, value); }
}
/// <summary>
/// Gets or sets a collection of <see cref="double"/> values that indicate the pattern of dashes and gaps that is used to outline shapes.
/// </summary>
public AvaloniaList<double>? BorderDashArray
{
get { return GetValue(BorderDashArrayProperty); }
set { SetValue(BorderDashArrayProperty, value); }
get => GetValue(BorderBrushProperty);
set => SetValue(BorderBrushProperty, value);
}
/// <summary>
@ -142,35 +105,8 @@ namespace Avalonia.Controls
/// </summary>
public Thickness BorderThickness
{
get { return GetValue(BorderThicknessProperty); }
set { SetValue(BorderThicknessProperty, value); }
}
/// <summary>
/// Gets or sets a value that specifies the distance within the dash pattern where a dash begins.
/// </summary>
public double BorderDashOffset
{
get { return GetValue(BorderDashOffsetProperty); }
set { SetValue(BorderDashOffsetProperty, value); }
}
/// <summary>
/// Gets or sets a <see cref="PenLineCap"/> enumeration value that describes the shape at the ends of a line.
/// </summary>
public PenLineCap BorderLineCap
{
get { return GetValue(BorderLineCapProperty); }
set { SetValue(BorderLineCapProperty, value); }
}
/// <summary>
/// Gets or sets a <see cref="PenLineJoin"/> enumeration value that specifies the type of join that is used at the vertices of a Shape.
/// </summary>
public PenLineJoin BorderLineJoin
{
get { return GetValue(BorderLineJoinProperty); }
set { SetValue(BorderLineJoinProperty, value); }
get => GetValue(BorderThicknessProperty);
set => SetValue(BorderThicknessProperty, value);
}
/// <summary>
@ -178,8 +114,8 @@ namespace Avalonia.Controls
/// </summary>
public CornerRadius CornerRadius
{
get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
get => GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
/// <summary>
@ -227,8 +163,14 @@ namespace Avalonia.Controls
/// <param name="context">The drawing context.</param>
public sealed override void Render(DrawingContext context)
{
_borderRenderHelper.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush,
BoxShadow, BorderDashOffset, BorderLineCap, BorderLineJoin, BorderDashArray);
_borderRenderHelper.Render(
context,
Bounds.Size,
LayoutThickness,
CornerRadius,
Background,
BorderBrush,
BoxShadow);
}
/// <summary>

4
src/Avalonia.Controls/Button.cs

@ -34,8 +34,8 @@ namespace Avalonia.Controls
[PseudoClasses(pcFlyoutOpen, pcPressed)]
public class Button : ContentControl, ICommandSource, IClickableControl
{
protected const string pcPressed = ":pressed";
protected const string pcFlyoutOpen = ":flyout-open";
private const string pcPressed = ":pressed";
private const string pcFlyoutOpen = ":flyout-open";
/// <summary>
/// Defines the <see cref="ClickMode"/> property.

2
src/Avalonia.Controls/Calendar/CalendarButton.cs

@ -39,7 +39,7 @@ namespace Avalonia.Controls.Primitives
public CalendarButton()
: base()
{
Content = DateTimeHelper.GetCurrentDateFormat().AbbreviatedMonthNames[0];
SetCurrentValue(ContentProperty, DateTimeHelper.GetCurrentDateFormat().AbbreviatedMonthNames[0]);
}
/// <summary>

2
src/Avalonia.Controls/Calendar/CalendarDayButton.cs

@ -34,7 +34,7 @@ namespace Avalonia.Controls.Primitives
: base()
{
//Focusable = false;
Content = DefaultContent.ToString(CultureInfo.CurrentCulture);
SetCurrentValue(ContentProperty, DefaultContent.ToString(CultureInfo.CurrentCulture));
}
/// <summary>

4
src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs

@ -26,8 +26,8 @@ namespace Avalonia.Controls
[PseudoClasses(pcFlyoutOpen, pcPressed)]
public partial class CalendarDatePicker : TemplatedControl
{
protected const string pcPressed = ":pressed";
protected const string pcFlyoutOpen = ":flyout-open";
private const string pcPressed = ":pressed";
private const string pcFlyoutOpen = ":flyout-open";
private const string ElementTextBox = "PART_TextBox";
private const string ElementButton = "PART_Button";

20
src/Avalonia.Controls/ComboBox.cs

@ -21,8 +21,9 @@ namespace Avalonia.Controls
[PseudoClasses(pcDropdownOpen, pcPressed)]
public class ComboBox : SelectingItemsControl
{
public const string pcDropdownOpen = ":dropdownopen";
public const string pcPressed = ":pressed";
internal const string pcDropdownOpen = ":dropdownopen";
internal const string pcPressed = ":pressed";
/// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
/// </summary>
@ -164,7 +165,7 @@ namespace Avalonia.Controls
UpdateSelectionBoxItem(SelectedItem);
}
public override void InvalidateMirrorTransform()
protected internal override void InvalidateMirrorTransform()
{
base.InvalidateMirrorTransform();
UpdateFlowDirection();
@ -360,19 +361,6 @@ namespace Avalonia.Controls
_subscriptionsOnOpen.Clear();
var toplevel = TopLevel.GetTopLevel(this);
if (toplevel != null)
{
toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) =>
{
//eat wheel scroll event outside dropdown popup while it's open
if (IsDropDownOpen && (ev.Source as Visual)?.GetVisualRoot() == toplevel)
{
ev.Handled = true;
}
}, Interactivity.RoutingStrategies.Tunnel).DisposeWith(_subscriptionsOnOpen);
}
this.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
foreach (var parent in this.GetVisualAncestors().OfType<Control>())

21
src/Avalonia.Controls/ContentControl.cs

@ -13,7 +13,7 @@ namespace Avalonia.Controls
/// <summary>
/// Displays <see cref="Content"/> according to an <see cref="IDataTemplate"/>.
/// </summary>
[TemplatePart("PART_ContentPresenter", typeof(IContentPresenter))]
[TemplatePart("PART_ContentPresenter", typeof(ContentPresenter))]
public class ContentControl : TemplatedControl, IContentControl, IContentPresenterHost
{
/// <summary>
@ -68,7 +68,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets the presenter from the control's template.
/// </summary>
public IContentPresenter? Presenter
public ContentPresenter? Presenter
{
get;
private set;
@ -96,16 +96,16 @@ namespace Avalonia.Controls
IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
/// <inheritdoc/>
bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
bool IContentPresenterHost.RegisterContentPresenter(ContentPresenter presenter)
{
return RegisterContentPresenter(presenter);
}
/// <summary>
/// Called when an <see cref="IContentPresenter"/> is registered with the control.
/// Called when an <see cref="ContentPresenter"/> is registered with the control.
/// </summary>
/// <param name="presenter">The presenter.</param>
protected virtual bool RegisterContentPresenter(IContentPresenter presenter)
protected virtual bool RegisterContentPresenter(ContentPresenter presenter)
{
if (presenter.Name == "PART_ContentPresenter")
{
@ -116,19 +116,14 @@ namespace Avalonia.Controls
return false;
}
protected virtual void ContentChanged(AvaloniaPropertyChangedEventArgs e)
private void ContentChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdateLogicalTree(e.OldValue, e.NewValue);
}
protected void UpdateLogicalTree(object? toRemove, object? toAdd)
{
if (toRemove is ILogical oldChild)
if (e.OldValue is ILogical oldChild)
{
LogicalChildren.Remove(oldChild);
}
if (toAdd is ILogical newChild)
if (e.NewValue is ILogical newChild)
{
LogicalChildren.Add(newChild);
}

6
src/Avalonia.Controls/Control.cs

@ -440,6 +440,12 @@ namespace Avalonia.Controls
return new NoneAutomationPeer(this);
}
internal AutomationPeer? GetAutomationPeer()
{
VerifyAccess();
return _automationPeer;
}
internal AutomationPeer GetOrCreateAutomationPeer()
{
VerifyAccess();

3
src/Avalonia.Controls/GridSplitter.cs

@ -695,7 +695,8 @@ namespace Avalonia.Controls
{
private readonly TranslateTransform _translation;
private readonly Decorator _decorator;
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", Justification = "Private object")]
public PreviewAdorner(Control? previewControl)
{
// Add a decorator to perform translations.

3
src/Avalonia.Controls/IContentControl.cs

@ -8,8 +8,7 @@ namespace Avalonia.Controls
/// Defines a control that displays <see cref="Content"/> according to a
/// <see cref="Avalonia.Controls.Templates.FuncDataTemplate"/>.
/// </summary>
[NotClientImplementable]
public interface IContentControl
internal interface IContentControl
{
/// <summary>
/// Gets or sets the content to display.

2
src/Avalonia.Controls/IHeadered.cs

@ -3,7 +3,7 @@ namespace Avalonia.Controls
/// <summary>
/// Defines a headered object.
/// </summary>
public interface IHeadered
internal interface IHeadered
{
/// <summary>
/// Gets or set the header.

3
src/Avalonia.Controls/IMenu.cs

@ -8,8 +8,7 @@ namespace Avalonia.Controls
/// <summary>
/// Represents a <see cref="Menu"/> or <see cref="ContextMenu"/>.
/// </summary>
[NotClientImplementable]
public interface IMenu : IMenuElement, IInputElement
internal interface IMenu : IMenuElement, IInputElement
{
/// <summary>
/// Gets the menu interaction handler.

3
src/Avalonia.Controls/IMenuElement.cs

@ -8,8 +8,7 @@ namespace Avalonia.Controls
/// <summary>
/// Represents an <see cref="IMenu"/> or <see cref="IMenuItem"/>.
/// </summary>
[NotClientImplementable]
public interface IMenuElement : IInputElement, ILogical
internal interface IMenuElement : IInputElement, ILogical
{
/// <summary>
/// Gets or sets the currently selected submenu item.

3
src/Avalonia.Controls/IMenuItem.cs

@ -5,8 +5,7 @@ namespace Avalonia.Controls
/// <summary>
/// Represents a <see cref="MenuItem"/>.
/// </summary>
[NotClientImplementable]
public interface IMenuItem : IMenuElement
internal interface IMenuItem : IMenuElement
{
/// <summary>
/// Gets or sets a value that indicates whether the item has a submenu.

2
src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs

@ -2,7 +2,7 @@ using Avalonia.Metadata;
namespace Avalonia.Controls
{
[Unstable]
[PrivateApi]
public interface INativeMenuExporterEventsImplBridge
{
void RaiseNeedsUpdate ();

24
src/Avalonia.Controls/ItemsControl.cs

@ -365,6 +365,30 @@ namespace Avalonia.Controls
/// </summary>
public IEnumerable<Control> GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty<Control>();
/// <summary>
/// Returns the <see cref="ItemsControl"/> that owns the specified container control.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>
/// The owning <see cref="ItemsControl"/> or null if the control is not an items container.
/// </returns>
public static ItemsControl? ItemsControlFromItemContaner(Control container)
{
var c = container.Parent as Control;
while (c is not null)
{
if (c is ItemsControl itemsControl)
{
return itemsControl.IndexFromContainer(container) >= 0 ? itemsControl : null;
}
c = c.Parent as Control;
}
return null;
}
/// <summary>
/// Creates or a container that can be used to display an item.
/// </summary>

70
src/Avalonia.Controls/ListBox.cs

@ -1,4 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection;
@ -47,7 +51,7 @@ namespace Avalonia.Controls
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1010",
Justification = "This property is owned by SelectingItemsControl, but protected there. ListBox changes its visibility.")]
public static readonly new StyledProperty<SelectionMode> SelectionModeProperty =
public static readonly new StyledProperty<SelectionMode> SelectionModeProperty =
SelectingItemsControl.SelectionModeProperty;
private IScrollable? _scroll;
@ -121,41 +125,36 @@ namespace Avalonia.Controls
return NeedsContainer<ListBoxItem>(item, out recycleKey);
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnGotFocus(e);
var hotkeys = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
var ctrl = hotkeys is not null && e.KeyModifiers.HasAllFlags(hotkeys.CommandModifiers);
if (e.NavigationMethod == NavigationMethod.Directional)
if (!ctrl &&
e.Key.ToNavigationDirection() is { } direction &&
direction.IsDirectional())
{
e.Handled |= MoveSelection(
direction,
WrapSelection,
e.KeyModifiers.HasAllFlags(KeyModifiers.Shift));
}
else if (SelectionMode.HasAllFlags(SelectionMode.Multiple) &&
hotkeys is not null && hotkeys.SelectAll.Any(x => x.Matches(e)))
{
e.Handled = UpdateSelectionFromEventSource(
Selection.SelectAll();
e.Handled = true;
}
else if (e.Key == Key.Space || e.Key == Key.Enter)
{
e.Handled |= UpdateSelectionFromEventSource(
e.Source,
true,
e.KeyModifiers.HasAllFlags(KeyModifiers.Shift),
e.KeyModifiers.HasAllFlags(KeyModifiers.Control),
fromFocus: true);
e.KeyModifiers.HasFlag(KeyModifiers.Shift),
ctrl);
}
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (e.Source is Visual source)
{
var point = e.GetCurrentPoint(source);
if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed)
{
e.Handled = UpdateSelectionFromEventSource(
e.Source,
true,
e.KeyModifiers.HasAllFlags(KeyModifiers.Shift),
e.KeyModifiers.HasAllFlags(AvaloniaLocator.Current.GetRequiredService<PlatformHotkeyConfiguration>().CommandModifiers),
point.Properties.IsRightButtonPressed);
}
}
base.OnKeyDown(e);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@ -163,5 +162,18 @@ namespace Avalonia.Controls
base.OnApplyTemplate(e);
Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
}
internal bool UpdateSelectionFromPointerEvent(Control source, PointerEventArgs e)
{
var hotkeys = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
var toggle = hotkeys is not null && e.KeyModifiers.HasAllFlags(hotkeys.CommandModifiers);
return UpdateSelectionFromEventSource(
source,
true,
e.KeyModifiers.HasAllFlags(KeyModifiers.Shift),
toggle,
e.GetCurrentPoint(source).Properties.IsRightButtonPressed);
}
}
}

67
src/Avalonia.Controls/ListBoxItem.cs

@ -2,6 +2,8 @@ using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Platform;
namespace Avalonia.Controls
{
@ -17,6 +19,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> IsSelectedProperty =
SelectingItemsControl.IsSelectedProperty.AddOwner<ListBoxItem>();
private static readonly Point s_invalidPoint = new Point(double.NaN, double.NaN);
private Point _pointerDownPoint = s_invalidPoint;
/// <summary>
/// Initializes static members of the <see cref="ListBoxItem"/> class.
/// </summary>
@ -40,5 +45,67 @@ namespace Avalonia.Controls
{
return new ListItemAutomationPeer(this);
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
_pointerDownPoint = s_invalidPoint;
if (e.Handled)
return;
if (!e.Handled && ItemsControl.ItemsControlFromItemContaner(this) is ListBox owner)
{
var p = e.GetCurrentPoint(this);
if (p.Properties.PointerUpdateKind is PointerUpdateKind.LeftButtonPressed or
PointerUpdateKind.RightButtonPressed)
{
if (p.Pointer.Type == PointerType.Mouse)
{
// If the pressed point comes from a mouse, perform the selection immediately.
e.Handled = owner.UpdateSelectionFromPointerEvent(this, e);
}
else
{
// Otherwise perform the selection when the pointer is released as to not
// interfere with gestures.
_pointerDownPoint = p.Position;
// Ideally we'd set handled here, but that would prevent the scroll gesture
// recognizer from working.
////e.Handled = true;
}
}
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (!e.Handled &&
!double.IsNaN(_pointerDownPoint.X) &&
e.InitialPressMouseButton is MouseButton.Left or MouseButton.Right)
{
var point = e.GetCurrentPoint(this);
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
var tapSize = settings?.GetTapSize(point.Pointer.Type) ?? new Size(4, 4);
var tapRect = new Rect(_pointerDownPoint, new Size())
.Inflate(new Thickness(tapSize.Width, tapSize.Height));
if (new Rect(Bounds.Size).ContainsExclusive(point.Position) &&
tapRect.ContainsExclusive(point.Position) &&
ItemsControl.ItemsControlFromItemContaner(this) is ListBox owner)
{
if (owner.UpdateSelectionFromPointerEvent(this, e))
e.Handled = true;
}
}
_pointerDownPoint = s_invalidPoint;
}
}
}

2
src/Avalonia.Controls/Menu.cs

@ -87,7 +87,7 @@ namespace Avalonia.Controls
{
base.OnAttachedToVisualTree(e);
var inputRoot = e.Root as IInputRoot;
var inputRoot = e.Root as TopLevel;
if (inputRoot?.AccessKeyHandler != null)
{

2
src/Avalonia.Controls/MenuItemAccessKeyHandler.cs

@ -9,7 +9,7 @@ namespace Avalonia.Controls
/// <summary>
/// Handles access keys within a <see cref="MenuItem"/>
/// </summary>
public class MenuItemAccessKeyHandler : IAccessKeyHandler
internal class MenuItemAccessKeyHandler : IAccessKeyHandler
{
/// <summary>
/// The registered access keys.

2
src/Avalonia.Controls/NativeMenuItemSeparator.cs

@ -4,7 +4,7 @@
{
public NativeMenuItemSeparator()
{
Header = "-";
SetCurrentValue(HeaderProperty, "-");
}
}
}

2
src/Avalonia.Controls/Panel.cs

@ -213,7 +213,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc />
public bool TryGetTotalCount(out int count)
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
count = Children.Count;
return true;

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

@ -39,91 +39,20 @@ namespace Avalonia.Controls.Platform
DelayRun = delayRun;
}
public virtual void Attach(IMenu menu)
{
if (Menu != null)
{
throw new NotSupportedException("DefaultMenuInteractionHandler is already attached.");
}
Menu = menu;
Menu.GotFocus += GotFocus;
Menu.LostFocus += LostFocus;
Menu.KeyDown += KeyDown;
Menu.PointerPressed += PointerPressed;
Menu.PointerReleased += PointerReleased;
Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
Menu.AddHandler(MenuBase.MenuOpenedEvent, MenuOpened);
Menu.AddHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
Menu.AddHandler(MenuItem.PointerExitedItemEvent, PointerExited);
Menu.AddHandler(InputElement.PointerMovedEvent, PointerMoved);
_root = Menu.VisualRoot;
if (_root is InputElement inputRoot)
{
inputRoot.AddHandler(InputElement.PointerPressedEvent, RootPointerPressed, RoutingStrategies.Tunnel);
}
if (_root is WindowBase window)
{
window.Deactivated += WindowDeactivated;
}
if (_root is TopLevel tl && tl.PlatformImpl is ITopLevelImpl pimpl)
pimpl.LostFocus += TopLevelLostPlatformFocus;
_inputManagerSubscription = InputManager?.Process.Subscribe(RawInput);
}
public virtual void Detach(IMenu menu)
{
if (Menu != menu)
{
throw new NotSupportedException("DefaultMenuInteractionHandler is not attached to the menu.");
}
Menu.GotFocus -= GotFocus;
Menu.LostFocus -= LostFocus;
Menu.KeyDown -= KeyDown;
Menu.PointerPressed -= PointerPressed;
Menu.PointerReleased -= PointerReleased;
Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
Menu.RemoveHandler(MenuBase.MenuOpenedEvent, MenuOpened);
Menu.RemoveHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
Menu.RemoveHandler(MenuItem.PointerExitedItemEvent, PointerExited);
Menu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved);
if (_root is InputElement inputRoot)
{
inputRoot.RemoveHandler(InputElement.PointerPressedEvent, RootPointerPressed);
}
if (_root is WindowBase root)
{
root.Deactivated -= WindowDeactivated;
}
if (_root is TopLevel tl && tl.PlatformImpl != null)
tl.PlatformImpl.LostFocus -= TopLevelLostPlatformFocus;
_inputManagerSubscription?.Dispose();
Menu = null;
_root = null;
}
public void Attach(MenuBase menu) => AttachCore(menu);
public void Detach(MenuBase menu) => DetachCore(menu);
protected Action<Action, TimeSpan> DelayRun { get; }
protected IInputManager? InputManager { get; }
protected IMenu? Menu { get; private set; }
internal IMenu? Menu { get; private set; }
protected static TimeSpan MenuShowDelay { get; } = TimeSpan.FromMilliseconds(400);
protected internal virtual void GotFocus(object? sender, GotFocusEventArgs e)
{
var item = GetMenuItem(e.Source as Control);
var item = GetMenuItemCore(e.Source as Control);
if (item?.Parent != null)
{
@ -133,7 +62,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void LostFocus(object? sender, RoutedEventArgs e)
{
var item = GetMenuItem(e.Source as Control);
var item = GetMenuItemCore(e.Source as Control);
if (item != null)
{
@ -143,146 +72,12 @@ namespace Avalonia.Controls.Platform
protected internal virtual void KeyDown(object? sender, KeyEventArgs e)
{
KeyDown(GetMenuItem(e.Source as Control), e);
}
protected internal virtual void KeyDown(IMenuItem? item, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Up:
case Key.Down:
{
if (item?.IsTopLevel == true && item.HasSubMenu)
{
if (!item.IsSubMenuOpen)
{
Open(item, true);
}
else
{
item.MoveSelection(NavigationDirection.First, true);
}
e.Handled = true;
}
else
{
goto default;
}
break;
}
case Key.Left:
{
if (item is { IsSubMenuOpen: true, SelectedItem: null })
{
item.Close();
}
else if (item?.Parent is IMenuItem { IsTopLevel: false, IsSubMenuOpen: true } parent)
{
parent.Close();
parent.Focus();
e.Handled = true;
}
else
{
goto default;
}
break;
}
case Key.Right:
{
if (item != null && !item.IsTopLevel && item.HasSubMenu)
{
Open(item, true);
e.Handled = true;
}
else
{
goto default;
}
break;
}
case Key.Enter:
{
if (item != null)
{
if (!item.HasSubMenu)
{
Click(item);
}
else
{
Open(item, true);
}
e.Handled = true;
}
break;
}
case Key.Escape:
{
if (item?.Parent is IMenuElement parent)
{
parent.Close();
parent.Focus();
}
else
{
Menu!.Close();
}
e.Handled = true;
break;
}
default:
{
var direction = e.Key.ToNavigationDirection();
if (direction?.IsDirectional() == true)
{
if (item == null && _isContextMenu)
{
if (Menu!.MoveSelection(direction.Value, true) == true)
{
e.Handled = true;
}
}
else if (item?.Parent?.MoveSelection(direction.Value, true) == true)
{
// If the the parent is an IMenu which successfully moved its selection,
// and the current menu is open then close the current menu and open the
// new menu.
if (item.IsSubMenuOpen &&
item.Parent is IMenu &&
item.Parent.SelectedItem is object &&
item.Parent.SelectedItem != item)
{
item.Close();
Open(item.Parent.SelectedItem, true);
}
e.Handled = true;
}
}
break;
}
}
if (!e.Handled && item?.Parent is IMenuItem parentItem)
{
KeyDown(parentItem, e);
}
KeyDown(GetMenuItemCore(e.Source as Control), e);
}
protected internal virtual void AccessKeyPressed(object? sender, RoutedEventArgs e)
{
var item = GetMenuItem(e.Source as Control);
var item = GetMenuItemCore(e.Source as Control);
if (item == null)
{
@ -303,7 +98,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerEntered(object? sender, RoutedEventArgs e)
{
var item = GetMenuItem(e.Source as Control);
var item = GetMenuItemCore(e.Source as Control);
if (item?.Parent == null)
{
@ -349,7 +144,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerMoved(object? sender, PointerEventArgs e)
{
// HACK: #8179 needs to be addressed to correctly implement it in the PointerPressed method.
var item = GetMenuItem(e.Source as Control) as MenuItem;
var item = GetMenuItemCore(e.Source as Control) as MenuItem;
if (item == null)
return;
@ -370,7 +165,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerExited(object? sender, RoutedEventArgs e)
{
var item = GetMenuItem(e.Source as Control);
var item = GetMenuItemCore(e.Source as Control);
if (item?.Parent == null)
{
@ -405,7 +200,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerPressed(object? sender, PointerPressedEventArgs e)
{
var item = GetMenuItem(e.Source as Control);
var item = GetMenuItemCore(e.Source as Control);
if (sender is Visual visual &&
e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed && item?.HasSubMenu == true)
@ -436,7 +231,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerReleased(object? sender, PointerReleasedEventArgs e)
{
var item = GetMenuItem(e.Source as Control);
var item = GetMenuItemCore(e.Source as Control);
if (e.InitialPressMouseButton == MouseButton.Left && item?.HasSubMenu == false)
{
@ -478,13 +273,84 @@ namespace Avalonia.Controls.Platform
{
Menu?.Close();
}
private void TopLevelLostPlatformFocus()
internal static MenuItem? GetMenuItem(StyledElement? item) => (MenuItem?)GetMenuItemCore(item);
internal void AttachCore(IMenu menu)
{
Menu?.Close();
if (Menu != null)
{
throw new NotSupportedException("DefaultMenuInteractionHandler is already attached.");
}
Menu = menu;
Menu.GotFocus += GotFocus;
Menu.LostFocus += LostFocus;
Menu.KeyDown += KeyDown;
Menu.PointerPressed += PointerPressed;
Menu.PointerReleased += PointerReleased;
Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
Menu.AddHandler(MenuBase.MenuOpenedEvent, MenuOpened);
Menu.AddHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
Menu.AddHandler(MenuItem.PointerExitedItemEvent, PointerExited);
Menu.AddHandler(InputElement.PointerMovedEvent, PointerMoved);
_root = Menu.VisualRoot;
if (_root is InputElement inputRoot)
{
inputRoot.AddHandler(InputElement.PointerPressedEvent, RootPointerPressed, RoutingStrategies.Tunnel);
}
if (_root is WindowBase window)
{
window.Deactivated += WindowDeactivated;
}
if (_root is TopLevel tl && tl.PlatformImpl is ITopLevelImpl pimpl)
pimpl.LostFocus += TopLevelLostPlatformFocus;
_inputManagerSubscription = InputManager?.Process.Subscribe(RawInput);
}
protected void Click(IMenuItem item)
internal void DetachCore(IMenu menu)
{
if (Menu != menu)
{
throw new NotSupportedException("DefaultMenuInteractionHandler is not attached to the menu.");
}
Menu.GotFocus -= GotFocus;
Menu.LostFocus -= LostFocus;
Menu.KeyDown -= KeyDown;
Menu.PointerPressed -= PointerPressed;
Menu.PointerReleased -= PointerReleased;
Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
Menu.RemoveHandler(MenuBase.MenuOpenedEvent, MenuOpened);
Menu.RemoveHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
Menu.RemoveHandler(MenuItem.PointerExitedItemEvent, PointerExited);
Menu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved);
if (_root is InputElement inputRoot)
{
inputRoot.RemoveHandler(InputElement.PointerPressedEvent, RootPointerPressed);
}
if (_root is WindowBase root)
{
root.Deactivated -= WindowDeactivated;
}
if (_root is TopLevel tl && tl.PlatformImpl != null)
tl.PlatformImpl.LostFocus -= TopLevelLostPlatformFocus;
_inputManagerSubscription?.Dispose();
Menu = null;
_root = null;
}
internal void Click(IMenuItem item)
{
item.RaiseClick();
@ -494,7 +360,7 @@ namespace Avalonia.Controls.Platform
}
}
protected void CloseMenu(IMenuItem item)
internal void CloseMenu(IMenuItem item)
{
var current = (IMenuElement?)item;
@ -506,7 +372,7 @@ namespace Avalonia.Controls.Platform
current?.Close();
}
protected void CloseWithDelay(IMenuItem item)
internal void CloseWithDelay(IMenuItem item)
{
void Execute()
{
@ -519,7 +385,141 @@ namespace Avalonia.Controls.Platform
DelayRun(Execute, MenuShowDelay);
}
protected void Open(IMenuItem item, bool selectFirst)
internal void KeyDown(IMenuItem? item, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Up:
case Key.Down:
{
if (item?.IsTopLevel == true && item.HasSubMenu)
{
if (!item.IsSubMenuOpen)
{
Open(item, true);
}
else
{
item.MoveSelection(NavigationDirection.First, true);
}
e.Handled = true;
}
else
{
goto default;
}
break;
}
case Key.Left:
{
if (item is { IsSubMenuOpen: true, SelectedItem: null })
{
item.Close();
}
else if (item?.Parent is IMenuItem { IsTopLevel: false, IsSubMenuOpen: true } parent)
{
parent.Close();
parent.Focus();
e.Handled = true;
}
else
{
goto default;
}
break;
}
case Key.Right:
{
if (item != null && !item.IsTopLevel && item.HasSubMenu)
{
Open(item, true);
e.Handled = true;
}
else
{
goto default;
}
break;
}
case Key.Enter:
{
if (item != null)
{
if (!item.HasSubMenu)
{
Click(item);
}
else
{
Open(item, true);
}
e.Handled = true;
}
break;
}
case Key.Escape:
{
if (item?.Parent is IMenuElement parent)
{
parent.Close();
parent.Focus();
}
else
{
Menu!.Close();
}
e.Handled = true;
break;
}
default:
{
var direction = e.Key.ToNavigationDirection();
if (direction?.IsDirectional() == true)
{
if (item == null && _isContextMenu)
{
if (Menu!.MoveSelection(direction.Value, true) == true)
{
e.Handled = true;
}
}
else if (item?.Parent?.MoveSelection(direction.Value, true) == true)
{
// If the the parent is an IMenu which successfully moved its selection,
// and the current menu is open then close the current menu and open the
// new menu.
if (item.IsSubMenuOpen &&
item.Parent is IMenu &&
item.Parent.SelectedItem is object &&
item.Parent.SelectedItem != item)
{
item.Close();
Open(item.Parent.SelectedItem, true);
}
e.Handled = true;
}
}
break;
}
}
if (!e.Handled && item?.Parent is IMenuItem parentItem)
{
KeyDown(parentItem, e);
}
}
internal void Open(IMenuItem item, bool selectFirst)
{
item.Open();
@ -529,7 +529,7 @@ namespace Avalonia.Controls.Platform
}
}
protected void OpenWithDelay(IMenuItem item)
internal void OpenWithDelay(IMenuItem item)
{
void Execute()
{
@ -542,7 +542,7 @@ namespace Avalonia.Controls.Platform
DelayRun(Execute, MenuShowDelay);
}
protected void SelectItemAndAncestors(IMenuItem item)
internal void SelectItemAndAncestors(IMenuItem item)
{
var current = (IMenuItem?)item;
@ -553,7 +553,7 @@ namespace Avalonia.Controls.Platform
}
}
protected static IMenuItem? GetMenuItem(StyledElement? item)
internal static IMenuItem? GetMenuItemCore(StyledElement? item)
{
while (true)
{
@ -565,6 +565,11 @@ namespace Avalonia.Controls.Platform
}
}
private void TopLevelLostPlatformFocus()
{
Menu?.Close();
}
private static void DefaultDelayRun(Action action, TimeSpan timeSpan)
{
DispatcherTimer.RunOnce(action, timeSpan);

58
src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs

@ -1,58 +0,0 @@
using System;
namespace Avalonia.Platform
{
/// <summary>
/// Defines an "Avalonia Module", a 3rd party extension to Avalonia that can be automatically initialized by an AppBuilder instance.
/// </summary>
/// <remarks>
/// Avalonia Modules can either be platform independent (ex default control styles provider) or dependent on a
/// specific windowing or rendering subsystem being used (ex native rendering speedup, subsystem-specific interop backends).
/// In the case of a subsystem-specific module, you can specify multiple module implementations, and also a fallback
/// platform-independent module if you so choose. Additionally, these different implementations can be in different assemblies.
/// They just need to all share the same module name.
///
/// For example, if I had a module Foo that has a special back-end for Skia and a less performant/less user friendly back-end for
/// any other rendering subsystem, I would do the following:
/// <code>
/// // In assembly FooModuleSkia.dll
/// [assembly:ExportAvaloniaModule("Foo", typeof(FooModuleSkia), ForRenderingSubsystem="Skia")]
///
/// class FooModuleSkia
/// {
/// public FooModuleSkia()
/// {
/// InitializeModule();
/// }
/// }
///
/// // In assembly FooModuleFallback.dll
/// [assembly:ExportAvaloniaModule("Foo", typeof(FooModuleFallback))]
///
/// class FooModuleFallback
/// {
/// public FooModuleFallback()
/// {
/// InitializeModule();
/// }
/// }
///
/// </code>
/// The fallback module will only be initialized if the Skia-specific module is not applicable.
/// </remarks>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class ExportAvaloniaModuleAttribute : Attribute
{
public ExportAvaloniaModuleAttribute(string name, Type moduleType)
{
Name = name;
ModuleType = moduleType;
}
public string Name { get; private set; }
public Type ModuleType { get; private set; }
public string ForWindowingSubsystem { get; set; } = "";
public string ForRenderingSubsystem { get; set; } = "";
}
}

4
src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs

@ -12,11 +12,11 @@ namespace Avalonia.Controls.Platform
/// Attaches the interaction handler to a menu.
/// </summary>
/// <param name="menu">The menu.</param>
void Attach(IMenu menu);
void Attach(MenuBase menu);
/// <summary>
/// Detaches the interaction handler from the attached menu.
/// </summary>
void Detach(IMenu menu);
void Detach(MenuBase menu);
}
}

13
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -17,7 +17,7 @@ namespace Avalonia.Controls.Presenters
/// Presents a single item of data inside a <see cref="TemplatedControl"/> template.
/// </summary>
[PseudoClasses(":empty")]
public class ContentPresenter : Control, IContentPresenter
public class ContentPresenter : Control
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -540,17 +540,6 @@ namespace Avalonia.Controls.Presenters
BoxShadow);
}
/// <summary>
/// Creates the child control.
/// </summary>
/// <returns>The child control or null.</returns>
protected virtual Control? CreateChild()
{
var content = Content;
var oldChild = Child;
return CreateChild(content, oldChild, ContentTemplate);
}
private Control? CreateChild(object? content, Control? oldChild, IDataTemplate? template)
{
var newChild = content as Control;

23
src/Avalonia.Controls/Presenters/IContentPresenter.cs

@ -1,23 +0,0 @@
using Avalonia.Controls.Primitives;
using Avalonia.Metadata;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Interface for controls that present a single item of data inside a
/// <see cref="TemplatedControl"/> template.
/// </summary>
[NotClientImplementable]
public interface IContentPresenter : IPresenter
{
/// <summary>
/// Gets the control displayed by the presenter.
/// </summary>
Control? Child { get; }
/// <summary>
/// Gets or sets the content to be displayed by the presenter.
/// </summary>
object? Content { get; set; }
}
}

9
src/Avalonia.Controls/Presenters/IContentPresenterHost.cs

@ -1,7 +1,5 @@
using Avalonia.Collections;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Controls.Presenters
{
@ -16,8 +14,7 @@ namespace Avalonia.Controls.Presenters
/// parent control's template is instantiated so they register themselves using this
/// interface.
/// </remarks>
[NotClientImplementable]
public interface IContentPresenterHost
internal interface IContentPresenterHost
{
/// <summary>
/// Gets a collection describing the logical children of the host control.
@ -25,13 +22,13 @@ namespace Avalonia.Controls.Presenters
IAvaloniaList<ILogical> LogicalChildren { get; }
/// <summary>
/// Registers an <see cref="IContentPresenter"/> with a host control.
/// Registers an <see cref="ContentPresenter"/> with a host control.
/// </summary>
/// <param name="presenter">The content presenter.</param>
/// <returns>
/// True if the content presenter should add its child to the logical children of the
/// host; otherwise false.
/// </returns>
bool RegisterContentPresenter(IContentPresenter presenter);
bool RegisterContentPresenter(ContentPresenter presenter);
}
}

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

@ -1,20 +0,0 @@
using Avalonia.Controls.Primitives;
using Avalonia.Metadata;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Interface for presenters such as <see cref="ContentPresenter"/> and
/// <see cref="ItemsPresenter"/>.
/// </summary>
/// <remarks>
/// A presenter is the gateway between a templated control and its content. When
/// a control which implements <see cref="IPresenter"/> is found in the template
/// of a <see cref="TemplatedControl"/> then that signals that the visual child
/// of the presenter is not a part of the template.
/// </remarks>
[NotClientImplementable]
public interface IPresenter : INamed
{
}
}

2
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -13,7 +13,7 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Presents a scrolling view of content inside a <see cref="ScrollViewer"/>.
/// </summary>
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
public class ScrollContentPresenter : ContentPresenter, IScrollable, IScrollAnchorProvider
{
private const double EdgeDetectionTolerance = 0.1;

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

@ -86,7 +86,7 @@ namespace Avalonia.Controls.Primitives
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_accessKeys = (e.Root as IInputRoot)?.AccessKeyHandler;
_accessKeys = (e.Root as TopLevel)?.AccessKeyHandler;
if (_accessKeys != null && AccessKey != 0)
{

4
src/Avalonia.Controls/Primitives/HeaderedContentControl.cs

@ -41,7 +41,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Gets the header presenter from the control's template.
/// </summary>
public IContentPresenter? HeaderPresenter
public ContentPresenter? HeaderPresenter
{
get;
private set;
@ -57,7 +57,7 @@ namespace Avalonia.Controls.Primitives
}
/// <inheritdoc/>
protected override bool RegisterContentPresenter(IContentPresenter presenter)
protected override bool RegisterContentPresenter(ContentPresenter presenter)
{
var result = base.RegisterContentPresenter(presenter);

8
src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs

@ -56,7 +56,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Gets the header presenter from the control's template.
/// </summary>
public IContentPresenter? HeaderPresenter
public ContentPresenter? HeaderPresenter
{
get;
private set;
@ -77,16 +77,16 @@ namespace Avalonia.Controls.Primitives
}
/// <inheritdoc/>
bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
bool IContentPresenterHost.RegisterContentPresenter(ContentPresenter presenter)
{
return RegisterContentPresenter(presenter);
}
/// <summary>
/// Called when an <see cref="IContentPresenter"/> is registered with the control.
/// Called when an <see cref="ContentPresenter"/> is registered with the control.
/// </summary>
/// <param name="presenter">The presenter.</param>
protected virtual bool RegisterContentPresenter(IContentPresenter presenter)
protected virtual bool RegisterContentPresenter(ContentPresenter presenter)
{
if (presenter.Name == "PART_HeaderPresenter")
{

8
src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs

@ -56,7 +56,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Gets the header presenter from the control's template.
/// </summary>
public IContentPresenter? HeaderPresenter
public ContentPresenter? HeaderPresenter
{
get;
private set;
@ -66,7 +66,7 @@ namespace Avalonia.Controls.Primitives
IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
/// <inheritdoc/>
bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
bool IContentPresenterHost.RegisterContentPresenter(ContentPresenter presenter)
{
return RegisterContentPresenter(presenter);
}
@ -83,10 +83,10 @@ namespace Avalonia.Controls.Primitives
}
/// <summary>
/// Called when an <see cref="IContentPresenter"/> is registered with the control.
/// Called when an <see cref="ContentPresenter"/> is registered with the control.
/// </summary>
/// <param name="presenter">The presenter.</param>
protected virtual bool RegisterContentPresenter(IContentPresenter presenter)
protected virtual bool RegisterContentPresenter(ContentPresenter presenter)
{
if (presenter.Name == "PART_HeaderPresenter")
{

2
src/Avalonia.Controls/Primitives/IPopupHost.cs

@ -52,7 +52,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Gets the presenter from the control's template.
/// </summary>
IContentPresenter? Presenter { get; }
ContentPresenter? Presenter { get; }
/// <summary>
/// Gets or sets whether the popup appears on top of all other windows.

1
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

@ -29,6 +29,7 @@ namespace Avalonia.Controls.Primitives
}
/// <inheritdoc />
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", Justification = "Explicit set")]
public void SetChild(Control? control)
{
Content = control;

37
src/Avalonia.Controls/Primitives/RangeBase.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Utilities;
namespace Avalonia.Controls.Primitives
@ -42,7 +43,23 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<RangeBase, double>(nameof(LargeChange), 10);
/// <summary>
/// Gets or sets the minimum value.
/// Defines the <see cref="ValueChanged"/> event.
/// </summary>
public static readonly RoutedEvent<RangeBaseValueChangedEventArgs> ValueChangedEvent =
RoutedEvent.Register<RangeBase, RangeBaseValueChangedEventArgs>(
nameof(ValueChanged), RoutingStrategies.Bubble);
/// <summary>
/// Occurs when the <see cref="Value"/> property changes.
/// </summary>
public event EventHandler<RangeBaseValueChangedEventArgs>? ValueChanged
{
add => AddHandler(ValueChangedEvent, value);
remove => RemoveHandler(ValueChangedEvent, value);
}
/// <summary>
/// Gets or sets the minimum possible value.
/// </summary>
public double Minimum
{
@ -65,7 +82,7 @@ namespace Avalonia.Controls.Primitives
}
/// <summary>
/// Gets or sets the maximum value.
/// Gets or sets the maximum possible value.
/// </summary>
public double Maximum
{
@ -104,18 +121,25 @@ namespace Avalonia.Controls.Primitives
: sender.GetValue(ValueProperty);
}
/// <summary>
/// Gets or sets the small increment value added or subtracted from the <see cref="Value"/>.
/// </summary>
public double SmallChange
{
get => GetValue(SmallChangeProperty);
set => SetValue(SmallChangeProperty, value);
}
/// <summary>
/// Gets or sets the large increment value added or subtracted from the <see cref="Value"/>.
/// </summary>
public double LargeChange
{
get => GetValue(LargeChangeProperty);
set => SetValue(LargeChangeProperty, value);
}
/// <inheritdoc/>
protected override void OnInitialized()
{
base.OnInitialized();
@ -124,6 +148,7 @@ namespace Avalonia.Controls.Primitives
CoerceValue(ValueProperty);
}
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
@ -136,6 +161,14 @@ namespace Avalonia.Controls.Primitives
{
OnMaximumChanged();
}
else if (change.Property == ValueProperty)
{
var valueChangedEventArgs = new RangeBaseValueChangedEventArgs(
change.GetOldValue<double>(),
change.GetNewValue<double>(),
ValueChangedEvent);
RaiseEvent(valueChangedEventArgs);
}
}
/// <summary>

47
src/Avalonia.Controls/Primitives/RangeBaseValueChangedEventArgs.cs

@ -0,0 +1,47 @@
using Avalonia.Interactivity;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Provides data specific to a <see cref="RangeBase.ValueChanged"/> event.
/// </summary>
public class RangeBaseValueChangedEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="RangeBaseValueChangedEventArgs"/> class.
/// </summary>
/// <param name="oldValue">The old value of the range value property.</param>
/// <param name="newValue">The new value of the range value property.</param>
/// <param name="routedEvent">The routed event associated with these event args.</param>
public RangeBaseValueChangedEventArgs(double oldValue, double newValue, RoutedEvent? routedEvent)
: base(routedEvent)
{
OldValue = oldValue;
NewValue = newValue;
}
/// <summary>
/// Initializes a new instance of the <see cref="RangeBaseValueChangedEventArgs"/> class.
/// </summary>
/// <param name="oldValue">The old value of the range value property.</param>
/// <param name="newValue">The new value of the range value property.</param>
/// <param name="routedEvent">The routed event associated with these event args.</param>
/// <param name="source">The source object that raised the routed event.</param>
public RangeBaseValueChangedEventArgs(double oldValue, double newValue, RoutedEvent? routedEvent, object? source)
: base(routedEvent, source)
{
OldValue = oldValue;
NewValue = newValue;
}
/// <summary>
/// Gets the old value of the range value property.
/// </summary>
public double OldValue { get; init; }
/// <summary>
/// Gets the new value of the range value property.
/// </summary>
public double NewValue { get; init; }
}
}

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

@ -618,38 +618,6 @@ namespace Avalonia.Controls.Primitives
base.OnTextInput(e);
}
/// <inheritdoc />
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (!e.Handled)
{
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
if (keymap is null)
return;
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
if (ItemCount > 0 &&
Match(keymap.SelectAll) &&
SelectionMode.HasAllFlags(SelectionMode.Multiple))
{
Selection.SelectAll();
e.Handled = true;
}
else if (e.Key == Key.Space || e.Key == Key.Enter)
{
UpdateSelectionFromEventSource(
e.Source,
true,
e.KeyModifiers.HasFlag(KeyModifiers.Shift),
e.KeyModifiers.HasFlag(KeyModifiers.Control));
}
}
}
/// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
@ -729,11 +697,16 @@ namespace Avalonia.Controls.Primitives
/// </summary>
/// <param name="direction">The direction to move.</param>
/// <param name="wrap">Whether to wrap when the selection reaches the first or last item.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <returns>True if the selection was moved; otherwise false.</returns>
protected bool MoveSelection(NavigationDirection direction, bool wrap)
protected bool MoveSelection(
NavigationDirection direction,
bool wrap = false,
bool rangeModifier = false)
{
var from = SelectedIndex != -1 ? ContainerFromIndex(SelectedIndex) : null;
return MoveSelection(from, direction, wrap);
var focused = FocusManager.GetFocusManager(this)?.GetFocusedElement();
var from = GetContainerFromEventSource(focused) ?? ContainerFromIndex(Selection.AnchorIndex);
return MoveSelection(from, direction, wrap, rangeModifier);
}
/// <summary>
@ -742,17 +715,37 @@ namespace Avalonia.Controls.Primitives
/// <param name="from">The container which serves as a starting point for the movement.</param>
/// <param name="direction">The direction to move.</param>
/// <param name="wrap">Whether to wrap when the selection reaches the first or last item.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <returns>True if the selection was moved; otherwise false.</returns>
protected bool MoveSelection(Control? from, NavigationDirection direction, bool wrap)
protected bool MoveSelection(
Control? from,
NavigationDirection direction,
bool wrap = false,
bool rangeModifier = false)
{
if (Presenter?.Panel is INavigableContainer container &&
GetNextControl(container, direction, from, wrap) is Control next)
if (Presenter?.Panel is not INavigableContainer container)
return false;
if (from is null)
{
direction = direction switch
{
NavigationDirection.Down => NavigationDirection.First,
NavigationDirection.Up => NavigationDirection.Last,
NavigationDirection.Right => NavigationDirection.First,
NavigationDirection.Left => NavigationDirection.Last,
_ => direction,
};
}
if (GetNextControl(container, direction, from, wrap) is Control next)
{
var index = IndexFromContainer(next);
if (index != -1)
{
SelectedIndex = index;
UpdateSelection(index, true, rangeModifier);
next.Focus();
return true;
}
}

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

@ -193,7 +193,7 @@ namespace Avalonia.Controls
UpdateContent();
};
Content = _content;
SetCurrentValue(ContentProperty, _content);
}
else
{

2
src/Avalonia.Controls/ScrollViewer.cs

@ -644,7 +644,7 @@ namespace Avalonia.Controls
(Presenter as IScrollAnchorProvider)?.UnregisterAnchorCandidate(element);
}
protected override bool RegisterContentPresenter(IContentPresenter presenter)
protected override bool RegisterContentPresenter(ContentPresenter presenter)
{
_childSubscription?.Dispose();
_childSubscription = null;

32
src/Avalonia.Controls/Shapes/Shape.cs

@ -126,8 +126,8 @@ namespace Avalonia.Controls.Shapes
/// </summary>
public IBrush? Fill
{
get { return GetValue(FillProperty); }
set { SetValue(FillProperty, value); }
get => GetValue(FillProperty);
set => SetValue(FillProperty, value);
}
/// <summary>
@ -135,8 +135,8 @@ namespace Avalonia.Controls.Shapes
/// </summary>
public Stretch Stretch
{
get { return GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
get => GetValue(StretchProperty);
set => SetValue(StretchProperty, value);
}
/// <summary>
@ -144,8 +144,8 @@ namespace Avalonia.Controls.Shapes
/// </summary>
public IBrush? Stroke
{
get { return GetValue(StrokeProperty); }
set { SetValue(StrokeProperty, value); }
get => GetValue(StrokeProperty);
set => SetValue(StrokeProperty, value);
}
/// <summary>
@ -153,8 +153,8 @@ namespace Avalonia.Controls.Shapes
/// </summary>
public AvaloniaList<double>? StrokeDashArray
{
get { return GetValue(StrokeDashArrayProperty); }
set { SetValue(StrokeDashArrayProperty, value); }
get => GetValue(StrokeDashArrayProperty);
set => SetValue(StrokeDashArrayProperty, value);
}
/// <summary>
@ -162,8 +162,8 @@ namespace Avalonia.Controls.Shapes
/// </summary>
public double StrokeDashOffset
{
get { return GetValue(StrokeDashOffsetProperty); }
set { SetValue(StrokeDashOffsetProperty, value); }
get => GetValue(StrokeDashOffsetProperty);
set => SetValue(StrokeDashOffsetProperty, value);
}
/// <summary>
@ -171,8 +171,8 @@ namespace Avalonia.Controls.Shapes
/// </summary>
public double StrokeThickness
{
get { return GetValue(StrokeThicknessProperty); }
set { SetValue(StrokeThicknessProperty, value); }
get => GetValue(StrokeThicknessProperty);
set => SetValue(StrokeThicknessProperty, value);
}
/// <summary>
@ -180,8 +180,8 @@ namespace Avalonia.Controls.Shapes
/// </summary>
public PenLineCap StrokeLineCap
{
get { return GetValue(StrokeLineCapProperty); }
set { SetValue(StrokeLineCapProperty, value); }
get => GetValue(StrokeLineCapProperty);
set => SetValue(StrokeLineCapProperty, value);
}
/// <summary>
@ -189,8 +189,8 @@ namespace Avalonia.Controls.Shapes
/// </summary>
public PenLineJoin StrokeJoin
{
get { return GetValue(StrokeJoinProperty); }
set { SetValue(StrokeJoinProperty, value); }
get => GetValue(StrokeJoinProperty);
set => SetValue(StrokeJoinProperty, value);
}
public sealed override void Render(DrawingContext context)

18
src/Avalonia.Controls/Slider.cs

@ -86,10 +86,10 @@ namespace Avalonia.Controls
TickBar.TicksProperty.AddOwner<Slider>();
// Slider required parts
protected bool _isDragging;
protected Track? _track;
protected Button? _decreaseButton;
protected Button? _increaseButton;
private bool _isDragging;
private Track? _track;
private Button? _decreaseButton;
private Button? _increaseButton;
private IDisposable? _decreaseButtonPressDispose;
private IDisposable? _decreaseButtonReleaseDispose;
private IDisposable? _increaseButtonSubscription;
@ -181,6 +181,16 @@ namespace Avalonia.Controls
set { SetValue(TickPlacementProperty, value); }
}
/// <summary>
/// Gets a value indicating whether the <see cref="Slider"/> is currently being dragged.
/// </summary>
protected bool IsDragging => _isDragging;
/// <summary>
/// Gets the <see cref="Track"/> part of the <see cref="Slider"/>.
/// </summary>
protected Track? Track => _track;
/// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{

6
src/Avalonia.Controls/SplitButton/SplitButton.cs

@ -18,9 +18,9 @@ namespace Avalonia.Controls
[PseudoClasses(pcFlyoutOpen, pcPressed)]
public class SplitButton : ContentControl, ICommandSource
{
protected const string pcChecked = ":checked";
protected const string pcPressed = ":pressed";
protected const string pcFlyoutOpen = ":flyout-open";
internal const string pcChecked = ":checked";
internal const string pcPressed = ":pressed";
internal const string pcFlyoutOpen = ":flyout-open";
/// <summary>
/// Raised when the user presses the primary part of the <see cref="SplitButton"/>.

20
src/Avalonia.Controls/SplitView/SplitView.cs

@ -22,15 +22,15 @@ namespace Avalonia.Controls
[PseudoClasses(pcLightDismiss)]
public class SplitView : ContentControl
{
protected const string pcOpen = ":open";
protected const string pcClosed = ":closed";
protected const string pcCompactOverlay = ":compactoverlay";
protected const string pcCompactInline = ":compactinline";
protected const string pcOverlay = ":overlay";
protected const string pcInline = ":inline";
protected const string pcLeft = ":left";
protected const string pcRight = ":right";
protected const string pcLightDismiss = ":lightDismiss";
private const string pcOpen = ":open";
private const string pcClosed = ":closed";
private const string pcCompactOverlay = ":compactoverlay";
private const string pcCompactInline = ":compactinline";
private const string pcOverlay = ":overlay";
private const string pcInline = ":inline";
private const string pcLeft = ":left";
private const string pcRight = ":right";
private const string pcLightDismiss = ":lightDismiss";
/// <summary>
/// Defines the <see cref="CompactPaneLength"/> property
@ -279,7 +279,7 @@ namespace Avalonia.Controls
remove => RemoveHandler(PaneOpeningEvent, value);
}
protected override bool RegisterContentPresenter(IContentPresenter presenter)
protected override bool RegisterContentPresenter(ContentPresenter presenter)
{
var result = base.RegisterContentPresenter(presenter);

72
src/Avalonia.Controls/TabControl.cs

@ -10,6 +10,7 @@ using Avalonia.LogicalTree;
using Avalonia.VisualTree;
using Avalonia.Automation;
using Avalonia.Controls.Metadata;
using Avalonia.Reactive;
namespace Avalonia.Controls
{
@ -19,6 +20,10 @@ namespace Avalonia.Controls
[TemplatePart("PART_ItemsPresenter", typeof(ItemsPresenter))]
public class TabControl : SelectingItemsControl, IContentPresenterHost
{
private object? _selectedContent;
private IDataTemplate? _selectedContentTemplate;
private CompositeDisposable? _selectedItemSubscriptions;
/// <summary>
/// Defines the <see cref="TabStripPlacement"/> property.
/// </summary>
@ -46,14 +51,14 @@ namespace Avalonia.Controls
/// <summary>
/// The selected content property
/// </summary>
public static readonly StyledProperty<object?> SelectedContentProperty =
AvaloniaProperty.Register<TabControl, object?>(nameof(SelectedContent));
public static readonly DirectProperty<TabControl, object?> SelectedContentProperty =
AvaloniaProperty.RegisterDirect<TabControl, object?>(nameof(SelectedContent), o => o.SelectedContent);
/// <summary>
/// The selected content template property
/// </summary>
public static readonly StyledProperty<IDataTemplate?> SelectedContentTemplateProperty =
AvaloniaProperty.Register<TabControl, IDataTemplate?>(nameof(SelectedContentTemplate));
public static readonly DirectProperty<TabControl, IDataTemplate?> SelectedContentTemplateProperty =
AvaloniaProperty.RegisterDirect<TabControl, IDataTemplate?>(nameof(SelectedContentTemplate), o => o.SelectedContentTemplate);
/// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
@ -115,11 +120,10 @@ namespace Avalonia.Controls
/// <value>
/// The content of the selected tab.
/// </value>
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1032", Justification = "This property is supposed to be a styled readonly property.")]
public object? SelectedContent
{
get { return GetValue(SelectedContentProperty); }
internal set { SetValue(SelectedContentProperty, value); }
get => _selectedContent;
internal set => SetAndRaise(SelectedContentProperty, ref _selectedContent, value);
}
/// <summary>
@ -128,22 +132,21 @@ namespace Avalonia.Controls
/// <value>
/// The content template of the selected tab.
/// </value>
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1032", Justification = "This property is supposed to be a styled readonly property.")]
public IDataTemplate? SelectedContentTemplate
{
get { return GetValue(SelectedContentTemplateProperty); }
internal set { SetValue(SelectedContentTemplateProperty, value); }
get => _selectedContentTemplate;
internal set => SetAndRaise(SelectedContentTemplateProperty, ref _selectedContentTemplate, value);
}
internal ItemsPresenter? ItemsPresenterPart { get; private set; }
internal IContentPresenter? ContentPart { get; private set; }
internal ContentPresenter? ContentPart { get; private set; }
/// <inheritdoc/>
IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
/// <inheritdoc/>
bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
bool IContentPresenterHost.RegisterContentPresenter(ContentPresenter presenter)
{
return RegisterContentPresenter(presenter);
}
@ -161,18 +164,10 @@ namespace Avalonia.Controls
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.PrepareContainerForItemOverride(element, item, index);
if (element is TabItem tabItem)
{
if (ContentTemplate is { } ct)
tabItem.ContentTemplate = ct;
tabItem.SetValue(TabStripPlacementProperty, TabStripPlacement);
}
if (index == SelectedIndex && element is ContentControl container)
if (index == SelectedIndex)
{
SelectedContentTemplate = container.ContentTemplate;
SelectedContent = container.Content;
UpdateSelectedContent(element);
}
}
@ -192,26 +187,33 @@ namespace Avalonia.Controls
UpdateSelectedContent();
}
private void UpdateSelectedContent()
private void UpdateSelectedContent(Control? container = null)
{
_selectedItemSubscriptions?.Dispose();
_selectedItemSubscriptions = null;
if (SelectedIndex == -1)
{
SelectedContent = SelectedContentTemplate = null;
}
else
{
var container = SelectedItem as IContentControl ??
ContainerFromIndex(SelectedIndex) as IContentControl;
SelectedContentTemplate = container?.ContentTemplate;
SelectedContent = container?.Content;
container ??= ContainerFromIndex(SelectedIndex);
if (container != null)
{
_selectedItemSubscriptions = new CompositeDisposable(
container.GetObservable(ContentControl.ContentProperty).Subscribe(v => SelectedContent = v),
// Note how we fall back to our own ContentTemplate if the container doesn't specify one
container.GetObservable(ContentControl.ContentTemplateProperty).Subscribe(v => SelectedContentTemplate = v ?? ContentTemplate));
}
}
}
/// <summary>
/// Called when an <see cref="IContentPresenter"/> is registered with the control.
/// Called when an <see cref="ContentPresenter"/> is registered with the control.
/// </summary>
/// <param name="presenter">The presenter.</param>
protected virtual bool RegisterContentPresenter(IContentPresenter presenter)
protected virtual bool RegisterContentPresenter(ContentPresenter presenter)
{
if (presenter.Name == "PART_SelectedContentHost")
{
@ -224,7 +226,7 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
ItemsPresenterPart = e.NameScope.Get<ItemsPresenter>("PART_ItemsPresenter");
ItemsPresenterPart = e.NameScope.Find<ItemsPresenter>("PART_ItemsPresenter");
ItemsPresenterPart?.ApplyTemplate();
// Set TabNavigation to Once on the panel if not already set and
@ -285,6 +287,16 @@ namespace Avalonia.Controls
{
RefreshContainers();
}
else if (change.Property == ContentTemplateProperty)
{
var newTemplate = change.GetNewValue<IDataTemplate?>();
if (SelectedContentTemplate != newTemplate &&
ContainerFromIndex(SelectedIndex) is { } container &&
container.GetValue(ContentControl.ContentTemplateProperty) == null)
{
SelectedContentTemplate = newTemplate; // See also UpdateSelectedContent
}
}
else if (change.Property == KeyboardNavigation.TabOnceActiveElementProperty &&
ItemsPresenterPart?.Panel is { } panel)
{

42
src/Avalonia.Controls/TabItem.cs

@ -1,22 +1,28 @@
using System;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
/// <summary>
/// An item in a <see cref="TabStrip"/> or <see cref="TabControl"/>.
/// An item in a <see cref="TabControl"/>.
/// </summary>
[PseudoClasses(":pressed", ":selected")]
public class TabItem : HeaderedContentControl, ISelectable
{
private Dock? _tabStripPlacement;
private IDisposable? _ownerSubscriptions;
/// <summary>
/// Defines the <see cref="TabStripPlacement"/> property.
/// </summary>
public static readonly StyledProperty<Dock> TabStripPlacementProperty =
TabControl.TabStripPlacementProperty.AddOwner<TabItem>();
public static readonly DirectProperty<TabItem, Dock?> TabStripPlacementProperty =
AvaloniaProperty.RegisterDirect<TabItem, Dock?>(nameof(TabStripPlacement), o => o.TabStripPlacement);
/// <summary>
/// Defines the <see cref="IsSelected"/> property.
@ -37,16 +43,12 @@ namespace Avalonia.Controls
}
/// <summary>
/// Gets the tab strip placement.
/// Gets the placement of this tab relative to the outer <see cref="TabControl"/>, if there is one.
/// </summary>
/// <value>
/// The tab strip placement.
/// </value>
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1031",
Justification = "This property is supposed to be inherited only and settable on parent TabControl.")]
public Dock TabStripPlacement
public Dock? TabStripPlacement
{
get { return GetValue(TabStripPlacementProperty); }
get => _tabStripPlacement;
private set => SetAndRaise(TabStripPlacementProperty, ref _tabStripPlacement, value);
}
/// <summary>
@ -60,6 +62,24 @@ namespace Avalonia.Controls
protected override AutomationPeer OnCreateAutomationPeer() => new ListItemAutomationPeer(this);
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_ownerSubscriptions?.Dispose();
_ownerSubscriptions = null;
if (this.FindAncestorOfType<TabControl>() is { } owner && owner.IndexFromContainer(this) != -1)
{
SubscribeToOwnerProperties(owner);
}
}
protected void SubscribeToOwnerProperties(AvaloniaObject owner)
{
_ownerSubscriptions = owner.GetObservable(TabControl.TabStripPlacementProperty).Subscribe(v => TabStripPlacement = v);
}
private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj)
{
if (Header == null)

9
src/Avalonia.Controls/TextBlock.cs

@ -144,8 +144,8 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<TextBlock, InlineCollection?>(
nameof(Inlines), t => t.Inlines, (t, v) => t.Inlines = v);
protected TextLayout? _textLayout;
protected Size _constraint;
private TextLayout? _textLayout;
private Size _constraint;
private IReadOnlyList<TextRun>? _textRuns;
private InlineCollection? _inlines;
@ -639,8 +639,7 @@ namespace Avalonia.Controls
TextTrimming,
_constraint.Width,
_constraint.Height,
maxLines: MaxLines,
lineHeight: LineHeight);
MaxLines);
}
/// <summary>
@ -829,7 +828,7 @@ namespace Avalonia.Controls
InvalidateTextLayout();
}
protected readonly record struct SimpleTextSource : ITextSource
private readonly record struct SimpleTextSource : ITextSource
{
private readonly string _text;
private readonly TextRunProperties _defaultProperties;

93
src/Avalonia.Controls/TextBox.cs

@ -18,9 +18,6 @@ using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Automation.Peers;
using Avalonia.Threading;
using Avalonia.Platform;
using System.Reflection;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Controls
{
@ -28,6 +25,7 @@ namespace Avalonia.Controls
/// Represents a control that can be used to display or edit unformatted text.
/// </summary>
[TemplatePart("PART_TextPresenter", typeof(TextPresenter))]
[TemplatePart("PART_ScrollViewer", typeof(ScrollViewer))]
[PseudoClasses(":empty")]
public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost
{
@ -158,7 +156,7 @@ namespace Avalonia.Controls
/// Defines see <see cref="TextPresenter.LineHeight"/> property.
/// </summary>
public static readonly StyledProperty<double> LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner<TextBox>();
TextBlock.LineHeightProperty.AddOwner<TextBox>(new(defaultValue: double.NaN));
/// <summary>
/// Defines see <see cref="TextBlock.LetterSpacing"/> property.
@ -310,6 +308,7 @@ namespace Avalonia.Controls
}
private TextPresenter? _presenter;
private ScrollViewer? _scrollViewer;
private readonly TextBoxTextInputMethodClient _imClient = new();
private readonly UndoRedoHelper<UndoRedoState> _undoRedoHelper;
private bool _isUndoingRedoing;
@ -490,7 +489,7 @@ namespace Avalonia.Controls
}
/// <summary>
/// Gets or sets the maximum character length of the TextBox
/// Gets or sets the maximum number of visible lines.
/// </summary>
public int MaxLength
{
@ -803,6 +802,8 @@ namespace Avalonia.Controls
{
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
_scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
_imClient.SetPresenter(_presenter, this);
if (IsFocused)
@ -855,6 +856,10 @@ namespace Avalonia.Controls
{
OnSelectionEndChanged(change);
}
else if (change.Property == MaxLinesProperty)
{
InvalidateMeasure();
}
else if (change.Property == UndoLimitProperty)
{
OnUndoLimitChanged(change.GetNewValue<int>());
@ -942,40 +947,10 @@ namespace Avalonia.Controls
{
return;
}
_selectedTextChangesMadeSinceLastUndoSnapshot++;
SnapshotUndoRedo(ignoreChangeCount: false);
if (_presenter != null && MaxLines > 0)
{
var lineCount = _presenter.TextLayout.TextLines.Count;
var length = 0;
var graphemeEnumerator = new GraphemeEnumerator(input.AsSpan());
while (graphemeEnumerator.MoveNext(out var grapheme))
{
if (grapheme.FirstCodepoint.IsBreakChar)
{
if (lineCount + 1 > MaxLines)
{
break;
}
else
{
lineCount++;
}
}
length += grapheme.Length;
}
if (length < input.Length)
{
input = input.Remove(Math.Max(0, length));
}
}
var currentText = Text ?? string.Empty;
var selectionLength = Math.Abs(SelectionStart - SelectionEnd);
var newLength = input.Length + currentText.Length - selectionLength;
@ -1017,7 +992,7 @@ namespace Avalonia.Controls
}
}
public string? RemoveInvalidCharacters(string? text)
private string? RemoveInvalidCharacters(string? text)
{
if (text is null)
return null;
@ -1518,7 +1493,7 @@ namespace Avalonia.Controls
_presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
@ -1976,5 +1951,47 @@ namespace Avalonia.Controls
{
CanRedo = _undoRedoHelper.CanRedo;
}
protected override Size MeasureOverride(Size availableSize)
{
if(_scrollViewer != null)
{
var maxHeight = double.PositiveInfinity;
if (MaxLines > 0 && double.IsNaN(Height))
{
var fontSize = FontSize;
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default);
var textLayout = new TextLayout(new MaxLinesTextSource(MaxLines), paragraphProperties);
maxHeight = Math.Ceiling(textLayout.Height);
}
_scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight);
}
return base.MeasureOverride(availableSize);
}
private class MaxLinesTextSource : ITextSource
{
private readonly int _maxLines;
public MaxLinesTextSource(int maxLines)
{
_maxLines = maxLines;
}
public TextRun? GetTextRun(int textSourceIndex)
{
if (textSourceIndex >= _maxLines)
{
return null;
}
return new TextEndOfLine(1);
}
}
}
}

11
src/Avalonia.Controls/TextChangedEventArgs.cs

@ -3,15 +3,24 @@
namespace Avalonia.Controls
{
/// <summary>
/// Provides data specific to a TextChanged event.
/// Provides data specific to a <see cref="TextBox.TextChanged"/> event.
/// </summary>
public class TextChangedEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="TextChangedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
public TextChangedEventArgs(RoutedEvent? routedEvent)
: base (routedEvent)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TextChangedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
/// <param name="source">The source object that raised the routed event.</param>
public TextChangedEventArgs(RoutedEvent? routedEvent, Interactive? source)
: base(routedEvent, source)
{

11
src/Avalonia.Controls/TextChangingEventArgs.cs

@ -3,15 +3,24 @@
namespace Avalonia.Controls
{
/// <summary>
/// Provides data specific to a TextChanging event.
/// Provides data specific to a <see cref="TextBox.TextChanging"/> event.
/// </summary>
public class TextChangingEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="TextChangingEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
public TextChangingEventArgs(RoutedEvent? routedEvent)
: base (routedEvent)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TextChangingEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
/// <param name="source">The source object that raised the routed event.</param>
public TextChangingEventArgs(RoutedEvent? routedEvent, Interactive? source)
: base(routedEvent, source)
{

8
src/Avalonia.Controls/ToggleSwitch.cs

@ -86,13 +86,13 @@ namespace Avalonia.Controls
set { SetValue(OffContentProperty, value); }
}
public IContentPresenter? OffContentPresenter
public ContentPresenter? OffContentPresenter
{
get;
private set;
}
public IContentPresenter? OnContentPresenter
public ContentPresenter? OnContentPresenter
{
get;
private set;
@ -142,7 +142,7 @@ namespace Avalonia.Controls
}
}
protected override bool RegisterContentPresenter(IContentPresenter presenter)
protected override bool RegisterContentPresenter(ContentPresenter presenter)
{
var result = base.RegisterContentPresenter(presenter);
@ -241,7 +241,7 @@ namespace Avalonia.Controls
}
}
protected void UpdateKnobPos(bool value)
private void UpdateKnobPos(bool value)
{
if ((_switchKnob != null) && (_knobsPanel != null))
{

25
src/Avalonia.Controls/TopLevel.cs

@ -375,7 +375,16 @@ namespace Avalonia.Controls
/// Gets the platform-specific window implementation.
/// </summary>
public ITopLevelImpl? PlatformImpl { get; private set; }
/// <summary>
/// Trys to get the platform handle for the TopLevel-derived control.
/// </summary>
/// <returns>
/// An <see cref="IPlatformHandle"/> describing the window handle, or null if the handle
/// could not be retrieved.
/// </returns>
public IPlatformHandle? TryGetPlatformHandle() => ((IWindowBaseImpl?) PlatformImpl)?.Handle;
/// <summary>
/// Gets the renderer for the window.
/// </summary>
@ -386,7 +395,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets the access key handler for the window.
/// </summary>
IAccessKeyHandler IInputRoot.AccessKeyHandler => _accessKeyHandler!;
internal IAccessKeyHandler AccessKeyHandler => _accessKeyHandler!;
/// <summary>
/// Gets or sets the keyboard navigation handler for the window.
@ -523,13 +532,13 @@ namespace Avalonia.Controls
/// <summary>
/// Creates the layout manager for this <see cref="TopLevel" />.
/// </summary>
protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this);
private protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this);
/// <summary>
/// Handles a paint notification from <see cref="ITopLevelImpl.Resized"/>.
/// </summary>
/// <param name="rect">The dirty area.</param>
protected virtual void HandlePaint(Rect rect)
private void HandlePaint(Rect rect)
{
Renderer.Paint(rect);
}
@ -537,7 +546,7 @@ namespace Avalonia.Controls
/// <summary>
/// Handles a closed notification from <see cref="ITopLevelImpl.Closed"/>.
/// </summary>
protected virtual void HandleClosed()
private protected virtual void HandleClosed()
{
Renderer.SceneInvalidated -= SceneInvalidated;
// We need to wait for the renderer to complete any in-flight operations
@ -595,7 +604,7 @@ namespace Avalonia.Controls
/// <see cref="ITopLevelImpl.ScalingChanged"/>.
/// </summary>
/// <param name="scaling">The window scaling.</param>
protected virtual void HandleScalingChanged(double scaling)
private void HandleScalingChanged(double scaling)
{
LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
ScalingChanged?.Invoke(this, EventArgs.Empty);
@ -615,7 +624,7 @@ namespace Avalonia.Controls
return false;
}
protected virtual void HandleTransparencyLevelChanged(WindowTransparencyLevel transparencyLevel)
private void HandleTransparencyLevelChanged(WindowTransparencyLevel transparencyLevel)
{
if(_transparencyFallbackBorder != null)
{
@ -740,7 +749,7 @@ namespace Avalonia.Controls
protected override bool BypassFlowDirectionPolicies => true;
public override void InvalidateMirrorTransform()
protected internal override void InvalidateMirrorTransform()
{
// Do nothing becuase TopLevel should't apply MirrorTransform on himself.
}

2
src/Avalonia.Controls/TransitioningContentControl.cs

@ -71,7 +71,7 @@ public class TransitioningContentControl : ContentControl
return result;
}
protected override bool RegisterContentPresenter(IContentPresenter presenter)
protected override bool RegisterContentPresenter(ContentPresenter presenter)
{
if (!base.RegisterContentPresenter(presenter) &&
presenter is ContentPresenter p &&

120
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -70,8 +70,8 @@ namespace Avalonia.Controls
private ScrollViewer? _scrollViewer;
private Rect _viewport = s_invalidViewport;
private Dictionary<object, Stack<Control>>? _recyclePool;
private Control? _unrealizedFocusedElement;
private int _unrealizedFocusedIndex = -1;
private Control? _focusedElement;
private int _focusedIndex = -1;
public VirtualizingStackPanel()
{
@ -265,12 +265,14 @@ namespace Avalonia.Controls
protected override IInputElement? GetControl(NavigationDirection direction, IInputElement? from, bool wrap)
{
var count = Items.Count;
var fromControl = from as Control;
if (count == 0 || from is not Control fromControl)
if (count == 0 ||
fromControl is null && direction is not NavigationDirection.First or NavigationDirection.Last)
return null;
var horiz = Orientation == Orientation.Horizontal;
var fromIndex = from != null ? IndexFromContainer(fromControl) : -1;
var fromIndex = fromControl != null ? IndexFromContainer(fromControl) : -1;
var toIndex = fromIndex;
switch (direction)
@ -330,14 +332,25 @@ namespace Avalonia.Controls
{
if (index < 0 || index >= Items.Count)
return null;
if (_realizedElements?.GetElement(index) is { } realized)
if (_scrollToIndex == index)
return _scrollToElement;
if (_focusedIndex == index)
return _focusedElement;
if (GetRealizedElement(index) is { } realized)
return realized;
if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer)
return c;
return null;
}
protected internal override int IndexFromContainer(Control container) => _realizedElements?.GetIndex(container) ?? -1;
protected internal override int IndexFromContainer(Control container)
{
if (container == _scrollToElement)
return _scrollToIndex;
if (container == _focusedElement)
return _focusedIndex;
return _realizedElements?.GetIndex(container) ?? -1;
}
protected internal override Control? ScrollIntoView(int index)
{
@ -355,16 +368,19 @@ namespace Avalonia.Controls
{
// Create and measure the element to be brought into view. Store it in a field so that
// it can be re-used in the layout pass.
_scrollToElement = GetOrCreateElement(items, index);
_scrollToElement.Measure(Size.Infinity);
_scrollToIndex = index;
var scrollToElement = GetOrCreateElement(items, index);
scrollToElement.Measure(Size.Infinity);
// Get the expected position of the elment and put it in place.
var anchorU = _realizedElements.GetOrEstimateElementU(index, ref _lastEstimatedElementSizeU);
var rect = Orientation == Orientation.Horizontal ?
new Rect(anchorU, 0, _scrollToElement.DesiredSize.Width, _scrollToElement.DesiredSize.Height) :
new Rect(0, anchorU, _scrollToElement.DesiredSize.Width, _scrollToElement.DesiredSize.Height);
_scrollToElement.Arrange(rect);
new Rect(anchorU, 0, scrollToElement.DesiredSize.Width, scrollToElement.DesiredSize.Height) :
new Rect(0, anchorU, scrollToElement.DesiredSize.Width, scrollToElement.DesiredSize.Height);
scrollToElement.Arrange(rect);
// Store the element and index so that they can be used in the layout pass.
_scrollToElement = scrollToElement;
_scrollToIndex = index;
// If the item being brought into view was added since the last layout pass then
// our bounds won't be updated, so any containing scroll viewers will not have an
@ -378,7 +394,7 @@ namespace Avalonia.Controls
}
// Try to bring the item into view.
_scrollToElement.BringIntoView();
scrollToElement.BringIntoView();
// If the viewport does not contain the item to scroll to, set _isWaitingForViewportUpdate:
// this should cause the following chain of events:
@ -397,10 +413,9 @@ namespace Avalonia.Controls
root.LayoutManager.ExecuteLayoutPass();
}
var result = _scrollToElement;
_scrollToElement = null;
_scrollToIndex = -1;
return result;
return scrollToElement;
}
return null;
@ -563,34 +578,48 @@ namespace Avalonia.Controls
{
Debug.Assert(ItemContainerGenerator is not null);
var e = GetRealizedElement(index);
if ((GetRealizedElement(index) ??
GetRealizedElement(index, ref _focusedIndex, ref _focusedElement) ??
GetRealizedElement(index, ref _scrollToIndex, ref _scrollToElement)) is { } realized)
return realized;
var item = items[index];
var generator = ItemContainerGenerator!;
if (e is null)
if (generator.NeedsContainer(item, index, out var recycleKey))
{
var item = items[index];
var generator = ItemContainerGenerator!;
if (generator.NeedsContainer(item, index, out var recycleKey))
{
e = GetRecycledElement(item, index, recycleKey) ??
CreateElement(item, index, recycleKey);
}
else
{
e = GetItemAsOwnContainer(item, index);
}
return GetRecycledElement(item, index, recycleKey) ??
CreateElement(item, index, recycleKey);
}
else
{
return GetItemAsOwnContainer(item, index);
}
return e;
}
private Control? GetRealizedElement(int index)
{
if (_scrollToIndex == index)
return _scrollToElement;
return _realizedElements?.GetElement(index);
}
private static Control? GetRealizedElement(
int index,
ref int specialIndex,
ref Control? specialElement)
{
if (specialIndex == index)
{
Debug.Assert(specialElement is not null);
var result = specialElement;
specialIndex = -1;
specialElement = null;
return result;
}
return null;
}
private Control GetItemAsOwnContainer(object? item, int index)
{
Debug.Assert(ItemContainerGenerator is not null);
@ -619,15 +648,6 @@ namespace Avalonia.Controls
var generator = ItemContainerGenerator!;
if (_unrealizedFocusedIndex == index && _unrealizedFocusedElement is not null)
{
var element = _unrealizedFocusedElement;
_unrealizedFocusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus;
_unrealizedFocusedElement = null;
_unrealizedFocusedIndex = -1;
return element;
}
if (_recyclePool?.TryGetValue(recycleKey, out var recyclePool) == true && recyclePool.Count > 0)
{
var recycled = recyclePool.Pop();
@ -673,9 +693,9 @@ namespace Avalonia.Controls
}
else if (element.IsKeyboardFocusWithin)
{
_unrealizedFocusedElement = element;
_unrealizedFocusedIndex = index;
_unrealizedFocusedElement.LostFocus += OnUnrealizedFocusedElementLostFocus;
_focusedElement = element;
_focusedIndex = index;
_focusedElement.LostFocus += OnUnrealizedFocusedElementLostFocus;
}
else
{
@ -744,13 +764,13 @@ namespace Avalonia.Controls
private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs e)
{
if (_unrealizedFocusedElement is null || sender != _unrealizedFocusedElement)
if (_focusedElement is null || sender != _focusedElement)
return;
_unrealizedFocusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus;
RecycleElement(_unrealizedFocusedElement, _unrealizedFocusedIndex);
_unrealizedFocusedElement = null;
_unrealizedFocusedIndex = -1;
_focusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus;
RecycleElement(_focusedElement, _focusedIndex);
_focusedElement = null;
_focusedIndex = -1;
}
/// <inheritdoc/>

4
src/Avalonia.Controls/Window.cs

@ -535,7 +535,7 @@ namespace Avalonia.Controls
return true;
}
protected virtual void HandleWindowStateChanged(WindowState state)
private void HandleWindowStateChanged(WindowState state)
{
WindowState = state;
@ -979,7 +979,7 @@ namespace Avalonia.Controls
return ClientSize;
}
protected sealed override void HandleClosed()
private protected sealed override void HandleClosed()
{
RaiseEvent(new RoutedEventArgs(WindowClosedEvent));

12
src/Avalonia.Controls/WindowBase.cs

@ -60,7 +60,7 @@ namespace Avalonia.Controls
impl.PositionChanged = HandlePositionChanged;
}
protected IDisposable FreezeVisibilityChangeHandling()
private protected IDisposable FreezeVisibilityChangeHandling()
{
return new IgnoreVisibilityChangesDisposable(this);
}
@ -178,14 +178,6 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Trys to get the platform handle for the window.
/// </summary>
/// <returns>
/// An <see cref="IPlatformHandle"/> describing the window handle, or null if the handle
/// could not be retrieved.
/// </returns>
public IPlatformHandle? TryGetPlatformHandle() => PlatformImpl?.Handle;
/// <summary>
/// Ensures that the window is initialized.
@ -226,7 +218,7 @@ namespace Avalonia.Controls
/// <param name="e">An <see cref="EventArgs"/> that contains the event data.</param>
protected virtual void OnResized(WindowResizedEventArgs e) => Resized?.Invoke(this, e);
protected override void HandleClosed()
private protected override void HandleClosed()
{
using (FreezeVisibilityChangeHandling())
{

2
src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs

@ -4,7 +4,7 @@ using Avalonia.Diagnostics.ViewModels;
namespace Avalonia.Diagnostics.Models
{
public class ConsoleContext
internal class ConsoleContext
{
private readonly ConsoleViewModel _owner;

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

Loading…
Cancel
Save