Browse Source

Merge branch 'master' into linux-fb-evdev-touch

pull/5894/head
Nikita Tsukanov 5 years ago
committed by GitHub
parent
commit
33438ea831
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      Documentation/build.md
  2. 4
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  3. 22
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs
  4. 2
      src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
  5. 10
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
  6. 9
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
  7. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
  8. 160
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
  9. 15
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
  10. 17
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  11. 135
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
  12. 57
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
  13. 140
      src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
  14. 2
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  15. 2
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  16. 18
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  17. 43
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs

22
Documentation/build.md

@ -9,10 +9,24 @@ git clone https://github.com/AvaloniaUI/Avalonia.git
git submodule update --init
```
### Install the required version of the .NET Core SDK
Go to https://dotnet.microsoft.com/download/visual-studio-sdks and install the latest version of the .NET Core SDK compatible with Avalonia UI. Make sure to download the SDK (not just the "runtime") package. The version compatible is indicated within the [global.json](https://github.com/AvaloniaUI/Avalonia/blob/master/global.json) file. Note that Avalonia UI does not always use the latest version and is hardcoded to use the last version known to be compatible (SDK releases may break the builds from time-to-time).
### Open in Visual Studio
Open the `Avalonia.sln` solution in Visual Studio 2019 or newer. The free Visual Studio Community
edition works fine. Run the `Samples\ControlCatalog.Desktop` project to see the sample application.
Open the `Avalonia.sln` solution in Visual Studio 2019 or newer. The free Visual Studio Community edition works fine. Build and run the `Samples\ControlCatalog.Desktop` or `ControlCatalog.NetCore` project to see the sample application.
### Troubleshooting
* **Error CS0006: Avalonia.DesktopRuntime.dll could not be found**
It is common for the first build to fail with the errors below (also discussed in [#4257](https://github.com/AvaloniaUI/Avalonia/issues/4257)).
```
>CSC : error CS0006: Metadata file 'C:\...\Avalonia\src\Avalonia.DesktopRuntime\bin\Debug\netcoreapp2.0\Avalonia.DesktopRuntime.dll' could not be found
>CSC : error CS0006: Metadata file 'C:\...\Avalonia\packages\Avalonia\bin\Debug\netcoreapp2.0\Avalonia.dll' could not be found
```
To correct this, right click on the `Avalonia.DesktopRuntime` project then press `Build` to build the project manually. Afterwards the solution should build normally and the ControlCatalog can be run.
# Linux/macOS
@ -20,9 +34,9 @@ It's *not* possible to build the *whole* project on Linux/macOS. You can only bu
MonoDevelop, Xamarin Studio and Visual Studio for Mac aren't capable of properly opening our solution. You can use Rider (at least 2017.2 EAP) or VSCode instead. They will fail to load most of platform specific projects, but you don't need them to run on .NET Core.
### Install the latest version of .NET Core
### Install the latest version of the .NET Core SDK
Go to https://www.microsoft.com/net/core and follow instructions for your OS. You need SDK (not just "runtime") package.
Go to https://www.microsoft.com/net/core and follow the instructions for your OS. Make sure to download the SDK (not just the "runtime") package.
### Additional requirements for macOS

4
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -163,6 +163,10 @@
<Style Selector="DataGridCell:invalid /template/ Rectangle#InvalidVisualElement">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGridCell > TextBox DataValidationErrors">
<Setter Property="Template" Value="{DynamicResource TooltipDataValidationContentTemplate}" />
<Setter Property="ErrorTemplate" Value="{DynamicResource TooltipDataValidationErrorTemplate}" />
</Style>
<Style Selector="DataGridColumnHeader">
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />

22
src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs

@ -1,22 +0,0 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace Avalonia.Diagnostics.Converters
{
internal class BoolToBrushConverter : IValueConverter
{
public IBrush Brush { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Brush : Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

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

@ -16,6 +16,8 @@ namespace Avalonia.Diagnostics.Models
public object Handler { get; }
public bool BeginsNewRoute { get; set; }
public string HandlerName
{
get

10
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs

@ -2,25 +2,17 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventOwnerTreeNode : EventTreeNodeBase
{
private static readonly RoutedEvent[] s_defaultEvents =
{
Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent,
InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent
};
public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsPageViewModel vm)
: base(null, type.Name)
{
Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name)
.Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) }));
.Select(e => new EventTreeNode(this, e, vm)));
IsExpanded = true;
}

9
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs

@ -9,7 +9,6 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class EventTreeNode : EventTreeNodeBase
{
private readonly RoutedEvent _event;
private readonly EventsPageViewModel _parentViewModel;
private bool _isRegistered;
private FiredEvent _currentEvent;
@ -20,10 +19,12 @@ namespace Avalonia.Diagnostics.ViewModels
Contract.Requires<ArgumentNullException>(@event != null);
Contract.Requires<ArgumentNullException>(vm != null);
_event = @event;
Event = @event;
_parentViewModel = vm;
}
public RoutedEvent Event { get; }
public override bool? IsEnabled
{
get => base.IsEnabled;
@ -53,8 +54,10 @@ namespace Avalonia.Diagnostics.ViewModels
{
if (IsEnabled.GetValueOrDefault() && !_isRegistered)
{
var allRoutes = RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble;
// FIXME: This leaks event handlers.
_event.AddClassHandler(typeof(object), HandleEvent, (RoutingStrategies)7, handledEventsToo: true);
Event.AddClassHandler(typeof(object), HandleEvent, allRoutes, handledEventsToo: true);
_isRegistered = true;
}
}

8
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs

@ -8,11 +8,13 @@ namespace Avalonia.Diagnostics.ViewModels
internal bool _updateParent = true;
private bool _isExpanded;
private bool? _isEnabled = false;
private bool _isVisible;
protected EventTreeNodeBase(EventTreeNodeBase parent, string text)
{
Parent = parent;
Text = text;
IsVisible = true;
}
public IAvaloniaReadOnlyList<EventTreeNodeBase> Children
@ -33,6 +35,12 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _isEnabled, value);
}
public bool IsVisible
{
get => _isVisible;
set => RaiseAndSetIfChanged(ref _isVisible, value);
}
public EventTreeNodeBase Parent
{
get;

160
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs

@ -1,28 +1,43 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventsPageViewModel : ViewModelBase
{
private readonly IControl _root;
private static readonly HashSet<RoutedEvent> s_defaultEvents = new HashSet<RoutedEvent>()
{
Button.ClickEvent,
InputElement.KeyDownEvent,
InputElement.KeyUpEvent,
InputElement.TextInputEvent,
InputElement.PointerReleasedEvent,
InputElement.PointerPressedEvent
};
private readonly MainViewModel _mainViewModel;
private string _eventTypeFilter;
private FiredEvent _selectedEvent;
private EventTreeNodeBase _selectedNode;
public EventsPageViewModel(IControl root)
public EventsPageViewModel(MainViewModel mainViewModel)
{
_root = root;
_mainViewModel = mainViewModel;
Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
.GroupBy(e => e.OwnerType)
.OrderBy(e => e.Key.Name)
.Select(g => new EventOwnerTreeNode(g.Key, g, this))
.ToArray();
EnableDefault();
}
public string Name => "Events";
@ -37,9 +52,140 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _selectedEvent, value);
}
private void Clear()
public EventTreeNodeBase SelectedNode
{
get => _selectedNode;
set => RaiseAndSetIfChanged(ref _selectedNode, value);
}
public string EventTypeFilter
{
get => _eventTypeFilter;
set => RaiseAndSetIfChanged(ref _eventTypeFilter, value);
}
public void Clear()
{
RecordedEvents.Clear();
}
public void DisableAll()
{
EvaluateNodeEnabled(_ => false);
}
public void EnableDefault()
{
EvaluateNodeEnabled(node => s_defaultEvents.Contains(node.Event));
}
public void RequestTreeNavigateTo(EventChainLink navTarget)
{
if (navTarget.Handler is IControl control)
{
_mainViewModel.RequestTreeNavigateTo(control, true);
}
}
public void SelectEventByType(RoutedEvent evt)
{
foreach (var node in Nodes)
{
var result = FindNode(node, evt);
if (result != null && result.IsVisible)
{
SelectedNode = result;
break;
}
}
static EventTreeNodeBase FindNode(EventTreeNodeBase node, RoutedEvent eventType)
{
if (node is EventTreeNode eventNode && eventNode.Event == eventType)
{
return node;
}
if (node.Children != null)
{
foreach (var child in node.Children)
{
var result = FindNode(child, eventType);
if (result != null)
{
return result;
}
}
}
return null;
}
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.PropertyName == nameof(EventTypeFilter))
{
UpdateEventFilters();
}
}
private void EvaluateNodeEnabled(Func<EventTreeNode, bool> eval)
{
void ProcessNode(EventTreeNodeBase node)
{
if (node is EventTreeNode eventNode)
{
node.IsEnabled = eval(eventNode);
}
if (node.Children != null)
{
foreach (var childNode in node.Children)
{
ProcessNode(childNode);
}
}
}
foreach (var node in Nodes)
{
ProcessNode(node);
}
}
private void UpdateEventFilters()
{
var filter = EventTypeFilter;
bool hasFilter = !string.IsNullOrEmpty(filter);
foreach (var node in Nodes)
{
FilterNode(node, false);
}
bool FilterNode(EventTreeNodeBase node, bool isParentVisible)
{
bool matchesFilter = !hasFilter || node.Text.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
bool hasVisibleChild = false;
if (node.Children != null)
{
foreach (var childNode in node.Children)
{
hasVisibleChild |= FilterNode(childNode, matchesFilter);
}
}
node.IsVisible = hasVisibleChild || matchesFilter || isParentVisible;
return node.IsVisible;
}
}
}
}

15
src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs

@ -62,13 +62,18 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public void AddToChain(object handler, bool handled, RoutingStrategies route)
{
AddToChain(new EventChainLink(handler, handled, route));
}
public void AddToChain(EventChainLink link)
{
if (EventChain.Count > 0)
{
var prevLink = EventChain[EventChain.Count-1];
if (prevLink.Route != link.Route)
{
link.BeginsNewRoute = true;
}
}
EventChain.Add(link);
if (HandledBy == null && link.Handled)
HandledBy = link;

17
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -4,6 +4,7 @@ using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
@ -27,7 +28,7 @@ namespace Avalonia.Diagnostics.ViewModels
_root = root;
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root));
_events = new EventsPageViewModel(root);
_events = new EventsPageViewModel(this);
UpdateFocusedControl();
KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged;
@ -193,5 +194,19 @@ namespace Avalonia.Diagnostics.ViewModels
UpdateFocusedControl();
}
}
public void RequestTreeNavigateTo(IControl control, bool isVisualTree)
{
var tree = isVisualTree ? _visualTree : _logicalTree;
var node = tree.FindNode(control);
if (node != null)
{
SelectedTab = isVisualTree ? 1 : 0;
tree.SelectControl(control);
}
}
}
}

135
src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml

@ -2,58 +2,129 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
x:Class="Avalonia.Diagnostics.Views.EventsPageView">
<UserControl.Resources>
<conv:BoolToBrushConverter x:Key="boolToBrush" Brush="#d9ffdc"/>
</UserControl.Resources>
<Grid ColumnDefinitions="*,4,3*">
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}"
Grid.RowSpan="2">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:EventTreeNodeBase"
ItemsSource="{Binding Children}">
<CheckBox Content="{Binding Text}" IsChecked="{Binding IsEnabled, Mode=TwoWay}" />
</TreeDataTemplate>
</TreeView.DataTemplates>
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.Styles>
</TreeView>
x:Class="Avalonia.Diagnostics.Views.EventsPageView"
Margin="2">
<UserControl.Styles>
<Style Selector="TextBlock.nav" >
<Setter Property="TextDecorations">
<TextDecorationCollection>
<TextDecoration Location="Underline" Stroke="Black" StrokeThickness="1" StrokeDashArray="2,2"/>
</TextDecorationCollection>
</Setter>
</Style>
<Style Selector="TextBlock.nav:pointerover" >
<Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}" />
<Setter Property="Cursor" Value="Help" />
</Style>
<Style Selector="ListBoxItem" >
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="ListBoxItem:selected /template/ ContentPresenter" >
<Setter Property="BorderBrush" Value="Black" />
</Style>
<Style Selector="ListBoxItem.handled" >
<Setter Property="Background" Value="#d9ffdc" />
</Style>
</UserControl.Styles>
<Grid ColumnDefinitions="1.1*,4,3*">
<Grid Grid.Column="0" RowDefinitions="Auto,*,Auto">
<TextBox Classes="clearButton" Grid.Row="0" Margin="0,0,0,2" Text="{Binding EventTypeFilter}" Watermark="Search event types" />
<TreeView Grid.Row="1" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" >
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:EventTreeNodeBase"
ItemsSource="{Binding Children}">
<CheckBox Content="{Binding Text}" IsChecked="{Binding IsEnabled, Mode=TwoWay}" />
</TreeDataTemplate>
</TreeView.DataTemplates>
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsVisible" Value="{Binding IsVisible}" />
</Style>
</TreeView.Styles>
</TreeView>
<StackPanel Grid.Row="2" Margin="0,2" Orientation="Horizontal" Spacing="2">
<Button Content="Disable all" Command="{Binding DisableAll}" />
<Button Content="Enable default" Command="{Binding EnableDefault}" />
</StackPanel>
</Grid>
<GridSplitter Width="4" Grid.Column="1" />
<Grid RowDefinitions="*,4,2*,Auto" Grid.Column="2">
<ListBox Name="eventsList" Items="{Binding RecordedEvents}"
<ListBox Name="EventsList" Items="{Binding RecordedEvents}"
SelectedItem="{Binding SelectedEvent, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Background="{Binding IsHandled, Converter={StaticResource boolToBrush}}"
Text="{Binding DisplayText}" />
<ListBoxItem Classes.handled="{Binding IsHandled}">
<Grid ColumnDefinitions="Auto,Auto,*,Auto">
<StackPanel Grid.Column="0" Spacing="2" Orientation="Horizontal" >
<TextBlock Tag="{Binding Event}" DoubleTapped="NavigateTo" Text="{Binding Event.Name}" FontWeight="Bold" Classes="nav" />
<TextBlock Text="on" />
<TextBlock Tag="{Binding Originator}" DoubleTapped="NavigateTo" Text="{Binding Originator.HandlerName}" Classes="nav" />
</StackPanel>
<StackPanel Margin="2,0,0,0" Grid.Column="1" Spacing="2" Orientation="Horizontal" IsVisible="{Binding IsHandled}" >
<TextBlock Text="::" />
<TextBlock Text="Handled by" />
<TextBlock Tag="{Binding HandledBy}" DoubleTapped="NavigateTo" Text="{Binding HandledBy.HandlerName}" Classes="nav" />
</StackPanel>
<StackPanel Grid.Column="3" Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock Text="Routing (" />
<TextBlock Text="{Binding Event.RoutingStrategies}"/>
<TextBlock Text=")"/>
</StackPanel>
</Grid>
</ListBoxItem>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Height="4" Grid.Row="1" />
<DockPanel Grid.Row="2" LastChildFill="True">
<TextBlock DockPanel.Dock="Top" FontSize="16" Text="Event chain:" />
<ListBox Items="{Binding SelectedEvent.EventChain}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal"
Background="{Binding Handled, Converter={StaticResource boolToBrush}}">
<TextBlock Text="{Binding Route}" />
<TextBlock Text=": " />
<TextBlock Text="{Binding HandlerName}" />
<TextBlock Text=" handled: " />
<TextBlock Text="{Binding Handled}" />
</StackPanel>
<ListBoxItem Classes.handled="{Binding Handled}">
<StackPanel Orientation="Vertical">
<Rectangle IsVisible="{Binding BeginsNewRoute}" StrokeDashArray="2,2" StrokeThickness="1" Stroke="Gray" />
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="{Binding Route}" FontWeight="Bold" />
<TextBlock Tag="{Binding}" DoubleTapped="NavigateTo" Text="{Binding HandlerName}" Classes="nav" />
</StackPanel>
</StackPanel>
</ListBoxItem>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<StackPanel Orientation="Horizontal" Grid.Row="3">
<Button Content="Clear" Margin="3" Command="{Binding Clear}" />
<StackPanel Orientation="Horizontal" Grid.Row="3" Spacing="2" Margin="0,2">
<Button Content="Clear" Command="{Binding Clear}" />
</StackPanel>
</Grid>
</Grid>
</UserControl>

57
src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs

@ -1,7 +1,14 @@
using System.Linq;
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
namespace Avalonia.Diagnostics.Views
{
@ -12,13 +19,53 @@ namespace Avalonia.Diagnostics.Views
public EventsPageView()
{
InitializeComponent();
_events = this.FindControl<ListBox>("events");
_events = this.FindControl<ListBox>("EventsList");
}
private void RecordedEvents_CollectionChanged(object sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
public void NavigateTo(object sender, TappedEventArgs e)
{
_events.ScrollIntoView(_events.Items.OfType<FiredEvent>().LastOrDefault());
if (DataContext is EventsPageViewModel vm && sender is Control control)
{
switch (control.Tag)
{
case EventChainLink chainLink:
{
vm.RequestTreeNavigateTo(chainLink);
break;
}
case RoutedEvent evt:
{
vm.SelectEventByType(evt);
break;
}
}
}
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is EventsPageViewModel vm)
{
vm.RecordedEvents.CollectionChanged += OnRecordedEventsChanged;
}
}
private void OnRecordedEventsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (sender is ObservableCollection<FiredEvent> events)
{
var evt = events.LastOrDefault();
if (evt is null)
{
return;
}
Dispatcher.UIThread.Post(() => _events.ScrollIntoView(evt));
}
}
private void InitializeComponent()

140
src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml

@ -1,44 +1,102 @@
<Style xmlns="https://github.com/avaloniaui"
Selector="DataValidationErrors"
xmlns:sys="clr-namespace:System;assembly=netstandard">
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="using:System">
<Design.PreviewWith>
<Border Padding="20">
<TextBox Text="Sample">
<DataValidationErrors.Error>
<sys:Exception/>
</DataValidationErrors.Error>
</TextBox>
<Border Padding="20">
<StackPanel Spacing="20">
<TextBox Text="Sample">
<DataValidationErrors.Error>
<sys:Exception />
</DataValidationErrors.Error>
</TextBox>
<TextBox Text="Sample">
<TextBox.Styles>
<Style Selector="DataValidationErrors">
<Setter Property="Template" Value="{DynamicResource TooltipDataValidationContentTemplate}" />
<Setter Property="ErrorTemplate" Value="{DynamicResource TooltipDataValidationErrorTemplate}" />
</Style>
</TextBox.Styles>
<DataValidationErrors.Error>
<sys:Exception />
</DataValidationErrors.Error>
</TextBox>
</StackPanel>
</Border>
</Design.PreviewWith>
<Setter Property="Template">
<ControlTemplate>
<DockPanel LastChildFill="True">
<ContentControl Margin="0 4 0 0"
DockPanel.Dock="Bottom"
ContentTemplate="{TemplateBinding ErrorTemplate}"
DataContext="{TemplateBinding Owner}"
Content="{Binding (DataValidationErrors.Errors)}"
IsVisible="{Binding (DataValidationErrors.HasErrors)}" />
<ContentPresenter
Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}" />
</DockPanel>
</ControlTemplate>
</Setter>
<Setter Property="ErrorTemplate">
<DataTemplate>
<ItemsControl Items="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding }" Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}" TextWrapping="Wrap" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</Setter>
</Style>
<Style Selector="DataValidationErrors">
<Style.Resources>
<DataTemplate x:Key="InlineDataValidationErrorTemplate">
<ItemsControl Items="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}"
Text="{Binding}"
TextWrapping="Wrap" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
<ControlTemplate x:Key="InlineDataValidationContentTemplate" TargetType="DataValidationErrors">
<DockPanel LastChildFill="True">
<ContentControl x:Name="InlineDataValidationContentControl"
Margin="0,4,0,0"
Content="{Binding (DataValidationErrors.Errors)}"
ContentTemplate="{TemplateBinding ErrorTemplate}"
DataContext="{TemplateBinding Owner}"
DockPanel.Dock="Bottom"
IsVisible="{Binding (DataValidationErrors.HasErrors)}" />
<ContentPresenter Name="PART_ContentPresenter"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</DockPanel>
</ControlTemplate>
<DataTemplate x:Key="TooltipDataValidationErrorTemplate">
<Panel Name="PART_InlineErrorTemplatePanel" Background="Transparent">
<Panel.Styles>
<Style Selector="Panel#PART_InlineErrorTemplatePanel">
<Setter Property="Margin" Value="8,0" />
</Style>
<Style Selector="Panel#PART_InlineErrorTemplatePanel ToolTip">
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlErrorTextForegroundBrush}" />
</Style>
<Style Selector="Panel#PART_InlineErrorTemplatePanel ToolTip TextBlock">
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
</Panel.Styles>
<ToolTip.Tip>
<ItemsControl Items="{Binding}" />
</ToolTip.Tip>
<Path Width="14"
Height="14"
Data="M14,7 A7,7 0 0,0 0,7 M0,7 A7,7 0 1,0 14,7 M7,3l0,5 M7,9l0,2"
Stroke="{DynamicResource SystemControlErrorTextForegroundBrush}"
StrokeThickness="2" />
</Panel>
</DataTemplate>
<ControlTemplate x:Key="TooltipDataValidationContentTemplate" TargetType="DataValidationErrors">
<DockPanel LastChildFill="True">
<ContentControl Content="{Binding (DataValidationErrors.Errors)}"
ContentTemplate="{TemplateBinding ErrorTemplate}"
DataContext="{TemplateBinding Owner}"
DockPanel.Dock="Right"
IsVisible="{Binding (DataValidationErrors.HasErrors)}" />
<ContentPresenter Name="PART_ContentPresenter"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</DockPanel>
</ControlTemplate>
</Style.Resources>
<Setter Property="Template" Value="{StaticResource InlineDataValidationContentTemplate}" />
<Setter Property="ErrorTemplate" Value="{StaticResource InlineDataValidationErrorTemplate}" />
</Style>
</Styles>

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

@ -422,7 +422,7 @@ namespace Avalonia.Media.TextFormatting
}
else
{
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
currentPosition = currentLength + measuredLength;
}
breakFound = true;

2
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -87,7 +87,7 @@ namespace Avalonia.Skia
{
var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _);
if (nextCodepoint == '\r' && codepoint == '\n' || nextCodepoint == '\n' && codepoint == '\r')
if (nextCodepoint == '\n' && codepoint == '\r')
{
count++;

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

@ -401,6 +401,24 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(expectedOffset, textLine.Start);
}
}
[Fact]
public void Should_FormatLine_With_Emergency_Breaks()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
var textSource = new SingleBufferTextSource("0123456789_0123456789_0123456789_0123456789", defaultProperties);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, 33, paragraphProperties);
Assert.NotNull(textLine.TextLineBreak?.RemainingCharacters);
}
}
public static IDisposable Start()
{

43
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs

@ -0,0 +1,43 @@
using System;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
public class TextShaperTests
{
[Fact]
public void Should_Form_Clusters_For_BreakPairs()
{
using (Start())
{
var text = "\n\r\n".AsMemory();
var glyphRun = TextShaper.Current.ShapeText(
text,
Typeface.Default,
12,
CultureInfo.CurrentCulture);
Assert.Equal(glyphRun.Characters.Length, text.Length);
Assert.Equal(glyphRun.GlyphClusters.Length, text.Length);
Assert.Equal(0, glyphRun.GlyphClusters[0]);
Assert.Equal(1, glyphRun.GlyphClusters[1]);
Assert.Equal(1, glyphRun.GlyphClusters[2]);
}
}
private static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(null),
textShaperImpl: new TextShaperImpl(),
fontManagerImpl: new CustomFontManagerImpl()));
return disposable;
}
}
}
Loading…
Cancel
Save