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.Xaml\\Avalonia.Markup.Xaml.csproj",
"src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj", "src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj",
"src\\Skia\\Avalonia.Skia\\Avalonia.Skia.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.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj", "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.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.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",
@ -66,4 +65,4 @@
"tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj"
] ]
} }
} }

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

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

29
samples/ControlCatalog/Pages/TabControlPage.xaml

@ -4,7 +4,27 @@
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:viewModels="using:ControlCatalog.ViewModels" xmlns:viewModels="using:ControlCatalog.ViewModels"
x:DataType="viewModels:TabControlPageViewModel"> 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 <TextBlock
DockPanel.Dock="Top" DockPanel.Dock="Top"
Classes="h2" Classes="h2"
@ -55,14 +75,14 @@
Margin="0 16" Margin="0 16"
DisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}" DisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}"
TabStripPlacement="{Binding TabPlacement}"> TabStripPlacement="{Binding TabPlacement}">
<TabControl.ContentTemplate> <TabControl.DataTemplates>
<DataTemplate x:DataType="viewModels:TabControlPageViewModelItem"> <DataTemplate x:DataType="viewModels:TabControlPageViewModelItem">
<StackPanel Orientation="Vertical" Spacing="8"> <StackPanel Orientation="Vertical" Spacing="8">
<TextBlock Text="{Binding Text}"/> <TextBlock Text="{Binding Text}"/>
<Image Source="{Binding Image}" Width="300"/> <Image Source="{Binding Image}" Width="300"/>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
</TabControl.ContentTemplate> </TabControl.DataTemplates>
<TabControl.Styles> <TabControl.Styles>
<Style Selector="TabItem" x:DataType="viewModels:TabControlPageViewModelItem"> <Style Selector="TabItem" x:DataType="viewModels:TabControlPageViewModelItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/> <Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
@ -78,12 +98,13 @@
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center"> VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Tab Placement:</TextBlock> <TextBlock VerticalAlignment="Center">Tab Placement:</TextBlock>
<ComboBox SelectedIndex="{Binding TabPlacement, Mode=TwoWay}"> <ComboBox SelectedIndex="{Binding TabPlacement, Mode=TwoWay}" Width="100">
<ComboBoxItem>Left</ComboBoxItem> <ComboBoxItem>Left</ComboBoxItem>
<ComboBoxItem>Bottom</ComboBoxItem> <ComboBoxItem>Bottom</ComboBoxItem>
<ComboBoxItem>Right</ComboBoxItem> <ComboBoxItem>Right</ComboBoxItem>
<ComboBoxItem>Top</ComboBoxItem> <ComboBoxItem>Top</ComboBoxItem>
</ComboBox> </ComboBox>
<CheckBox Name="UseContentTemplates">Set TabItem.ContentTemplate</CheckBox>
</StackPanel> </StackPanel>
</Grid> </Grid>
</DockPanel> </DockPanel>

3
samples/IntegrationTestApp/MainWindow.axaml

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

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

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

2
src/Avalonia.Base/AvaloniaProperty.cs

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

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

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

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

@ -237,7 +237,7 @@ namespace Avalonia.Data
/// </summary> /// </summary>
/// <param name="value">The untyped value.</param> /// <param name="value">The untyped value.</param>
/// <returns>The typed binding value.</returns> /// <returns>The typed binding value.</returns>
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)] [RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConversionRequiresUnreferencedCodeMessage)]
public static BindingValue<T> FromUntyped(object? value) public static BindingValue<T> FromUntyped(object? value)
{ {
return FromUntyped(value, typeof(T)); return FromUntyped(value, typeof(T));
@ -251,7 +251,7 @@ namespace Avalonia.Data
/// <param name="value">The untyped value.</param> /// <param name="value">The untyped value.</param>
/// <param name="targetType">The runtime target type.</param> /// <param name="targetType">The runtime target type.</param>
/// <returns>The typed binding value.</returns> /// <returns>The typed binding value.</returns>
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)] [RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConversionRequiresUnreferencedCodeMessage)]
public static BindingValue<T> FromUntyped(object? value, Type targetType) public static BindingValue<T> FromUntyped(object? value, Type targetType)
{ {
if (value == AvaloniaProperty.UnsetValue) 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 /// Provides a default set of value conversions for bindings that do not specify a value
/// converter. /// converter.
/// </summary> /// </summary>
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)] [RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]
public class DefaultValueConverter : IValueConverter public class DefaultValueConverter : IValueConverter
{ {
/// <summary> /// <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 /// Binds to an expression on an object using a type value converter to convert the values
/// that are sent and received. /// that are sent and received.
/// </summary> /// </summary>
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)] [RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]
internal class BindingExpression : LightweightObservableBase<object?>, IAvaloniaSubject<object?>, IDescription internal class BindingExpression : LightweightObservableBase<object?>, IAvaloniaSubject<object?>, IDescription
{ {
private readonly ExpressionObserver _inner; 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 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 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 ImplicitTypeConvertionRequiresUnreferencedCodeMessage = "Implicit convertion methods are required for type conversion."; 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 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 TypeConvertionRequiresUnreferencedCodeMessage = "Convertion methods are required for type conversion, including op_Implicit, op_Explicit, Parse and TypeConverter."; 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 ReflectionBindingRequiresUnreferencedCodeMessage = "BindingExpression and ReflectionBinding heavily use reflection. Consider using CompiledBindings instead.";
public const string ReflectionBindingSupressWarningMessage = "BindingExpression and ReflectionBinding internal heavily use reflection."; 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> /// <summary>
/// Defines the interface for classes that handle access keys for a window. /// Defines the interface for classes that handle access keys for a window.
/// </summary> /// </summary>
[Unstable] internal interface IAccessKeyHandler
public interface IAccessKeyHandler
{ {
/// <summary> /// <summary>
/// Gets or sets the window's main menu. /// Gets or sets the window's main menu.

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

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

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

@ -1,15 +1,12 @@
using System; using System;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
{ {
/// <summary> /// <summary>
/// Defines the interface for a window's main menu. /// Defines the interface for a window's main menu.
/// </summary> /// </summary>
[NotClientImplementable] internal interface IMainMenu
public interface IMainMenu
{ {
/// <summary> /// <summary>
/// Gets a value indicating whether the menu is open. /// 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="direction">The direction to move.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param> /// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
public void Move( public void Move(
IInputElement element, IInputElement? element,
NavigationDirection direction, NavigationDirection direction,
KeyModifiers keyModifiers = KeyModifiers.None) 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) if (next != null)
{ {
@ -101,10 +104,9 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
protected virtual void OnKeyDown(object? sender, KeyEventArgs e) protected virtual void OnKeyDown(object? sender, KeyEventArgs e)
{ {
var current = FocusManager.GetFocusManager(e.Source as IInputElement)?.GetFocusedElement(); if (e.Key == Key.Tab)
if (current != null && e.Key == Key.Tab)
{ {
var current = FocusManager.GetFocusManager(e.Source as IInputElement)?.GetFocusedElement();
var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ? var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ?
NavigationDirection.Next : NavigationDirection.Previous; NavigationDirection.Next : NavigationDirection.Previous;
Move(current, direction, e.KeyModifiers); Move(current, direction, e.KeyModifiers);

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

@ -70,6 +70,17 @@ namespace Avalonia.Input
if (relativeTo == null) if (relativeTo == null)
return pt; 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; 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="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, /// <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> /// this value is the number of code points until the next valid caret position.</param>
[DebuggerStepThrough]
public CharacterHit(int firstCharacterIndex, int trailingLength = 0) public CharacterHit(int firstCharacterIndex, int trailingLength = 0)
{ {
FirstCharacterIndex = firstCharacterIndex; FirstCharacterIndex = firstCharacterIndex;

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

@ -18,14 +18,14 @@ namespace Avalonia.Media.TextFormatting
public static BidiReorderer Instance public static BidiReorderer Instance
=> t_instance ??= new(); => 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(_runs.Length == 0);
Debug.Assert(_ranges.Length == 0); Debug.Assert(_ranges.Length == 0);
if (textRuns.IsEmpty) if (textRuns.IsEmpty)
{ {
return; return Array.Empty<IndexedTextRun>();
} }
try try
@ -46,6 +46,22 @@ namespace Avalonia.Media.TextFormatting
// Reorder them into visual order. // Reorder them into visual order.
var firstIndex = LinearReorder(); 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. // 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 // 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)) if (max == 0 || (min == max && (max & 1) == 0))
{ {
// Nothing to reverse. // Nothing to reverse.
return; return indexedTextRuns;
} }
// Now apply the reversal and replace the original contents. // Now apply the reversal and replace the original contents.
@ -107,13 +123,25 @@ namespace Avalonia.Media.TextFormatting
var index = 0; var index = 0;
currentIndex = firstIndex; currentIndex = firstIndex;
while (currentIndex >= 0) while (currentIndex >= 0)
{ {
ref var current = ref _runs[currentIndex]; 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; currentIndex = current.NextRunIndex;
} }
return indexedTextRuns;
} }
finally finally
{ {
@ -227,25 +255,6 @@ namespace Avalonia.Media.TextFormatting
return previousIndex; 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 private struct BidiRange
{ {
public BidiRange(sbyte level, int leftRunIndex, int rightRunIndex, int previousRangeIndex) 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 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.Collections.Generic;
using System.Diagnostics;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
@ -10,6 +11,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Constructing TextBounds object /// Constructing TextBounds object
/// </summary> /// </summary>
[DebuggerStepThrough]
internal TextBounds(Rect bounds, FlowDirection flowDirection, IList<TextRunBounds> runBounds) internal TextBounds(Rect bounds, FlowDirection flowDirection, IList<TextRunBounds> runBounds)
{ {
Rectangle = bounds; Rectangle = bounds;

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

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

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

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

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

@ -208,7 +208,7 @@ namespace Avalonia.PropertyStore
IsOverridenCurrentValue = isOverriddenCurrentValue; IsOverridenCurrentValue = isOverriddenCurrentValue;
IsCoercedDefaultValue = isCoercedDefaultValue; IsCoercedDefaultValue = isCoercedDefaultValue;
if (_uncommon?._coerce is { } coerce) if (!isCoercedDefaultValue && _uncommon?._coerce is { } coerce)
v = coerce(owner.Owner, value); v = coerce(owner.Owner, value);
if (priority <= Priority) if (priority <= Priority)
@ -262,7 +262,8 @@ namespace Avalonia.PropertyStore
if (_uncommon?._coerce is { } coerce) if (_uncommon?._coerce is { } coerce)
{ {
v = coerce(owner.Owner, value); 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)) 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); 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) public void OnNext(object? value)
{ {
if (value == BindingOperations.DoNothing) if (value == BindingOperations.DoNothing)

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

@ -7,7 +7,7 @@ namespace Avalonia.PropertyStore
{ {
internal static class UntypedValueUtils internal static class UntypedValueUtils
{ {
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConversionSupressWarningMessage)]
public static BindingValue<T> ConvertAndValidate<T>( public static BindingValue<T> ConvertAndValidate<T>(
object? value, object? value,
Type targetType, Type targetType,
@ -24,7 +24,7 @@ namespace Avalonia.PropertyStore
return v; return v;
} }
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConversionSupressWarningMessage)]
public static bool TryConvertAndValidate<T>( public static bool TryConvertAndValidate<T>(
StyledProperty<T> property, StyledProperty<T> property,
object? value, 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) 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); 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) private bool ShouldSetValue(AvaloniaObject target, object? value, [NotNullWhen(true)] out TValue? converted)
{ {
if (value == BindingOperations.DoNothing) 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? MovePrevious() => _previous;
private protected override Selector? MovePreviousOrParent() => _previous; private protected override Selector? MovePreviousOrParent() => _previous;
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.TypeConvertionSupressWarningMessage)] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.TypeConversionSupressWarningMessage)]
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = TrimmingMessages.TypeConvertionSupressWarningMessage)] [UnconditionalSuppressMessage("Trimming", "IL2067", Justification = TrimmingMessages.TypeConversionSupressWarningMessage)]
internal static bool Compare(Type propertyType, object? propertyValue, object? value) internal static bool Compare(Type propertyType, object? propertyValue, object? value)
{ {
if (propertyType == typeof(object) && if (propertyType == typeof(object) &&

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

@ -65,7 +65,7 @@ namespace Avalonia.Styling
void IValueEntry.Unsubscribe() { } void IValueEntry.Unsubscribe() { }
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConversionSupressWarningMessage)]
internal override ISetterInstance Instance(IStyleInstance instance, StyledElement target) internal override ISetterInstance Instance(IStyleInstance instance, StyledElement target)
{ {
if (target is not AvaloniaObject ao) 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="culture">The culture to use.</param>
/// <param name="result">If successful, contains the convert value.</param> /// <param name="result">If successful, contains the convert value.</param>
/// <returns>True if the cast was successful, otherwise false.</returns> /// <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) public static bool TryConvert(Type to, object? value, CultureInfo? culture, out object? result)
{ {
if (value == null) if (value == null)
@ -246,7 +246,7 @@ namespace Avalonia.Utilities
/// <param name="value">The value to convert.</param> /// <param name="value">The value to convert.</param>
/// <param name="result">If successful, contains the converted value.</param> /// <param name="result">If successful, contains the converted value.</param>
/// <returns>True if the convert was successful, otherwise false.</returns> /// <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) public static bool TryConvertImplicit(Type to, object? value, out object? result)
{ {
if (value == null) if (value == null)
@ -309,7 +309,7 @@ namespace Avalonia.Utilities
/// <param name="type">The type to convert to..</param> /// <param name="type">The type to convert to..</param>
/// <param name="culture">The culture to use.</param> /// <param name="culture">The culture to use.</param>
/// <returns>A value of <paramref name="type"/>.</returns> /// <returns>A value of <paramref name="type"/>.</returns>
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)] [RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]
public static object? ConvertOrDefault(object? value, Type type, CultureInfo culture) public static object? ConvertOrDefault(object? value, Type type, CultureInfo culture)
{ {
return TryConvert(type, value, culture, out var result) ? result : Default(type); 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="value">The value to convert.</param>
/// <param name="type">The type to convert to.</param> /// <param name="type">The type to convert to.</param>
/// <returns>A value of <paramref name="type"/>.</returns> /// <returns>A value of <paramref name="type"/>.</returns>
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)] [RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConversionRequiresUnreferencedCodeMessage)]
public static object? ConvertImplicitOrDefault(object? value, Type type) public static object? ConvertImplicitOrDefault(object? value, Type type)
{ {
return TryConvertImplicit(type, value, out var result) ? result : Default(type); return TryConvertImplicit(type, value, out var result) ? result : Default(type);
} }
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)] [RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConversionRequiresUnreferencedCodeMessage)]
public static T ConvertImplicit<T>(object? value) public static T ConvertImplicit<T>(object? value)
{ {
if (TryConvertImplicit(typeof(T), value, out var result)) 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 /// Computes the <see cref="HasMirrorTransform"/> value according to the
/// <see cref="FlowDirection"/> and <see cref="BypassFlowDirectionPolicies"/> /// <see cref="FlowDirection"/> and <see cref="BypassFlowDirectionPolicies"/>
/// </summary> /// </summary>
public virtual void InvalidateMirrorTransform() protected internal virtual void InvalidateMirrorTransform()
{ {
var flowDirection = this.FlowDirection; var flowDirection = this.FlowDirection;
var parentFlowDirection = FlowDirection.LeftToRight; var parentFlowDirection = FlowDirection.LeftToRight;

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

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

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

@ -31,11 +31,31 @@ namespace Avalonia.Automation.Peers
return CreatePeerForElement(element); 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) public static AutomationPeer CreatePeerForElement(Control element)
{ {
return element.GetOrCreateAutomationPeer(); 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 void BringIntoViewCore() => Owner.BringIntoView();
protected override IReadOnlyList<AutomationPeer> GetOrCreateChildrenCore() protected override IReadOnlyList<AutomationPeer> GetOrCreateChildrenCore()
@ -151,7 +171,7 @@ namespace Avalonia.Automation.Peers
protected override bool HasKeyboardFocusCore() => Owner.IsFocused; protected override bool HasKeyboardFocusCore() => Owner.IsFocused;
protected override bool IsContentElementCore() => true; protected override bool IsContentElementCore() => true;
protected override bool IsControlElementCore() => 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 bool IsKeyboardFocusableCore() => Owner.Focusable;
protected override void SetFocusCore() => Owner.Focus(); protected override void SetFocusCore() => Owner.Focus();

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

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

90
src/Avalonia.Controls/Border.cs

@ -47,30 +47,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<BoxShadows> BoxShadowProperty = public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
AvaloniaProperty.Register<Border, BoxShadows>(nameof(BoxShadow)); 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 readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
private Thickness? _layoutThickness; private Thickness? _layoutThickness;
private double _scale; private double _scale;
@ -86,10 +62,6 @@ namespace Avalonia.Controls
BorderBrushProperty, BorderBrushProperty,
BorderThicknessProperty, BorderThicknessProperty,
CornerRadiusProperty, CornerRadiusProperty,
BorderDashArrayProperty,
BorderLineCapProperty,
BorderLineJoinProperty,
BorderDashOffsetProperty,
BoxShadowProperty); BoxShadowProperty);
AffectsMeasure<Border>(BorderThicknessProperty); AffectsMeasure<Border>(BorderThicknessProperty);
} }
@ -115,8 +87,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public IBrush? Background public IBrush? Background
{ {
get { return GetValue(BackgroundProperty); } get => GetValue(BackgroundProperty);
set { SetValue(BackgroundProperty, value); } set => SetValue(BackgroundProperty, value);
} }
/// <summary> /// <summary>
@ -124,17 +96,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public IBrush? BorderBrush public IBrush? BorderBrush
{ {
get { return GetValue(BorderBrushProperty); } get => GetValue(BorderBrushProperty);
set { SetValue(BorderBrushProperty, value); } 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); }
} }
/// <summary> /// <summary>
@ -142,35 +105,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public Thickness BorderThickness public Thickness BorderThickness
{ {
get { return GetValue(BorderThicknessProperty); } get => GetValue(BorderThicknessProperty);
set { SetValue(BorderThicknessProperty, value); } 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); }
} }
/// <summary> /// <summary>
@ -178,8 +114,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public CornerRadius CornerRadius public CornerRadius CornerRadius
{ {
get { return GetValue(CornerRadiusProperty); } get => GetValue(CornerRadiusProperty);
set { SetValue(CornerRadiusProperty, value); } set => SetValue(CornerRadiusProperty, value);
} }
/// <summary> /// <summary>
@ -227,8 +163,14 @@ namespace Avalonia.Controls
/// <param name="context">The drawing context.</param> /// <param name="context">The drawing context.</param>
public sealed override void Render(DrawingContext context) public sealed override void Render(DrawingContext context)
{ {
_borderRenderHelper.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush, _borderRenderHelper.Render(
BoxShadow, BorderDashOffset, BorderLineCap, BorderLineJoin, BorderDashArray); context,
Bounds.Size,
LayoutThickness,
CornerRadius,
Background,
BorderBrush,
BoxShadow);
} }
/// <summary> /// <summary>

4
src/Avalonia.Controls/Button.cs

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

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

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

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

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

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

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

20
src/Avalonia.Controls/ComboBox.cs

@ -21,8 +21,9 @@ namespace Avalonia.Controls
[PseudoClasses(pcDropdownOpen, pcPressed)] [PseudoClasses(pcDropdownOpen, pcPressed)]
public class ComboBox : SelectingItemsControl public class ComboBox : SelectingItemsControl
{ {
public const string pcDropdownOpen = ":dropdownopen"; internal const string pcDropdownOpen = ":dropdownopen";
public const string pcPressed = ":pressed"; internal const string pcPressed = ":pressed";
/// <summary> /// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property. /// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
/// </summary> /// </summary>
@ -164,7 +165,7 @@ namespace Avalonia.Controls
UpdateSelectionBoxItem(SelectedItem); UpdateSelectionBoxItem(SelectedItem);
} }
public override void InvalidateMirrorTransform() protected internal override void InvalidateMirrorTransform()
{ {
base.InvalidateMirrorTransform(); base.InvalidateMirrorTransform();
UpdateFlowDirection(); UpdateFlowDirection();
@ -360,19 +361,6 @@ namespace Avalonia.Controls
_subscriptionsOnOpen.Clear(); _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); this.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
foreach (var parent in this.GetVisualAncestors().OfType<Control>()) foreach (var parent in this.GetVisualAncestors().OfType<Control>())

21
src/Avalonia.Controls/ContentControl.cs

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

6
src/Avalonia.Controls/Control.cs

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

3
src/Avalonia.Controls/GridSplitter.cs

@ -695,7 +695,8 @@ namespace Avalonia.Controls
{ {
private readonly TranslateTransform _translation; private readonly TranslateTransform _translation;
private readonly Decorator _decorator; private readonly Decorator _decorator;
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", Justification = "Private object")]
public PreviewAdorner(Control? previewControl) public PreviewAdorner(Control? previewControl)
{ {
// Add a decorator to perform translations. // 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 /// Defines a control that displays <see cref="Content"/> according to a
/// <see cref="Avalonia.Controls.Templates.FuncDataTemplate"/>. /// <see cref="Avalonia.Controls.Templates.FuncDataTemplate"/>.
/// </summary> /// </summary>
[NotClientImplementable] internal interface IContentControl
public interface IContentControl
{ {
/// <summary> /// <summary>
/// Gets or sets the content to display. /// Gets or sets the content to display.

2
src/Avalonia.Controls/IHeadered.cs

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

3
src/Avalonia.Controls/IMenu.cs

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

3
src/Avalonia.Controls/IMenuElement.cs

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

3
src/Avalonia.Controls/IMenuItem.cs

@ -5,8 +5,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Represents a <see cref="MenuItem"/>. /// Represents a <see cref="MenuItem"/>.
/// </summary> /// </summary>
[NotClientImplementable] internal interface IMenuItem : IMenuElement
public interface IMenuItem : IMenuElement
{ {
/// <summary> /// <summary>
/// Gets or sets a value that indicates whether the item has a submenu. /// 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 namespace Avalonia.Controls
{ {
[Unstable] [PrivateApi]
public interface INativeMenuExporterEventsImplBridge public interface INativeMenuExporterEventsImplBridge
{ {
void RaiseNeedsUpdate (); void RaiseNeedsUpdate ();

24
src/Avalonia.Controls/ItemsControl.cs

@ -365,6 +365,30 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public IEnumerable<Control> GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty<Control>(); 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> /// <summary>
/// Creates or a container that can be used to display an item. /// Creates or a container that can be used to display an item.
/// </summary> /// </summary>

70
src/Avalonia.Controls/ListBox.cs

@ -1,4 +1,8 @@
using System;
using System.Collections; using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection; using Avalonia.Controls.Selection;
@ -47,7 +51,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1010", [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1010",
Justification = "This property is owned by SelectingItemsControl, but protected there. ListBox changes its visibility.")] 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; SelectingItemsControl.SelectionModeProperty;
private IScrollable? _scroll; private IScrollable? _scroll;
@ -121,41 +125,36 @@ namespace Avalonia.Controls
return NeedsContainer<ListBoxItem>(item, out recycleKey); return NeedsContainer<ListBoxItem>(item, out recycleKey);
} }
/// <inheritdoc/> protected override void OnKeyDown(KeyEventArgs e)
protected override void OnGotFocus(GotFocusEventArgs 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, e.Source,
true, true,
e.KeyModifiers.HasAllFlags(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Shift),
e.KeyModifiers.HasAllFlags(KeyModifiers.Control), ctrl);
fromFocus: true);
} }
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (e.Source is Visual source) base.OnKeyDown(e);
{
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);
}
}
} }
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@ -163,5 +162,18 @@ namespace Avalonia.Controls
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer"); 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.Metadata;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Platform;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -17,6 +19,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> IsSelectedProperty = public static readonly StyledProperty<bool> IsSelectedProperty =
SelectingItemsControl.IsSelectedProperty.AddOwner<ListBoxItem>(); SelectingItemsControl.IsSelectedProperty.AddOwner<ListBoxItem>();
private static readonly Point s_invalidPoint = new Point(double.NaN, double.NaN);
private Point _pointerDownPoint = s_invalidPoint;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ListBoxItem"/> class. /// Initializes static members of the <see cref="ListBoxItem"/> class.
/// </summary> /// </summary>
@ -40,5 +45,67 @@ namespace Avalonia.Controls
{ {
return new ListItemAutomationPeer(this); 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); base.OnAttachedToVisualTree(e);
var inputRoot = e.Root as IInputRoot; var inputRoot = e.Root as TopLevel;
if (inputRoot?.AccessKeyHandler != null) if (inputRoot?.AccessKeyHandler != null)
{ {

2
src/Avalonia.Controls/MenuItemAccessKeyHandler.cs

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

2
src/Avalonia.Controls/NativeMenuItemSeparator.cs

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

2
src/Avalonia.Controls/Panel.cs

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

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

@ -39,91 +39,20 @@ namespace Avalonia.Controls.Platform
DelayRun = delayRun; DelayRun = delayRun;
} }
public virtual void Attach(IMenu menu) public void Attach(MenuBase menu) => AttachCore(menu);
{ public void Detach(MenuBase menu) => DetachCore(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;
}
protected Action<Action, TimeSpan> DelayRun { get; } protected Action<Action, TimeSpan> DelayRun { get; }
protected IInputManager? InputManager { 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 static TimeSpan MenuShowDelay { get; } = TimeSpan.FromMilliseconds(400);
protected internal virtual void GotFocus(object? sender, GotFocusEventArgs e) 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) if (item?.Parent != null)
{ {
@ -133,7 +62,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void LostFocus(object? sender, RoutedEventArgs e) 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) if (item != null)
{ {
@ -143,146 +72,12 @@ namespace Avalonia.Controls.Platform
protected internal virtual void KeyDown(object? sender, KeyEventArgs e) protected internal virtual void KeyDown(object? sender, KeyEventArgs e)
{ {
KeyDown(GetMenuItem(e.Source as Control), e); KeyDown(GetMenuItemCore(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);
}
} }
protected internal virtual void AccessKeyPressed(object? sender, RoutedEventArgs 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) if (item == null)
{ {
@ -303,7 +98,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerEntered(object? sender, RoutedEventArgs e) 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) if (item?.Parent == null)
{ {
@ -349,7 +144,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerMoved(object? sender, PointerEventArgs e) protected internal virtual void PointerMoved(object? sender, PointerEventArgs e)
{ {
// HACK: #8179 needs to be addressed to correctly implement it in the PointerPressed method. // 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) if (item == null)
return; return;
@ -370,7 +165,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerExited(object? sender, RoutedEventArgs e) 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) if (item?.Parent == null)
{ {
@ -405,7 +200,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerPressed(object? sender, PointerPressedEventArgs e) 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 && if (sender is Visual visual &&
e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed && item?.HasSubMenu == true) 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) 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) if (e.InitialPressMouseButton == MouseButton.Left && item?.HasSubMenu == false)
{ {
@ -478,13 +273,84 @@ namespace Avalonia.Controls.Platform
{ {
Menu?.Close(); 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(); item.RaiseClick();
@ -494,7 +360,7 @@ namespace Avalonia.Controls.Platform
} }
} }
protected void CloseMenu(IMenuItem item) internal void CloseMenu(IMenuItem item)
{ {
var current = (IMenuElement?)item; var current = (IMenuElement?)item;
@ -506,7 +372,7 @@ namespace Avalonia.Controls.Platform
current?.Close(); current?.Close();
} }
protected void CloseWithDelay(IMenuItem item) internal void CloseWithDelay(IMenuItem item)
{ {
void Execute() void Execute()
{ {
@ -519,7 +385,141 @@ namespace Avalonia.Controls.Platform
DelayRun(Execute, MenuShowDelay); 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(); item.Open();
@ -529,7 +529,7 @@ namespace Avalonia.Controls.Platform
} }
} }
protected void OpenWithDelay(IMenuItem item) internal void OpenWithDelay(IMenuItem item)
{ {
void Execute() void Execute()
{ {
@ -542,7 +542,7 @@ namespace Avalonia.Controls.Platform
DelayRun(Execute, MenuShowDelay); DelayRun(Execute, MenuShowDelay);
} }
protected void SelectItemAndAncestors(IMenuItem item) internal void SelectItemAndAncestors(IMenuItem item)
{ {
var current = (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) while (true)
{ {
@ -565,6 +565,11 @@ namespace Avalonia.Controls.Platform
} }
} }
private void TopLevelLostPlatformFocus()
{
Menu?.Close();
}
private static void DefaultDelayRun(Action action, TimeSpan timeSpan) private static void DefaultDelayRun(Action action, TimeSpan timeSpan)
{ {
DispatcherTimer.RunOnce(action, 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. /// Attaches the interaction handler to a menu.
/// </summary> /// </summary>
/// <param name="menu">The menu.</param> /// <param name="menu">The menu.</param>
void Attach(IMenu menu); void Attach(MenuBase menu);
/// <summary> /// <summary>
/// Detaches the interaction handler from the attached menu. /// Detaches the interaction handler from the attached menu.
/// </summary> /// </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. /// Presents a single item of data inside a <see cref="TemplatedControl"/> template.
/// </summary> /// </summary>
[PseudoClasses(":empty")] [PseudoClasses(":empty")]
public class ContentPresenter : Control, IContentPresenter public class ContentPresenter : Control
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Background"/> property. /// Defines the <see cref="Background"/> property.
@ -540,17 +540,6 @@ namespace Avalonia.Controls.Presenters
BoxShadow); 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) private Control? CreateChild(object? content, Control? oldChild, IDataTemplate? template)
{ {
var newChild = content as Control; 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.Collections;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
@ -16,8 +14,7 @@ namespace Avalonia.Controls.Presenters
/// parent control's template is instantiated so they register themselves using this /// parent control's template is instantiated so they register themselves using this
/// interface. /// interface.
/// </remarks> /// </remarks>
[NotClientImplementable] internal interface IContentPresenterHost
public interface IContentPresenterHost
{ {
/// <summary> /// <summary>
/// Gets a collection describing the logical children of the host control. /// Gets a collection describing the logical children of the host control.
@ -25,13 +22,13 @@ namespace Avalonia.Controls.Presenters
IAvaloniaList<ILogical> LogicalChildren { get; } IAvaloniaList<ILogical> LogicalChildren { get; }
/// <summary> /// <summary>
/// Registers an <see cref="IContentPresenter"/> with a host control. /// Registers an <see cref="ContentPresenter"/> with a host control.
/// </summary> /// </summary>
/// <param name="presenter">The content presenter.</param> /// <param name="presenter">The content presenter.</param>
/// <returns> /// <returns>
/// True if the content presenter should add its child to the logical children of the /// True if the content presenter should add its child to the logical children of the
/// host; otherwise false. /// host; otherwise false.
/// </returns> /// </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> /// <summary>
/// Presents a scrolling view of content inside a <see cref="ScrollViewer"/>. /// Presents a scrolling view of content inside a <see cref="ScrollViewer"/>.
/// </summary> /// </summary>
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider public class ScrollContentPresenter : ContentPresenter, IScrollable, IScrollAnchorProvider
{ {
private const double EdgeDetectionTolerance = 0.1; 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) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnAttachedToVisualTree(e); base.OnAttachedToVisualTree(e);
_accessKeys = (e.Root as IInputRoot)?.AccessKeyHandler; _accessKeys = (e.Root as TopLevel)?.AccessKeyHandler;
if (_accessKeys != null && AccessKey != 0) if (_accessKeys != null && AccessKey != 0)
{ {

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

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

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

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

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

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

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

@ -52,7 +52,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Gets the presenter from the control's template. /// Gets the presenter from the control's template.
/// </summary> /// </summary>
IContentPresenter? Presenter { get; } ContentPresenter? Presenter { get; }
/// <summary> /// <summary>
/// Gets or sets whether the popup appears on top of all other windows. /// 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 /> /// <inheritdoc />
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", Justification = "Explicit set")]
public void SetChild(Control? control) public void SetChild(Control? control)
{ {
Content = control; Content = control;

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

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
@ -42,7 +43,23 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<RangeBase, double>(nameof(LargeChange), 10); AvaloniaProperty.Register<RangeBase, double>(nameof(LargeChange), 10);
/// <summary> /// <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> /// </summary>
public double Minimum public double Minimum
{ {
@ -65,7 +82,7 @@ namespace Avalonia.Controls.Primitives
} }
/// <summary> /// <summary>
/// Gets or sets the maximum value. /// Gets or sets the maximum possible value.
/// </summary> /// </summary>
public double Maximum public double Maximum
{ {
@ -104,18 +121,25 @@ namespace Avalonia.Controls.Primitives
: sender.GetValue(ValueProperty); : sender.GetValue(ValueProperty);
} }
/// <summary>
/// Gets or sets the small increment value added or subtracted from the <see cref="Value"/>.
/// </summary>
public double SmallChange public double SmallChange
{ {
get => GetValue(SmallChangeProperty); get => GetValue(SmallChangeProperty);
set => SetValue(SmallChangeProperty, value); set => SetValue(SmallChangeProperty, value);
} }
/// <summary>
/// Gets or sets the large increment value added or subtracted from the <see cref="Value"/>.
/// </summary>
public double LargeChange public double LargeChange
{ {
get => GetValue(LargeChangeProperty); get => GetValue(LargeChangeProperty);
set => SetValue(LargeChangeProperty, value); set => SetValue(LargeChangeProperty, value);
} }
/// <inheritdoc/>
protected override void OnInitialized() protected override void OnInitialized()
{ {
base.OnInitialized(); base.OnInitialized();
@ -124,6 +148,7 @@ namespace Avalonia.Controls.Primitives
CoerceValue(ValueProperty); CoerceValue(ValueProperty);
} }
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
@ -136,6 +161,14 @@ namespace Avalonia.Controls.Primitives
{ {
OnMaximumChanged(); OnMaximumChanged();
} }
else if (change.Property == ValueProperty)
{
var valueChangedEventArgs = new RangeBaseValueChangedEventArgs(
change.GetOldValue<double>(),
change.GetNewValue<double>(),
ValueChangedEvent);
RaiseEvent(valueChangedEventArgs);
}
} }
/// <summary> /// <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); 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 /> /// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
@ -729,11 +697,16 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
/// <param name="direction">The direction to move.</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="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> /// <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; var focused = FocusManager.GetFocusManager(this)?.GetFocusedElement();
return MoveSelection(from, direction, wrap); var from = GetContainerFromEventSource(focused) ?? ContainerFromIndex(Selection.AnchorIndex);
return MoveSelection(from, direction, wrap, rangeModifier);
} }
/// <summary> /// <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="from">The container which serves as a starting point for the movement.</param>
/// <param name="direction">The direction to move.</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="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> /// <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 && if (Presenter?.Panel is not INavigableContainer container)
GetNextControl(container, direction, from, wrap) is Control next) 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); var index = IndexFromContainer(next);
if (index != -1) if (index != -1)
{ {
SelectedIndex = index; UpdateSelection(index, true, rangeModifier);
next.Focus();
return true; return true;
} }
} }

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

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

2
src/Avalonia.Controls/ScrollViewer.cs

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

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

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

18
src/Avalonia.Controls/Slider.cs

@ -86,10 +86,10 @@ namespace Avalonia.Controls
TickBar.TicksProperty.AddOwner<Slider>(); TickBar.TicksProperty.AddOwner<Slider>();
// Slider required parts // Slider required parts
protected bool _isDragging; private bool _isDragging;
protected Track? _track; private Track? _track;
protected Button? _decreaseButton; private Button? _decreaseButton;
protected Button? _increaseButton; private Button? _increaseButton;
private IDisposable? _decreaseButtonPressDispose; private IDisposable? _decreaseButtonPressDispose;
private IDisposable? _decreaseButtonReleaseDispose; private IDisposable? _decreaseButtonReleaseDispose;
private IDisposable? _increaseButtonSubscription; private IDisposable? _increaseButtonSubscription;
@ -181,6 +181,16 @@ namespace Avalonia.Controls
set { SetValue(TickPlacementProperty, value); } 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/> /// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {

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

@ -18,9 +18,9 @@ namespace Avalonia.Controls
[PseudoClasses(pcFlyoutOpen, pcPressed)] [PseudoClasses(pcFlyoutOpen, pcPressed)]
public class SplitButton : ContentControl, ICommandSource public class SplitButton : ContentControl, ICommandSource
{ {
protected const string pcChecked = ":checked"; internal const string pcChecked = ":checked";
protected const string pcPressed = ":pressed"; internal const string pcPressed = ":pressed";
protected const string pcFlyoutOpen = ":flyout-open"; internal const string pcFlyoutOpen = ":flyout-open";
/// <summary> /// <summary>
/// Raised when the user presses the primary part of the <see cref="SplitButton"/>. /// 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)] [PseudoClasses(pcLightDismiss)]
public class SplitView : ContentControl public class SplitView : ContentControl
{ {
protected const string pcOpen = ":open"; private const string pcOpen = ":open";
protected const string pcClosed = ":closed"; private const string pcClosed = ":closed";
protected const string pcCompactOverlay = ":compactoverlay"; private const string pcCompactOverlay = ":compactoverlay";
protected const string pcCompactInline = ":compactinline"; private const string pcCompactInline = ":compactinline";
protected const string pcOverlay = ":overlay"; private const string pcOverlay = ":overlay";
protected const string pcInline = ":inline"; private const string pcInline = ":inline";
protected const string pcLeft = ":left"; private const string pcLeft = ":left";
protected const string pcRight = ":right"; private const string pcRight = ":right";
protected const string pcLightDismiss = ":lightDismiss"; private const string pcLightDismiss = ":lightDismiss";
/// <summary> /// <summary>
/// Defines the <see cref="CompactPaneLength"/> property /// Defines the <see cref="CompactPaneLength"/> property
@ -279,7 +279,7 @@ namespace Avalonia.Controls
remove => RemoveHandler(PaneOpeningEvent, value); remove => RemoveHandler(PaneOpeningEvent, value);
} }
protected override bool RegisterContentPresenter(IContentPresenter presenter) protected override bool RegisterContentPresenter(ContentPresenter presenter)
{ {
var result = base.RegisterContentPresenter(presenter); var result = base.RegisterContentPresenter(presenter);

72
src/Avalonia.Controls/TabControl.cs

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

42
src/Avalonia.Controls/TabItem.cs

@ -1,22 +1,28 @@
using System;
using Avalonia.Automation; using Avalonia.Automation;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// An item in a <see cref="TabStrip"/> or <see cref="TabControl"/>. /// An item in a <see cref="TabControl"/>.
/// </summary> /// </summary>
[PseudoClasses(":pressed", ":selected")] [PseudoClasses(":pressed", ":selected")]
public class TabItem : HeaderedContentControl, ISelectable public class TabItem : HeaderedContentControl, ISelectable
{ {
private Dock? _tabStripPlacement;
private IDisposable? _ownerSubscriptions;
/// <summary> /// <summary>
/// Defines the <see cref="TabStripPlacement"/> property. /// Defines the <see cref="TabStripPlacement"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<Dock> TabStripPlacementProperty = public static readonly DirectProperty<TabItem, Dock?> TabStripPlacementProperty =
TabControl.TabStripPlacementProperty.AddOwner<TabItem>(); AvaloniaProperty.RegisterDirect<TabItem, Dock?>(nameof(TabStripPlacement), o => o.TabStripPlacement);
/// <summary> /// <summary>
/// Defines the <see cref="IsSelected"/> property. /// Defines the <see cref="IsSelected"/> property.
@ -37,16 +43,12 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Gets the tab strip placement. /// Gets the placement of this tab relative to the outer <see cref="TabControl"/>, if there is one.
/// </summary> /// </summary>
/// <value> public Dock? TabStripPlacement
/// 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
{ {
get { return GetValue(TabStripPlacementProperty); } get => _tabStripPlacement;
private set => SetAndRaise(TabStripPlacementProperty, ref _tabStripPlacement, value);
} }
/// <summary> /// <summary>
@ -60,6 +62,24 @@ namespace Avalonia.Controls
protected override AutomationPeer OnCreateAutomationPeer() => new ListItemAutomationPeer(this); 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) private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj)
{ {
if (Header == null) if (Header == null)

9
src/Avalonia.Controls/TextBlock.cs

@ -144,8 +144,8 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<TextBlock, InlineCollection?>( AvaloniaProperty.RegisterDirect<TextBlock, InlineCollection?>(
nameof(Inlines), t => t.Inlines, (t, v) => t.Inlines = v); nameof(Inlines), t => t.Inlines, (t, v) => t.Inlines = v);
protected TextLayout? _textLayout; private TextLayout? _textLayout;
protected Size _constraint; private Size _constraint;
private IReadOnlyList<TextRun>? _textRuns; private IReadOnlyList<TextRun>? _textRuns;
private InlineCollection? _inlines; private InlineCollection? _inlines;
@ -639,8 +639,7 @@ namespace Avalonia.Controls
TextTrimming, TextTrimming,
_constraint.Width, _constraint.Width,
_constraint.Height, _constraint.Height,
maxLines: MaxLines, MaxLines);
lineHeight: LineHeight);
} }
/// <summary> /// <summary>
@ -829,7 +828,7 @@ namespace Avalonia.Controls
InvalidateTextLayout(); InvalidateTextLayout();
} }
protected readonly record struct SimpleTextSource : ITextSource private readonly record struct SimpleTextSource : ITextSource
{ {
private readonly string _text; private readonly string _text;
private readonly TextRunProperties _defaultProperties; 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.Media.TextFormatting.Unicode;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Platform;
using System.Reflection;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -28,6 +25,7 @@ namespace Avalonia.Controls
/// Represents a control that can be used to display or edit unformatted text. /// Represents a control that can be used to display or edit unformatted text.
/// </summary> /// </summary>
[TemplatePart("PART_TextPresenter", typeof(TextPresenter))] [TemplatePart("PART_TextPresenter", typeof(TextPresenter))]
[TemplatePart("PART_ScrollViewer", typeof(ScrollViewer))]
[PseudoClasses(":empty")] [PseudoClasses(":empty")]
public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost
{ {
@ -158,7 +156,7 @@ namespace Avalonia.Controls
/// Defines see <see cref="TextPresenter.LineHeight"/> property. /// Defines see <see cref="TextPresenter.LineHeight"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<double> LineHeightProperty = public static readonly StyledProperty<double> LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner<TextBox>(); TextBlock.LineHeightProperty.AddOwner<TextBox>(new(defaultValue: double.NaN));
/// <summary> /// <summary>
/// Defines see <see cref="TextBlock.LetterSpacing"/> property. /// Defines see <see cref="TextBlock.LetterSpacing"/> property.
@ -310,6 +308,7 @@ namespace Avalonia.Controls
} }
private TextPresenter? _presenter; private TextPresenter? _presenter;
private ScrollViewer? _scrollViewer;
private readonly TextBoxTextInputMethodClient _imClient = new(); private readonly TextBoxTextInputMethodClient _imClient = new();
private readonly UndoRedoHelper<UndoRedoState> _undoRedoHelper; private readonly UndoRedoHelper<UndoRedoState> _undoRedoHelper;
private bool _isUndoingRedoing; private bool _isUndoingRedoing;
@ -490,7 +489,7 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Gets or sets the maximum character length of the TextBox /// Gets or sets the maximum number of visible lines.
/// </summary> /// </summary>
public int MaxLength public int MaxLength
{ {
@ -803,6 +802,8 @@ namespace Avalonia.Controls
{ {
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter"); _presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
_scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
_imClient.SetPresenter(_presenter, this); _imClient.SetPresenter(_presenter, this);
if (IsFocused) if (IsFocused)
@ -855,6 +856,10 @@ namespace Avalonia.Controls
{ {
OnSelectionEndChanged(change); OnSelectionEndChanged(change);
} }
else if (change.Property == MaxLinesProperty)
{
InvalidateMeasure();
}
else if (change.Property == UndoLimitProperty) else if (change.Property == UndoLimitProperty)
{ {
OnUndoLimitChanged(change.GetNewValue<int>()); OnUndoLimitChanged(change.GetNewValue<int>());
@ -942,40 +947,10 @@ namespace Avalonia.Controls
{ {
return; return;
} }
_selectedTextChangesMadeSinceLastUndoSnapshot++; _selectedTextChangesMadeSinceLastUndoSnapshot++;
SnapshotUndoRedo(ignoreChangeCount: false); 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 currentText = Text ?? string.Empty;
var selectionLength = Math.Abs(SelectionStart - SelectionEnd); var selectionLength = Math.Abs(SelectionStart - SelectionEnd);
var newLength = input.Length + currentText.Length - selectionLength; 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) if (text is null)
return null; return null;
@ -1518,7 +1493,7 @@ namespace Avalonia.Controls
_presenter.MoveCaretToPoint(point); _presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex; var caretIndex = _presenter.CaretIndex;
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
@ -1976,5 +1951,47 @@ namespace Avalonia.Controls
{ {
CanRedo = _undoRedoHelper.CanRedo; 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 namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Provides data specific to a TextChanged event. /// Provides data specific to a <see cref="TextBox.TextChanged"/> event.
/// </summary> /// </summary>
public class TextChangedEventArgs : RoutedEventArgs 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) public TextChangedEventArgs(RoutedEvent? routedEvent)
: base (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) public TextChangedEventArgs(RoutedEvent? routedEvent, Interactive? source)
: base(routedEvent, source) : base(routedEvent, source)
{ {

11
src/Avalonia.Controls/TextChangingEventArgs.cs

@ -3,15 +3,24 @@
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Provides data specific to a TextChanging event. /// Provides data specific to a <see cref="TextBox.TextChanging"/> event.
/// </summary> /// </summary>
public class TextChangingEventArgs : RoutedEventArgs 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) public TextChangingEventArgs(RoutedEvent? routedEvent)
: base (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) public TextChangingEventArgs(RoutedEvent? routedEvent, Interactive? source)
: base(routedEvent, source) : base(routedEvent, source)
{ {

8
src/Avalonia.Controls/ToggleSwitch.cs

@ -86,13 +86,13 @@ namespace Avalonia.Controls
set { SetValue(OffContentProperty, value); } set { SetValue(OffContentProperty, value); }
} }
public IContentPresenter? OffContentPresenter public ContentPresenter? OffContentPresenter
{ {
get; get;
private set; private set;
} }
public IContentPresenter? OnContentPresenter public ContentPresenter? OnContentPresenter
{ {
get; get;
private set; 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); 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)) if ((_switchKnob != null) && (_knobsPanel != null))
{ {

25
src/Avalonia.Controls/TopLevel.cs

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

120
src/Avalonia.Controls/VirtualizingStackPanel.cs

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

4
src/Avalonia.Controls/Window.cs

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

12
src/Avalonia.Controls/WindowBase.cs

@ -60,7 +60,7 @@ namespace Avalonia.Controls
impl.PositionChanged = HandlePositionChanged; impl.PositionChanged = HandlePositionChanged;
} }
protected IDisposable FreezeVisibilityChangeHandling() private protected IDisposable FreezeVisibilityChangeHandling()
{ {
return new IgnoreVisibilityChangesDisposable(this); 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> /// <summary>
/// Ensures that the window is initialized. /// 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> /// <param name="e">An <see cref="EventArgs"/> that contains the event data.</param>
protected virtual void OnResized(WindowResizedEventArgs e) => Resized?.Invoke(this, e); protected virtual void OnResized(WindowResizedEventArgs e) => Resized?.Invoke(this, e);
protected override void HandleClosed() private protected override void HandleClosed()
{ {
using (FreezeVisibilityChangeHandling()) using (FreezeVisibilityChangeHandling())
{ {

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

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

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

Loading…
Cancel
Save