Browse Source

Merge branch 'master' into master

Vadim Melnikov 5 years ago
parent
commit
5a39838af7
100 changed files with 4330 additions and 996 deletions
  1. 0 1
      build/CoreLibraries.props
  2. 5 2
      native/Avalonia.Native/src/OSX/platformthreading.mm
  3. 1 0
      samples/BindingDemo/BindingDemo.csproj
  4. 1 0
      samples/ControlCatalog/ControlCatalog.csproj
  5. 1 0
      samples/ControlCatalog/MainView.xaml
  6. 134 0
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  7. 18 0
      samples/ControlCatalog/Pages/TextBlockPage.xaml.cs
  8. 1 0
      samples/RenderDemo/RenderDemo.csproj
  9. 1 0
      samples/VirtualizationDemo/VirtualizationDemo.csproj
  10. 1 1
      src/Avalonia.Base/ValueStore.cs
  11. 1 1
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  12. 46 42
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  13. 35 2
      src/Avalonia.Controls/Application.cs
  14. 1 2
      src/Avalonia.Controls/ContextMenu.cs
  15. 206 10
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  16. 79 3
      src/Avalonia.Controls/Primitives/AccessText.cs
  17. 77 51
      src/Avalonia.Controls/TextBlock.cs
  18. BIN
      src/Avalonia.Diagnostics/Assets/Fonts/SourceSansPro-Regular.ttf
  19. 11 1
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  20. 0 24
      src/Avalonia.Diagnostics/DevTools.xaml
  21. 0 159
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  22. 31 0
      src/Avalonia.Diagnostics/DevToolsExtensions.cs
  23. 22 0
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs
  24. 61 0
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  25. 36 0
      src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
  26. 19 0
      src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleHistoryItem.cs
  27. 0 0
      src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
  28. 3 5
      src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
  29. 95 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  30. 57 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  31. 112 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
  32. 179 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  33. 1 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
  34. 19 3
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
  35. 0 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
  36. 2 15
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
  37. 0 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
  38. 2 4
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
  39. 126 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  40. 60 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  41. 8 7
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  42. 31 16
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  43. 17 9
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
  44. 3 2
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  45. 56 0
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml
  46. 64 0
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs
  47. 25 0
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  48. 18 0
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
  49. 3 2
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
  50. 2 2
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
  51. 55 0
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  52. 66 0
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
  53. 18 0
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
  54. 74 0
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  55. 11 8
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
  56. 3 6
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  57. 0 0
      src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs
  58. 0 25
      src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs
  59. 0 76
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  60. 0 16
      src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs
  61. 0 58
      src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs
  62. 0 75
      src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
  63. 0 51
      src/Avalonia.Diagnostics/Views/GridRepeater.cs
  64. 0 33
      src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs
  65. 0 146
      src/Avalonia.Diagnostics/Views/SimpleGrid.cs
  66. 1 1
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
  67. 0 1
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  68. 6 3
      src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs
  69. 1 1
      src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs
  70. 3 2
      src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
  71. 2 2
      src/Avalonia.Input/NavigationDirection.cs
  72. 37 25
      src/Avalonia.Styling/StyledElement.cs
  73. 27 19
      src/Avalonia.Styling/Styling/Styles.cs
  74. 0 1
      src/Avalonia.Themes.Default/ComboBox.xaml
  75. 2 4
      src/Avalonia.Themes.Default/MenuItem.xaml
  76. BIN
      src/Avalonia.Visuals/Assets/GraphemeBreak.trie
  77. BIN
      src/Avalonia.Visuals/Assets/UnicodeData.trie
  78. 4 0
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  79. 4 8
      src/Avalonia.Visuals/Media/FontManager.cs
  80. 162 65
      src/Avalonia.Visuals/Media/GlyphRun.cs
  81. 19 5
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  82. 56 0
      src/Avalonia.Visuals/Media/Immutable/ImmutableTextDecoration.cs
  83. 106 0
      src/Avalonia.Visuals/Media/TextDecoration.cs
  84. 82 0
      src/Avalonia.Visuals/Media/TextDecorationCollection.cs
  85. 31 0
      src/Avalonia.Visuals/Media/TextDecorationLocation.cs
  86. 29 0
      src/Avalonia.Visuals/Media/TextDecorationUnit.cs
  87. 66 0
      src/Avalonia.Visuals/Media/TextDecorations.cs
  88. 22 0
      src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
  89. 74 0
      src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs
  90. 15 0
      src/Avalonia.Visuals/Media/TextFormatting/ITextSource.cs
  91. 212 0
      src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs
  92. 417 0
      src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs
  93. 259 0
      src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs
  94. 21 0
      src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
  95. 9 0
      src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs
  96. 9 0
      src/Avalonia.Visuals/Media/TextFormatting/TextEndOfParagraph.cs
  97. 74 0
      src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs
  98. 186 0
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs
  99. 387 0
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  100. 109 0
      src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs

+ 0 - 1
build/CoreLibraries.props

@@ -4,7 +4,6 @@
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Animation/Avalonia.Animation.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Controls/Avalonia.Controls.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj" />
-      <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Input/Avalonia.Input.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Interactivity/Avalonia.Interactivity.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Layout/Avalonia.Layout.csproj" />

+ 5 - 2
native/Avalonia.Native/src/OSX/platformthreading.mm

@@ -157,11 +157,14 @@ NSArray<NSString*>* _modes;
 
 -(void) perform
 {
+    ComPtr<IAvnSignaledCallback> cb;
     @synchronized (self) {
         _signaled  = false;
-        if(_parent != NULL && _parent->SignaledCallback != NULL)
-            _parent->SignaledCallback->Signaled(0, false);
+        if(_parent != NULL)
+            cb = _parent->SignaledCallback;
     }
+    if(cb != nullptr)
+        cb->Signaled(0, false);
 }
 
 -(void) setParent:(PlatformThreadingInterface *)parent

+ 1 - 0
samples/BindingDemo/BindingDemo.csproj

@@ -4,6 +4,7 @@
     <TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
   </PropertyGroup>
   <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
   </ItemGroup>

+ 1 - 0
samples/ControlCatalog/ControlCatalog.csproj

@@ -21,6 +21,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
   </ItemGroup>

+ 1 - 0
samples/ControlCatalog/MainView.xaml

@@ -54,6 +54,7 @@
       <TabItem Header="TabControl"><pages:TabControlPage/></TabItem>
       <TabItem Header="TabStrip"><pages:TabStripPage/></TabItem>
       <TabItem Header="TextBox"><pages:TextBoxPage/></TabItem>
+      <TabItem Header="TextBlock"><pages:TextBlockPage/></TabItem>
       <TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
       <TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
       <TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>

+ 134 - 0
samples/ControlCatalog/Pages/TextBlockPage.xaml

@@ -0,0 +1,134 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Class="ControlCatalog.Pages.TextBlockPage">
+  <StackPanel>
+    <TextBlock Classes="h1">TextBlock</TextBlock>
+    <TextBlock Classes="h2">A control that can display text</TextBlock>
+    <StackPanel
+      Orientation="Horizontal"
+      Spacing="16"
+      HorizontalAlignment="Center"
+      Margin="0,16,0,0">
+      <StackPanel.Styles>
+        <Style Selector="Border">
+          <Setter Property="BorderThickness" Value="1"/>
+          <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
+          <Setter Property="Padding" Value="2"/>
+        </Style>
+      </StackPanel.Styles>
+      <Border>
+        <StackPanel Width="200" Spacing="8">
+          <TextBlock TextTrimming="CharacterEllipsis" Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/>
+          <TextBlock TextTrimming="WordEllipsis" Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/>
+          <TextBlock Text="Left aligned text" TextAlignment="Left" />
+          <TextBlock Text="Center aligned text" TextAlignment="Center" />
+          <TextBlock Text="Right aligned text" TextAlignment="Right" />
+        </StackPanel>
+      </Border>
+      <Border>
+        <StackPanel Width="200" Spacing="8">
+          <TextBlock
+            TextWrapping="Wrap"
+            Text="Multiline TextBlock with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
+        </StackPanel>
+      </Border>
+      <Border>
+        <StackPanel Width="200" Spacing="8">
+          <TextBlock Text="Custom font regular" FontWeight="Normal" FontStyle="Normal" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/>
+          <TextBlock Text="Custom font bold" FontWeight="Bold" FontStyle="Normal" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/>
+          <TextBlock Text="Custom font italic" FontWeight="Normal" FontStyle="Italic" FontFamily="/Assets/Fonts/SourceSansPro-Italic.ttf#Source Sans Pro"/>
+          <TextBlock Text="Custom font italic bold" FontWeight="Bold" FontStyle="Italic" FontFamily="/Assets/Fonts/SourceSansPro-*.ttf#Source Sans Pro"/>
+        </StackPanel>
+      </Border>
+    </StackPanel>
+    <StackPanel
+      Orientation="Horizontal"
+      Spacing="16"
+      HorizontalAlignment="Center"
+      Margin="0,16,0,0">
+      <StackPanel.Styles>
+        <Style Selector="Border">
+          <Setter Property="BorderThickness" Value="1"/>
+          <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
+          <Setter Property="Padding" Value="2"/>
+        </Style>
+      </StackPanel.Styles>
+      <Border>
+        <StackPanel Width="200" Spacing="8">
+          <TextBlock TextDecorations="Underline" Text="Underline"/>
+          <TextBlock TextDecorations="Strikethrough" Text="Strikethrough"/>
+          <TextBlock TextDecorations="Overline" Text="Overline" />
+          <TextBlock TextDecorations="Baseline" Text="Baseline"/>
+          <TextBlock Text="Custom TextDecorations">
+            <TextBlock.TextDecorations>
+              <TextDecorationCollection>
+                <TextDecoration
+                  Location="Overline"
+                  PenThicknessUnit="Pixel">
+                  <TextDecoration.Pen>
+                    <Pen Thickness="2">
+                      <Pen.Brush>
+                        <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
+                          <LinearGradientBrush.GradientStops>
+                            <GradientStop Offset="0" Color="Red"/>
+                            <GradientStop Offset="1" Color="Green"/>
+                          </LinearGradientBrush.GradientStops>
+                        </LinearGradientBrush>
+                      </Pen.Brush>
+                    </Pen>
+                  </TextDecoration.Pen>
+                </TextDecoration>
+                <TextDecoration
+                  Location="Strikethrough"
+                  PenThicknessUnit="Pixel">
+                  <TextDecoration.Pen>
+                    <Pen Thickness="1">
+                      <Pen.Brush>
+                        <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
+                          <LinearGradientBrush.GradientStops>
+                            <GradientStop Offset="0" Color="Green"/>
+                            <GradientStop Offset="1" Color="Blue"/>
+                          </LinearGradientBrush.GradientStops>
+                        </LinearGradientBrush>
+                      </Pen.Brush>
+                    </Pen>
+                  </TextDecoration.Pen>
+                </TextDecoration>
+                <TextDecoration
+                  Location="Underline"
+                  PenThicknessUnit="Pixel">
+                  <TextDecoration.Pen>
+                    <Pen Thickness="2">
+                      <Pen.Brush>
+                        <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
+                          <LinearGradientBrush.GradientStops>
+                            <GradientStop Offset="0" Color="Blue"/>
+                            <GradientStop Offset="1" Color="Red"/>
+                          </LinearGradientBrush.GradientStops>
+                        </LinearGradientBrush>
+                      </Pen.Brush>
+                    </Pen>
+                  </TextDecoration.Pen>
+                </TextDecoration>
+              </TextDecorationCollection>
+            </TextBlock.TextDecorations>
+          </TextBlock>
+        </StackPanel>
+      </Border>
+      <Border>
+        <StackPanel Width="200" Spacing="8">
+          <TextBlock Text="🏻 👌🏻"/>
+          <TextBlock Text="🏼 👌🏼" />
+          <TextBlock Text="🏽 👌🏽"/>
+          <TextBlock Text="🏾 👌🏾"/>
+          <TextBlock Text="🏿 👌🏿"/>
+        </StackPanel>
+      </Border>
+      <Border>
+        <StackPanel Width="200" Spacing="8">
+          <TextBlock Text="👪 👨‍👩‍👧 👨‍👩‍👧‍👦"/>
+        </StackPanel>
+      </Border>
+    </StackPanel>
+  </StackPanel>
+</UserControl>

+ 18 - 0
samples/ControlCatalog/Pages/TextBlockPage.xaml.cs

@@ -0,0 +1,18 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+    public class TextBlockPage : UserControl
+    {
+        public TextBlockPage()
+        {
+            this.InitializeComponent();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 1 - 0
samples/RenderDemo/RenderDemo.csproj

@@ -4,6 +4,7 @@
     <TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
   </PropertyGroup>
   <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
   </ItemGroup>

+ 1 - 0
samples/VirtualizationDemo/VirtualizationDemo.csproj

@@ -4,6 +4,7 @@
     <TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
   </PropertyGroup>
   <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
   </ItemGroup>

+ 1 - 1
src/Avalonia.Base/ValueStore.cs

@@ -173,7 +173,7 @@ namespace Avalonia
             {
                 return new Diagnostics.AvaloniaPropertyValue(
                     property,
-                    slot.Value.HasValue ? (object)slot.Value : AvaloniaProperty.UnsetValue,
+                    slot.Value.HasValue ? slot.Value.Value : AvaloniaProperty.UnsetValue,
                     slot.ValuePriority,
                     null);
             }

+ 1 - 1
src/Avalonia.Controls.DataGrid/DataGrid.cs

@@ -65,7 +65,7 @@ namespace Avalonia.Controls
         private const double DATAGRID_minimumColumnHeaderHeight = 4;
         internal const double DATAGRID_maximumStarColumnWidth = 10000;
         internal const double DATAGRID_minimumStarColumnWidth = 0.001;
-        private const double DATAGRID_mouseWheelDelta = 48.0;
+        private const double DATAGRID_mouseWheelDelta = 72.0;
         private const double DATAGRID_maxHeadersThickness = 32768;
 
         private const double DATAGRID_defaultRowHeight = 22;

+ 46 - 42
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@@ -67,7 +67,7 @@ namespace Avalonia.Controls
 
         static DataGridColumnHeader()
         {
-            AreSeparatorsVisibleProperty.Changed.AddClassHandler<DataGridColumnHeader>((x,e) => x.OnAreSeparatorsVisibleChanged(e));
+            AreSeparatorsVisibleProperty.Changed.AddClassHandler<DataGridColumnHeader>((x, e) => x.OnAreSeparatorsVisibleChanged(e));
         }
 
         /// <summary>
@@ -103,7 +103,7 @@ namespace Avalonia.Controls
         {
             get;
             set;
-        } 
+        }
         internal DataGrid OwningGrid => OwningColumn?.OwningGrid;
 
         internal int ColumnIndex
@@ -116,19 +116,19 @@ namespace Avalonia.Controls
                 }
                 return OwningColumn.Index;
             }
-        } 
+        }
 
         internal ListSortDirection? CurrentSortingState
         {
             get;
             private set;
-        } 
+        }
 
         private bool IsMouseOver
         {
             get;
             set;
-        } 
+        }
 
         private bool IsPressed
         {
@@ -158,14 +158,14 @@ namespace Avalonia.Controls
                 && OwningGrid.DataConnection.AllowSort)
             {
                 var sort = OwningColumn.GetSortDescription();
-                if(sort != null)
+                if (sort != null)
                 {
                     CurrentSortingState = sort.Descending ? ListSortDirection.Descending : ListSortDirection.Ascending;
                 }
             }
-            PseudoClasses.Set(":sortascending", 
+            PseudoClasses.Set(":sortascending",
                 CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Ascending);
-            PseudoClasses.Set(":sortdescending", 
+            PseudoClasses.Set(":sortdescending",
                 CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Descending);
         }
 
@@ -195,7 +195,7 @@ namespace Avalonia.Controls
             // completed a click without dragging, so we're sorting
             InvokeProcessSort(keyModifiers);
             handled = true;
-        } 
+        }
 
         internal void InvokeProcessSort(KeyModifiers keyModifiers)
         {
@@ -208,7 +208,7 @@ namespace Avalonia.Controls
             {
                 Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers));
             }
-        } 
+        }
 
         //TODO GroupSorting
         internal void ProcessSort(KeyModifiers keyModifiers)
@@ -246,45 +246,49 @@ namespace Avalonia.Controls
                         owningGrid.DataConnection.SortDescriptions.Clear();
                     }
 
-                    if (sort != null)
+                    // if ctrl is held down, we only clear the sort directions
+                    if (!ctrl)
                     {
-                        newSort = sort.SwitchSortDirection();
-
-                        // changing direction should not affect sort order, so we replace this column's
-                        // sort description instead of just adding it to the end of the collection
-                        int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort);
-                        if (oldIndex >= 0)
+                        if (sort != null)
                         {
-                            owningGrid.DataConnection.SortDescriptions.Remove(sort);
-                            owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort);
+                            newSort = sort.SwitchSortDirection();
+
+                            // changing direction should not affect sort order, so we replace this column's
+                            // sort description instead of just adding it to the end of the collection
+                            int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort);
+                            if (oldIndex >= 0)
+                            {
+                                owningGrid.DataConnection.SortDescriptions.Remove(sort);
+                                owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort);
+                            }
+                            else
+                            {
+                                owningGrid.DataConnection.SortDescriptions.Add(newSort);
+                            }
                         }
                         else
                         {
+                            string propertyName = OwningColumn.GetSortPropertyName();
+                            // no-opt if we couldn't find a property to sort on
+                            if (string.IsNullOrEmpty(propertyName))
+                            {
+                                return;
+                            }
+
+                            newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
                             owningGrid.DataConnection.SortDescriptions.Add(newSort);
                         }
                     }
-                    else
-                    {
-                        string propertyName = OwningColumn.GetSortPropertyName();
-                        // no-opt if we couldn't find a property to sort on
-                        if (string.IsNullOrEmpty(propertyName))
-                        {
-                            return;
-                        }
-
-                        newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
-                        owningGrid.DataConnection.SortDescriptions.Add(newSort);
-                    }
                 }
             }
-        } 
+        }
 
         private bool CanReorderColumn(DataGridColumn column)
         {
-            return OwningGrid.CanUserReorderColumns 
+            return OwningGrid.CanUserReorderColumns
                 && !(column is DataGridFillerColumn)
                 && (column.CanUserReorderInternal.HasValue && column.CanUserReorderInternal.Value || !column.CanUserReorderInternal.HasValue);
-        } 
+        }
 
         /// <summary>
         /// Determines whether a column can be resized by dragging the border of its header.  If star sizing
@@ -302,7 +306,7 @@ namespace Avalonia.Controls
                 return false;
             }
             return column.ActualCanUserResize;
-        }  
+        }
 
         private static bool TrySetResizeColumn(DataGridColumn column)
         {
@@ -316,7 +320,7 @@ namespace Avalonia.Controls
                 return true;
             }
             return false;
-        } 
+        }
 
         //TODO DragDrop
 
@@ -371,7 +375,7 @@ namespace Avalonia.Controls
             {
                 if (_dragMode == DragMode.MouseDown)
                 {
-                   OnMouseLeftButtonUp_Click(args.KeyModifiers, ref handled);
+                    OnMouseLeftButtonUp_Click(args.KeyModifiers, ref handled);
                 }
                 else if (_dragMode == DragMode.Reorder)
                 {
@@ -449,7 +453,7 @@ namespace Avalonia.Controls
 
             OnMouseLeave();
             ApplyState();
-        } 
+        }
 
         private void DataGridColumnHeader_PointerPressed(object sender, PointerPressedEventArgs e)
         {
@@ -577,7 +581,7 @@ namespace Avalonia.Controls
             {
                 return OwningGrid.Columns.Count - 1;
             }
-        } 
+        }
 
         /// <summary>
         /// Returns true if the mouse is 
@@ -723,7 +727,7 @@ namespace Avalonia.Controls
                     Point targetPosition = new Point(0, 0);
                     if (targetColumn == null || targetColumn == OwningGrid.ColumnsInternal.FillerColumn || targetColumn.IsFrozen != OwningColumn.IsFrozen)
                     {
-                        targetColumn = 
+                        targetColumn =
                             OwningGrid.ColumnsInternal.GetLastColumn(
                                 isVisible: true,
                                 isFrozen: OwningColumn.IsFrozen,
@@ -741,7 +745,7 @@ namespace Avalonia.Controls
 
                 handled = true;
             }
-        } 
+        }
 
         private void OnMouseMove_Resize(ref bool handled, Point mousePositionHeaders)
         {
@@ -764,7 +768,7 @@ namespace Avalonia.Controls
 
                 handled = true;
             }
-        } 
+        }
 
         private void SetDragCursor(Point mousePosition)
         {

+ 35 - 2
src/Avalonia.Controls/Application.cs

@@ -44,6 +44,7 @@ namespace Avalonia
         private readonly Styler _styler = new Styler();
         private Styles _styles;
         private IResourceDictionary _resources;
+        private bool _notifyingResourcesChanged;
 
         /// <summary>
         /// Defines the <see cref="DataContext"/> property.
@@ -160,7 +161,19 @@ namespace Avalonia
         /// <remarks>
         /// Global styles apply to all windows in the application.
         /// </remarks>
-        public Styles Styles => _styles ?? (_styles = new Styles());
+        public Styles Styles
+        {
+            get
+            {
+                if (_styles == null)
+                {
+                    _styles = new Styles(this);
+                    _styles.ResourcesChanged += ThisResourcesChanged;
+                }
+
+                return _styles;
+            }
+        }
 
         /// <inheritdoc/>
         bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
@@ -233,9 +246,29 @@ namespace Avalonia
             
         }
 
+        private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
+        {
+            if (_notifyingResourcesChanged)
+            {
+                return;
+            }
+
+            try
+            {
+                _notifyingResourcesChanged = true;
+                (_resources as ISetResourceParent)?.ParentResourcesChanged(e);
+                (_styles as ISetResourceParent)?.ParentResourcesChanged(e);
+                ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
+            }
+            finally
+            {
+                _notifyingResourcesChanged = false;
+            }
+        }
+
         private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e)
         {
-            ResourcesChanged?.Invoke(this, e);
+            NotifyResourcesChanged(e);
         }
 
         private string _name;

+ 1 - 2
src/Avalonia.Controls/ContextMenu.cs

@@ -104,8 +104,7 @@ namespace Avalonia.Controls
                 {
                     PlacementMode = PlacementMode.Pointer,
                     PlacementTarget = control,
-                    StaysOpen = false,
-                    ObeyScreenEdges = true
+                    StaysOpen = false
                 };
 
                 _popup.Opened += PopupOpened;

+ 206 - 10
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -4,12 +4,13 @@
 using System;
 using System.Reactive.Linq;
 using Avalonia.Media;
+using Avalonia.Metadata;
 using Avalonia.Threading;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Controls.Presenters
 {
-    public class TextPresenter : TextBlock
+    public class TextPresenter : Control
     {
         public static readonly DirectProperty<TextPresenter, int> CaretIndexProperty =
             TextBox.CaretIndexProperty.AddOwner<TextPresenter>(
@@ -38,11 +39,41 @@ namespace Avalonia.Controls.Presenters
                 o => o.SelectionEnd,
                 (o, v) => o.SelectionEnd = v);
 
+        /// <summary>
+        /// Defines the <see cref="Text"/> property.
+        /// </summary>
+        public static readonly DirectProperty<TextPresenter, string> TextProperty =
+            AvaloniaProperty.RegisterDirect<TextPresenter, string>(
+                nameof(Text),
+                o => o.Text,
+                (o, v) => o.Text = v);
+
+        /// <summary>
+        /// Defines the <see cref="TextAlignment"/> property.
+        /// </summary>
+        public static readonly StyledProperty<TextAlignment> TextAlignmentProperty =
+            TextBlock.TextAlignmentProperty.AddOwner<TextPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="TextWrapping"/> property.
+        /// </summary>
+        public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
+            TextBlock.TextWrappingProperty.AddOwner<TextPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="Background"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IBrush> BackgroundProperty =
+            Border.BackgroundProperty.AddOwner<TextPresenter>();
+
         private readonly DispatcherTimer _caretTimer;
         private int _caretIndex;
         private int _selectionStart;
         private int _selectionEnd;
         private bool _caretBlink;
+        private string _text;
+        private FormattedText _formattedText;
+        private Size _constraint;
 
         static TextPresenter()
         {
@@ -61,11 +92,104 @@ namespace Avalonia.Controls.Presenters
 
         public TextPresenter()
         {
-            _caretTimer = new DispatcherTimer();
-            _caretTimer.Interval = TimeSpan.FromMilliseconds(500);
+            _text = string.Empty;
+            _caretTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
             _caretTimer.Tick += CaretTimerTick;
         }
 
+        /// <summary>
+        /// Gets or sets a brush used to paint the control's background.
+        /// </summary>
+        public IBrush Background
+        {
+            get => GetValue(BackgroundProperty);
+            set => SetValue(BackgroundProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the text.
+        /// </summary>
+        [Content]
+        public string Text
+        {
+            get => _text;
+            set => SetAndRaise(TextProperty, ref _text, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the font family.
+        /// </summary>
+        public FontFamily FontFamily
+        {
+            get => TextBlock.GetFontFamily(this);
+            set => TextBlock.SetFontFamily(this, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the font size.
+        /// </summary>
+        public double FontSize
+        {
+            get => TextBlock.GetFontSize(this);
+            set => TextBlock.SetFontSize(this, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the font style.
+        /// </summary>
+        public FontStyle FontStyle
+        {
+            get => TextBlock.GetFontStyle(this);
+            set => TextBlock.SetFontStyle(this, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the font weight.
+        /// </summary>
+        public FontWeight FontWeight
+        {
+            get => TextBlock.GetFontWeight(this);
+            set => TextBlock.SetFontWeight(this, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a brush used to paint the text.
+        /// </summary>
+        public IBrush Foreground
+        {
+            get => TextBlock.GetForeground(this);
+            set => TextBlock.SetForeground(this, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the control's text wrapping mode.
+        /// </summary>
+        public TextWrapping TextWrapping
+        {
+            get => GetValue(TextWrappingProperty);
+            set => SetValue(TextWrappingProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the text alignment.
+        /// </summary>
+        public TextAlignment TextAlignment
+        {
+            get => GetValue(TextAlignmentProperty);
+            set => SetValue(TextAlignmentProperty, value);
+        }
+
+        /// <summary>
+        /// Gets the <see cref="FormattedText"/> used to render the text.
+        /// </summary>
+        public FormattedText FormattedText
+        {
+            get
+            {
+                return _formattedText ?? (_formattedText = CreateFormattedText(Bounds.Size, Text));
+            }
+        }
+
         public int CaretIndex
         {
             get
@@ -138,6 +262,54 @@ namespace Avalonia.Controls.Presenters
             return hit.TextPosition + (hit.IsTrailing ? 1 : 0);
         }
 
+        /// <summary>
+        /// Creates the <see cref="FormattedText"/> used to render the text.
+        /// </summary>
+        /// <param name="constraint">The constraint of the text.</param>
+        /// <param name="text">The text to format.</param>
+        /// <returns>A <see cref="FormattedText"/> object.</returns>
+        private FormattedText CreateFormattedTextInternal(Size constraint, string text)
+        {
+            return new FormattedText
+            {
+                Constraint = constraint,
+                Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
+                FontSize = FontSize,
+                Text = text ?? string.Empty,
+                TextAlignment = TextAlignment,
+                TextWrapping = TextWrapping,
+            };
+        }
+
+        /// <summary>
+        /// Invalidates <see cref="FormattedText"/>.
+        /// </summary>
+        protected void InvalidateFormattedText()
+        {
+            if (_formattedText != null)
+            {
+                _constraint = _formattedText.Constraint;
+                _formattedText = null;
+            }
+        }
+
+        /// <summary>
+        /// Renders the <see cref="TextPresenter"/> to a drawing context.
+        /// </summary>
+        /// <param name="context">The drawing context.</param>
+        private void RenderInternal(DrawingContext context)
+        {
+            var background = Background;
+
+            if (background != null)
+            {
+                context.FillRectangle(background, new Rect(Bounds.Size));
+            }
+
+            FormattedText.Constraint = Bounds.Size;
+            context.DrawText(Foreground, new Point(), FormattedText);
+        }
+
         public override void Render(DrawingContext context)
         {
             var selectionStart = SelectionStart;
@@ -150,7 +322,7 @@ namespace Avalonia.Controls.Presenters
 
                 // issue #600: set constraint before any FormattedText manipulation
                 //             see base.Render(...) implementation
-                FormattedText.Constraint = Bounds.Size;
+                FormattedText.Constraint = _constraint;
 
                 var rects = FormattedText.HitTestTextRange(start, length);
 
@@ -160,7 +332,7 @@ namespace Avalonia.Controls.Presenters
                 }
             }
 
-            base.Render(context);
+            RenderInternal(context);
 
             if (selectionStart == selectionEnd)
             {
@@ -168,7 +340,7 @@ namespace Avalonia.Controls.Presenters
 
                 if (caretBrush is null)
                 {
-                    var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color;
+                    var backgroundColor = (Background as SolidColorBrush)?.Color;
                     if (backgroundColor.HasValue)
                     {
                         byte red = (byte)~(backgroundColor.Value.R);
@@ -255,17 +427,17 @@ namespace Avalonia.Controls.Presenters
         /// <param name="constraint">The constraint of the text.</param>
         /// <param name="text">The text to generated the <see cref="FormattedText"/> for.</param>
         /// <returns>A <see cref="FormattedText"/> object.</returns>
-        protected override FormattedText CreateFormattedText(Size constraint, string text)
+        protected virtual FormattedText CreateFormattedText(Size constraint, string text)
         {
             FormattedText result = null;
 
             if (PasswordChar != default(char))
             {
-                result = base.CreateFormattedText(constraint, new string(PasswordChar, text?.Length ?? 0));
+                result = CreateFormattedTextInternal(constraint, new string(PasswordChar, text?.Length ?? 0));
             }
             else
             {
-                result = base.CreateFormattedText(constraint, text);
+                result = CreateFormattedTextInternal(constraint, text);
             }
 
             var selectionStart = SelectionStart;
@@ -284,13 +456,37 @@ namespace Avalonia.Controls.Presenters
             return result;
         }
 
+        /// <summary>
+        /// Measures the control.
+        /// </summary>
+        /// <param name="availableSize">The available size for the control.</param>
+        /// <returns>The desired size.</returns>
+        private Size MeasureInternal(Size availableSize)
+        {
+            if (!string.IsNullOrEmpty(Text))
+            {
+                if (TextWrapping == TextWrapping.Wrap)
+                {
+                    FormattedText.Constraint = new Size(availableSize.Width, double.PositiveInfinity);
+                }
+                else
+                {
+                    FormattedText.Constraint = Size.Infinity;
+                }
+
+                return FormattedText.Bounds.Size;
+            }
+
+            return new Size();
+        }
+
         protected override Size MeasureOverride(Size availableSize)
         {
             var text = Text;
 
             if (!string.IsNullOrEmpty(text))
             {
-                return base.MeasureOverride(availableSize);
+                return MeasureInternal(availableSize);
             }
             else
             {

+ 79 - 3
src/Avalonia.Controls/Primitives/AccessText.cs

@@ -4,6 +4,7 @@
 using System;
 using Avalonia.Input;
 using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
 
 namespace Avalonia.Controls.Primitives
 {
@@ -69,7 +70,7 @@ namespace Avalonia.Controls.Primitives
 
             if (underscore != -1 && ShowAccessKey)
             {
-                var rect = FormattedText.HitTestTextPosition(underscore);
+                var rect = HitTestTextPosition(underscore);
                 var offset = new Vector(0, -0.5);
                 context.DrawLine(
                     new Pen(Foreground, 1),
@@ -78,10 +79,85 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
+        /// <summary>
+        /// Get the pixel location relative to the top-left of the layout box given the text position.
+        /// </summary>
+        /// <param name="textPosition">The text position.</param>
+        /// <returns></returns>
+        private Rect HitTestTextPosition(int textPosition)
+        {
+            if (TextLayout == null)
+            {
+                return new Rect();
+            }
+
+            if (TextLayout.TextLines.Count == 0)
+            {
+                return new Rect();
+            }
+
+            if (textPosition < 0 || textPosition >= Text.Length)
+            {
+                var lastLine = TextLayout.TextLines[TextLayout.TextLines.Count - 1];
+
+                var offsetX = lastLine.LineMetrics.BaselineOrigin.X;
+
+                var lineX = offsetX + lastLine.LineMetrics.Size.Width;
+
+                var lineY = Bounds.Height - lastLine.LineMetrics.Size.Height;
+
+                return new Rect(lineX, lineY, 0, lastLine.LineMetrics.Size.Height);
+            }
+
+            var currentY = 0.0;
+
+            foreach (var textLine in TextLayout.TextLines)
+            {
+                if (textLine.Text.End < textPosition)
+                {
+                    currentY += textLine.LineMetrics.Size.Height;
+
+                    continue;
+                }
+
+                var currentX = textLine.LineMetrics.BaselineOrigin.X;
+
+                foreach (var textRun in textLine.TextRuns)
+                {
+                    if (!(textRun is ShapedTextRun shapedRun))
+                    {
+                        continue;
+                    }
+
+                    if (shapedRun.GlyphRun.Characters.End < textPosition)
+                    {
+                        currentX += shapedRun.GlyphRun.Bounds.Width;
+
+                        continue;
+                    }
+
+                    var characterHit = shapedRun.GlyphRun.FindNearestCharacterHit(textPosition, out var width);
+
+                    var distance = shapedRun.GlyphRun.GetDistanceFromCharacterHit(characterHit);
+
+                    currentX += distance - width;
+
+                    if (characterHit.TrailingLength == 0)
+                    {
+                        width = 0.0;
+                    }
+
+                    return new Rect(currentX, currentY, width, shapedRun.GlyphRun.Bounds.Height);
+                }
+            }
+
+            return new Rect();
+        }
+
         /// <inheritdoc/>
-        protected override FormattedText CreateFormattedText(Size constraint, string text)
+        protected override TextLayout CreateTextLayout(Size constraint, string text)
         {
-            return base.CreateFormattedText(constraint, StripAccessKey(text));
+            return base.CreateTextLayout(constraint, StripAccessKey(text));
         }
 
         /// <summary>

+ 77 - 51
src/Avalonia.Controls/TextBlock.cs

@@ -4,6 +4,7 @@
 using System.Reactive.Linq;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
 using Avalonia.Metadata;
 
 namespace Avalonia.Controls
@@ -87,8 +88,20 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
             AvaloniaProperty.Register<TextBlock, TextWrapping>(nameof(TextWrapping));
 
+        /// <summary>
+        /// Defines the <see cref="TextTrimming"/> property.
+        /// </summary>
+        public static readonly StyledProperty<TextTrimming> TextTrimmingProperty =
+            AvaloniaProperty.Register<TextBlock, TextTrimming>(nameof(TextTrimming));
+
+        /// <summary>
+        /// Defines the <see cref="TextDecorations"/> property.
+        /// </summary>
+        public static readonly StyledProperty<TextDecorationCollection> TextDecorationsProperty =
+            AvaloniaProperty.Register<TextBlock, TextDecorationCollection>(nameof(TextDecorations));
+
         private string _text;
-        private FormattedText _formattedText;
+        private TextLayout _textLayout;
         private Size _constraint;
 
         /// <summary>
@@ -110,7 +123,7 @@ namespace Avalonia.Controls
                 FontSizeProperty.Changed,
                 FontStyleProperty.Changed,
                 FontWeightProperty.Changed
-            ).AddClassHandler<TextBlock>((x,_) => x.OnTextPropertiesChanged());
+            ).AddClassHandler<TextBlock>((x, _) => x.OnTextPropertiesChanged());
         }
 
         /// <summary>
@@ -121,6 +134,17 @@ namespace Avalonia.Controls
             _text = string.Empty;
         }
 
+        /// <summary>
+        /// Gets the <see cref="TextLayout"/> used to render the text.
+        /// </summary>
+        public TextLayout TextLayout
+        {
+            get
+            {
+                return _textLayout ?? (_textLayout = CreateTextLayout(_constraint, Text));
+            }
+        }
+
         /// <summary>
         /// Gets or sets a brush used to paint the control's background.
         /// </summary>
@@ -186,28 +210,21 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Gets the <see cref="FormattedText"/> used to render the text.
+        /// Gets or sets the control's text wrapping mode.
         /// </summary>
-        public FormattedText FormattedText
+        public TextWrapping TextWrapping
         {
-            get
-            {
-                if (_formattedText == null)
-                {
-                    _formattedText = CreateFormattedText(_constraint, Text);
-                }
-
-                return _formattedText;
-            }
+            get { return GetValue(TextWrappingProperty); }
+            set { SetValue(TextWrappingProperty, value); }
         }
 
         /// <summary>
-        /// Gets or sets the control's text wrapping mode.
+        /// Gets or sets the control's text trimming mode.
         /// </summary>
-        public TextWrapping TextWrapping
+        public TextTrimming TextTrimming
         {
-            get { return GetValue(TextWrappingProperty); }
-            set { SetValue(TextWrappingProperty, value); }
+            get { return GetValue(TextTrimmingProperty); }
+            set { SetValue(TextTrimmingProperty, value); }
         }
 
         /// <summary>
@@ -219,6 +236,15 @@ namespace Avalonia.Controls
             set { SetValue(TextAlignmentProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the text decorations.
+        /// </summary>
+        public TextDecorationCollection TextDecorations
+        {
+            get => GetValue(TextDecorationsProperty);
+            set => SetValue(TextDecorationsProperty, value);
+        }
+
         /// <summary>
         /// Gets the value of the attached <see cref="FontFamilyProperty"/> on a control.
         /// </summary>
@@ -337,39 +363,41 @@ namespace Avalonia.Controls
                 context.FillRectangle(background, new Rect(Bounds.Size));
             }
 
-            FormattedText.Constraint = Bounds.Size;
-            context.DrawText(Foreground, new Point(), FormattedText);
+            TextLayout?.Draw(context.PlatformImpl, new Point());
         }
 
         /// <summary>
-        /// Creates the <see cref="FormattedText"/> used to render the text.
+        /// Creates the <see cref="TextLayout"/> used to render the text.
         /// </summary>
         /// <param name="constraint">The constraint of the text.</param>
         /// <param name="text">The text to format.</param>
-        /// <returns>A <see cref="FormattedText"/> object.</returns>
-        protected virtual FormattedText CreateFormattedText(Size constraint, string text)
+        /// <returns>A <see cref="TextLayout"/> object.</returns>
+        protected virtual TextLayout CreateTextLayout(Size constraint, string text)
         {
-            return new FormattedText
+            if (constraint == Size.Empty)
             {
-                Constraint = constraint,
-                Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
-                FontSize = FontSize,
-                Text = text ?? string.Empty,
-                TextAlignment = TextAlignment,
-                TextWrapping = TextWrapping,
-            };
+                return null;
+            }
+
+            return new TextLayout(
+                text ?? string.Empty,
+                FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
+                FontSize,
+                Foreground,
+                TextAlignment,
+                TextWrapping,
+                TextTrimming,
+                TextDecorations,
+                constraint.Width,
+                constraint.Height);
         }
 
         /// <summary>
-        /// Invalidates <see cref="FormattedText"/>.
+        /// Invalidates <see cref="TextLayout"/>.
         /// </summary>
-        protected void InvalidateFormattedText()
+        protected void InvalidateTextLayout()
         {
-            if (_formattedText != null)
-            {
-                _constraint = _formattedText.Constraint;
-                _formattedText = null;
-            }
+            _textLayout = null;
         }
 
         /// <summary>
@@ -379,33 +407,31 @@ namespace Avalonia.Controls
         /// <returns>The desired size.</returns>
         protected override Size MeasureOverride(Size availableSize)
         {
-            if (!string.IsNullOrEmpty(Text))
+            if (string.IsNullOrEmpty(Text))
             {
-                if (TextWrapping == TextWrapping.Wrap)
-                {
-                    FormattedText.Constraint = new Size(availableSize.Width, double.PositiveInfinity);
-                }
-                else
-                {
-                    FormattedText.Constraint = Size.Infinity;
-                }
-
-                return FormattedText.Bounds.Size;
+                return new Size();
             }
 
-            return new Size();
+            if (_constraint != availableSize)
+            {
+                InvalidateTextLayout();
+            }
+
+            _constraint = availableSize;
+
+            return TextLayout?.Bounds.Size ?? Size.Empty;
         }
 
         protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
             base.OnAttachedToLogicalTree(e);
-            InvalidateFormattedText();
+            InvalidateTextLayout();
             InvalidateMeasure();
         }
 
         private void OnTextPropertiesChanged()
         {
-            InvalidateFormattedText();
+            InvalidateTextLayout();
             InvalidateMeasure();
         }
     }

BIN
src/Avalonia.Diagnostics/Assets/Fonts/SourceSansPro-Regular.ttf


+ 11 - 1
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@@ -1,8 +1,15 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
+    <RootNamespace>Avalonia</RootNamespace>
   </PropertyGroup>
   <ItemGroup>
+    <Compile Update="**\*.xaml.cs">
+      <DependentUpon>%(Filename)</DependentUpon>
+    </Compile>
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
     <ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
     <ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
     <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
@@ -14,7 +21,10 @@
     <ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
     <ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
     <ProjectReference Include="..\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
-  </ItemGroup>  
+  </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.4.0" />
+  </ItemGroup>
   <Import Project="..\..\build\EmbedXaml.props" />
   <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\BuildTargets.targets" />

+ 0 - 24
src/Avalonia.Diagnostics/DevTools.xaml

@@ -1,24 +0,0 @@
-<UserControl xmlns="https://github.com/avaloniaui"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             x:Class="Avalonia.Diagnostics.DevTools">
-  <Grid RowDefinitions="*,Auto" Margin="4">
-
-    <TabControl Grid.Row="0" Items="{Binding Tools}" SelectedItem="{Binding SelectedTool}">
-      <TabControl.ItemTemplate>
-        <DataTemplate>
-          <TextBlock Text="{Binding Name}" />
-        </DataTemplate>
-      </TabControl.ItemTemplate>
-    </TabControl>
-
-    <StackPanel Grid.Row="1" Spacing="4" Orientation="Horizontal">
-      <TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
-      <Separator Width="8" />
-      <TextBlock>Focused:</TextBlock>
-      <TextBlock Text="{Binding FocusedControl}" />
-      <Separator Width="8" />
-      <TextBlock>Pointer Over:</TextBlock>
-      <TextBlock Text="{Binding PointerOverElement}" />
-    </StackPanel>
-  </Grid>
-</UserControl>

+ 0 - 159
src/Avalonia.Diagnostics/DevTools.xaml.cs

@@ -1,159 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reactive.Linq;
-using Avalonia.Controls;
-using Avalonia.Controls.Primitives;
-using Avalonia.Diagnostics.ViewModels;
-using Avalonia.Input;
-using Avalonia.Input.Raw;
-using Avalonia.Interactivity;
-using Avalonia.Markup.Xaml;
-using Avalonia.Rendering;
-using Avalonia.VisualTree;
-
-namespace Avalonia
-{
-    public static class DevToolsExtensions
-    {
-        public static void AttachDevTools(this TopLevel control)
-        {
-            Diagnostics.DevTools.Attach(control, new KeyGesture(Key.F12));
-        }
-
-        public static void AttachDevTools(this TopLevel control, KeyGesture gesture)
-        {
-            Diagnostics.DevTools.Attach(control, gesture);
-        }
-
-        public static void OpenDevTools(this TopLevel control)
-        {
-            Diagnostics.DevTools.OpenDevTools(control);
-        }
-    }
-}
-
-namespace Avalonia.Diagnostics
-{
-    public class DevTools : UserControl
-    {
-        private static readonly Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
-        private static readonly HashSet<IRenderRoot> s_visualTreeRoots = new HashSet<IRenderRoot>();
-        private readonly IDisposable _keySubscription;
-
-        public DevTools(IControl root)
-        {
-            InitializeComponent();
-            Root = root;
-            DataContext = new DevToolsViewModel(root);
-
-            _keySubscription = InputManager.Instance.Process
-                .OfType<RawKeyEventArgs>()
-                .Subscribe(RawKeyDown);
-        }
-
-        // HACK: needed for XAMLIL, will fix that later
-        public DevTools()
-        {
-        }
-
-        public IControl Root { get; }
-
-        public static IDisposable Attach(TopLevel control, KeyGesture gesture)
-        {
-            void PreviewKeyDown(object sender, KeyEventArgs e)
-            {
-                if (gesture.Matches(e))
-                {
-                    OpenDevTools(control);
-                }
-            }
-
-            return control.AddHandler(
-                KeyDownEvent,
-                PreviewKeyDown,
-                RoutingStrategies.Tunnel);
-        }
-
-        internal static void OpenDevTools(TopLevel control)
-        {
-            if (s_open.TryGetValue(control, out var devToolsWindow))
-            {
-                devToolsWindow.Activate();
-            }
-            else
-            {
-                var devTools = new DevTools(control);
-
-                devToolsWindow = new Window
-                {
-                    Width = 1024,
-                    Height = 512,
-                    Content = devTools,
-                    DataTemplates = { new ViewLocator<ViewModelBase>() },
-                    Title = "Avalonia DevTools"
-                };
-
-                devToolsWindow.Closed += devTools.DevToolsClosed;
-                s_open.Add(control, devToolsWindow);
-                MarkAsDevTool(devToolsWindow);
-                devToolsWindow.Show();
-            }
-        }
-
-        private void DevToolsClosed(object sender, EventArgs e)
-        {
-            var devToolsWindow = (Window)sender;
-            var devTools = (DevTools)devToolsWindow.Content;
-            s_open.Remove((TopLevel)devTools.Root);
-            RemoveDevTool(devToolsWindow);
-            _keySubscription.Dispose();
-            devToolsWindow.Closed -= DevToolsClosed;
-        }
-
-        private void InitializeComponent()
-        {
-            AvaloniaXamlLoader.Load(this);
-        }
-
-        private void RawKeyDown(RawKeyEventArgs e)
-        {
-            const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift;
-
-            if (e.Modifiers == modifiers)
-            {
-                var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point);
-                var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible))
-                    .FirstOrDefault();
-
-                if (control != null)
-                {
-                    var vm = (DevToolsViewModel)DataContext;
-                    vm.SelectControl((IControl)control);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Marks a visual as part of the DevTools, so it can be excluded from event tracking.
-        /// </summary>
-        /// <param name="visual">The visual whose root is to be marked.</param>
-        public static void MarkAsDevTool(IVisual visual)
-        {
-            s_visualTreeRoots.Add(visual.GetVisualRoot());
-        }
-
-        public static void RemoveDevTool(IVisual visual)
-        {
-            s_visualTreeRoots.Remove(visual.GetVisualRoot());
-        }
-
-        public static bool BelongsToDevTool(IVisual visual)
-        {
-            return s_visualTreeRoots.Contains(visual.GetVisualRoot());
-        }
-    }
-}

+ 31 - 0
src/Avalonia.Diagnostics/DevToolsExtensions.cs

@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using Avalonia.Diagnostics;
+using Avalonia.Input;
+
+namespace Avalonia
+{
+    /// <summary>
+    /// Extension methods for attaching DevTools..
+    /// </summary>
+    public static class DevToolsExtensions
+    {
+        /// <summary>
+        /// Attaches DevTools to a window, to be opened with the F12 key.
+        /// </summary>
+        /// <param name="root">The window to attach DevTools to.</param>
+        public static void AttachDevTools(this TopLevel root)
+        {
+            DevTools.Attach(root, new KeyGesture(Key.F12));
+        }
+
+        /// <summary>
+        /// Attaches DevTools to a window, to be opened with the specified key gesture.
+        /// </summary>
+        /// <param name="root">The window to attach DevTools to.</param>
+        /// <param name="gesture">The key gesture to open DevTools.</param>
+        public static void AttachDevTools(this TopLevel root, KeyGesture gesture)
+        {
+            DevTools.Attach(root, gesture);
+        }
+    }
+}

+ 22 - 0
src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs

@@ -0,0 +1,22 @@
+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();
+        }
+    }
+}

+ 61 - 0
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using Avalonia.Controls;
+using Avalonia.Diagnostics.Views;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Diagnostics
+{
+    public static class DevTools
+    {
+        private static readonly Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
+
+        public static IDisposable Attach(TopLevel root, KeyGesture gesture)
+        {
+            void PreviewKeyDown(object sender, KeyEventArgs e)
+            {
+                if (gesture.Matches(e))
+                {
+                    Open(root);
+                }
+            }
+
+            return root.AddHandler(
+                InputElement.KeyDownEvent,
+                PreviewKeyDown,
+                RoutingStrategies.Tunnel);
+        }
+
+        public static IDisposable Open(TopLevel root)
+        {
+            if (s_open.TryGetValue(root, out var window))
+            {
+                window.Activate();
+            }
+            else
+            {
+                window = new MainWindow
+                {
+                    Width = 1024,
+                    Height = 512,
+                    Root = root,
+                };
+
+                window.Closed += DevToolsClosed;
+                s_open.Add(root, window);
+                window.Show();
+            }
+
+            return Disposable.Create(() => window?.Close());
+        }
+
+        private static void DevToolsClosed(object sender, EventArgs e)
+        {
+            var window = (MainWindow)sender;
+            s_open.Remove(window.Root);
+            window.Closed -= DevToolsClosed;
+        }
+    }
+}

+ 36 - 0
src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs

@@ -0,0 +1,36 @@
+#pragma warning disable IDE1006 // Naming Styles
+
+using Avalonia.Diagnostics.ViewModels;
+
+namespace Avalonia.Diagnostics.Models
+{
+    public class ConsoleContext
+    {
+        private readonly ConsoleViewModel _owner;
+
+        internal ConsoleContext(ConsoleViewModel owner) => _owner = owner;
+
+        public readonly string help = @"Welcome to Avalonia DevTools. Here you can execute arbitrary C# code using Roslyn scripting.
+
+The following variables are available:
+
+e: The control currently selected in the logical or visual tree view
+root: The root of the visual tree
+
+The following commands are available:
+
+clear(): Clear the output history
+";
+
+        public dynamic e { get; internal set; }
+        public dynamic root { get; internal set; }
+
+        internal static object NoOutput { get; } = new object();
+
+        public object clear()
+        {
+            _owner.History.Clear();
+            return NoOutput;
+        }
+    }
+}

+ 19 - 0
src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleHistoryItem.cs

@@ -0,0 +1,19 @@
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Diagnostics.Models
+{
+    internal class ConsoleHistoryItem
+    {
+        public ConsoleHistoryItem(string input, object output)
+        {
+            Input = input;
+            Output = output;
+            Foreground = output is Exception ? Brushes.Red : Brushes.Green;
+        }
+
+        public string Input { get; }
+        public object Output { get; }
+        public IBrush Foreground { get; }
+    }
+}

+ 0 - 0
src/Avalonia.Diagnostics/Models/EventChainLink.cs → src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs


+ 3 - 5
src/Avalonia.Diagnostics/ViewLocator.cs → src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs

@@ -1,13 +1,11 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
 using System;
 using Avalonia.Controls;
 using Avalonia.Controls.Templates;
+using Avalonia.Diagnostics.ViewModels;
 
 namespace Avalonia.Diagnostics
 {
-    internal class ViewLocator<TViewModel> : IDataTemplate
+    internal class ViewLocator : IDataTemplate
     {
         public bool SupportsRecycling => false;
 
@@ -28,7 +26,7 @@ namespace Avalonia.Diagnostics
 
         public bool Match(object data)
         {
-            return data is TViewModel;
+            return data is ViewModelBase;
         }
     }
 }

+ 95 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs

@@ -0,0 +1,95 @@
+using System.ComponentModel;
+using Avalonia.Collections;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class AvaloniaPropertyViewModel : PropertyViewModel
+    {
+        private readonly AvaloniaObject _target;
+        private string _type;
+        private object _value;
+        private string _priority;
+        private string _group;
+
+        public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property)
+        {
+            _target = o;
+            Property = property;
+
+            Name = property.IsAttached ?
+                $"[{property.OwnerType.Name}.{property.Name}]" :
+                property.Name;
+
+            if (property.IsDirect)
+            {
+                _group = "Properties";
+                Priority = "Direct";
+            }
+
+            Update();
+        }
+
+        public AvaloniaProperty Property { get; }
+        public override object Key => Property;
+        public override string Name { get; }
+        public bool IsAttached => Property.IsAttached;
+
+        public string Priority
+        {
+            get => _priority;
+            private set => RaiseAndSetIfChanged(ref _priority, value);
+        }
+
+        public override string Type => _type;
+
+        public override string Value
+        {
+            get => ConvertToString(_value);
+            set
+            {
+                try
+                {
+                    var convertedValue = ConvertFromString(value, Property.PropertyType);
+                    _target.SetValue(Property, convertedValue);
+                }
+                catch { }
+            }
+        }
+
+        public override string Group
+        {
+            get => _group;
+        }
+
+        public override void Update()
+        {
+            if (Property.IsDirect)
+            {
+                RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value));
+                RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
+            }
+            else
+            {
+                var val = _target.GetDiagnostic(Property);
+
+                RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value));
+                RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
+
+                if (val != null)
+                {
+                    SetGroup(IsAttached ? "Attached Properties" : "Properties");
+                    Priority = val.Priority.ToString();
+                }
+                else
+                {
+                    SetGroup(Priority = "Unset");
+                }
+            }
+        }
+
+        private void SetGroup(string group)
+        {
+            RaiseAndSetIfChanged(ref _group, group, nameof(Group));
+        }
+    }
+}

+ 57 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs

@@ -0,0 +1,57 @@
+using System.ComponentModel;
+using System.Reflection;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class ClrPropertyViewModel : PropertyViewModel
+    {
+        private readonly object _target;
+        private string _type;
+        private object _value;
+
+        public ClrPropertyViewModel(object o, PropertyInfo property)
+        {
+            _target = o;
+            Property = property;
+
+            if (!property.DeclaringType.IsInterface)
+            {
+                Name = property.Name;
+            }
+            else
+            {
+                Name = property.DeclaringType.Name + '.' + property.Name;
+            }
+
+            Update();
+        }
+
+        public PropertyInfo Property { get; }
+        public override object Key => Name;
+        public override string Name { get; }
+        public override string Group => "CLR Properties";
+
+        public override string Type => _type;
+
+        public override string Value 
+        {
+            get => ConvertToString(_value);
+            set
+            {
+                try
+                {
+                    var convertedValue = ConvertFromString(value, Property.PropertyType);
+                    Property.SetValue(_target, convertedValue);
+                }
+                catch { }
+            }
+        }
+
+        public override void Update()
+        {
+            var val = Property.GetValue(_target);
+            RaiseAndSetIfChanged(ref _value, val, nameof(Value));
+            RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
+        }
+    }
+}

+ 112 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs

@@ -0,0 +1,112 @@
+using System;
+using System.Reflection;
+using System.Threading.Tasks;
+using Avalonia.Collections;
+using Avalonia.Diagnostics.Models;
+using Microsoft.CodeAnalysis.CSharp.Scripting;
+using Microsoft.CodeAnalysis.Scripting;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class ConsoleViewModel : ViewModelBase
+    {
+        private readonly ConsoleContext _context;
+        private readonly Action<ConsoleContext> _updateContext;
+        private int _historyIndex = -1;
+        private string _input;
+        private bool _isVisible;
+        private ScriptState<object> _state;
+
+        public ConsoleViewModel(Action<ConsoleContext> updateContext)
+        {
+            _context = new ConsoleContext(this);
+            _updateContext = updateContext;
+        }
+
+        public string Input
+        {
+            get => _input;
+            set => RaiseAndSetIfChanged(ref _input, value);
+        }
+
+        public bool IsVisible
+        {
+            get => _isVisible;
+            set => RaiseAndSetIfChanged(ref _isVisible, value);
+        }
+
+        public AvaloniaList<ConsoleHistoryItem> History { get; } = new AvaloniaList<ConsoleHistoryItem>();
+
+        public async Task Execute()
+        {
+            if (string.IsNullOrWhiteSpace(Input))
+            {
+                return;
+            }
+
+            try
+            {
+                var options = ScriptOptions.Default
+                    .AddReferences(Assembly.GetAssembly(typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo)));
+
+                _updateContext(_context);
+
+                if (_state == null)
+                {
+                    _state = await CSharpScript.RunAsync(Input, options: options, globals: _context);
+                }
+                else
+                {
+                    _state = await _state.ContinueWithAsync(Input);
+                }
+
+                if (_state.ReturnValue != ConsoleContext.NoOutput)
+                {
+                    History.Add(new ConsoleHistoryItem(Input, _state.ReturnValue ?? "(null)"));
+                }
+            }
+            catch (Exception ex)
+            {
+                History.Add(new ConsoleHistoryItem(Input, ex));
+            }
+
+            Input = string.Empty;
+            _historyIndex = -1;
+        }
+
+        public void HistoryUp()
+        {
+            if (History.Count > 0)
+            {
+                if (_historyIndex == -1)
+                {
+                    _historyIndex = History.Count - 1;
+                }
+                else if (_historyIndex > 0)
+                {
+                    --_historyIndex;
+                }
+
+                Input = History[_historyIndex].Input;
+            }
+        }
+
+        public void HistoryDown()
+        {
+            if (History.Count > 0 && _historyIndex >= 0)
+            {
+                if (_historyIndex == History.Count - 1)
+                {
+                    _historyIndex = -1;
+                    Input = string.Empty;
+                }
+                else
+                {
+                    Input = History[++_historyIndex].Input;
+                }
+            }
+        }
+
+        public void ToggleVisibility() => IsVisible = !IsVisible;
+    }
+}

+ 179 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using Avalonia.Collections;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class ControlDetailsViewModel : ViewModelBase, IDisposable
+    {
+        private readonly IVisual _control;
+        private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
+        private AvaloniaPropertyViewModel _selectedProperty;
+        private string _propertyFilter;
+
+        public ControlDetailsViewModel(IVisual control, string propertyFilter)
+        {
+            _control = control;
+
+            var properties = GetAvaloniaProperties(control)
+                .Concat(GetClrProperties(control))
+                .OrderBy(x => x, PropertyComparer.Instance)
+                .ThenBy(x => x.Name)
+                .ToList();
+
+            _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList());
+            _propertyFilter = propertyFilter;
+
+            var view = new DataGridCollectionView(properties);
+            view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)));
+            view.Filter = FilterProperty;
+            PropertiesView = view;
+
+            if (control is INotifyPropertyChanged inpc)
+            {
+                inpc.PropertyChanged += ControlPropertyChanged;
+            }
+
+            if (control is AvaloniaObject ao)
+            {
+                ao.PropertyChanged += ControlPropertyChanged;
+            }
+        }
+
+        public DataGridCollectionView PropertiesView { get; }
+
+        public string PropertyFilter
+        {
+            get => _propertyFilter;
+            set
+            {
+                if (RaiseAndSetIfChanged(ref _propertyFilter, value))
+                {
+                    PropertiesView.Refresh();
+                }
+            }
+        }
+
+        public AvaloniaPropertyViewModel SelectedProperty
+        {
+            get => _selectedProperty;
+            set => RaiseAndSetIfChanged(ref _selectedProperty, value);
+        }
+
+        public void Dispose()
+        {
+            if (_control is INotifyPropertyChanged inpc)
+            {
+                inpc.PropertyChanged -= ControlPropertyChanged;
+            }
+
+            if (_control is AvaloniaObject ao)
+            {
+                ao.PropertyChanged -= ControlPropertyChanged;
+            }
+        }
+
+        private IEnumerable<PropertyViewModel> GetAvaloniaProperties(object o)
+        {
+            if (o is AvaloniaObject ao)
+            {
+                return AvaloniaPropertyRegistry.Instance.GetRegistered(ao)
+                    .Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(ao.GetType()))
+                    .Select(x => new AvaloniaPropertyViewModel(ao, x));
+            }
+            else
+            {
+                return Enumerable.Empty<AvaloniaPropertyViewModel>();
+            }
+        }
+
+        private IEnumerable<PropertyViewModel> GetClrProperties(object o)
+        {
+            foreach (var p in GetClrProperties(o, o.GetType()))
+            {
+                yield return p;
+            }
+
+            foreach (var i in o.GetType().GetInterfaces())
+            {
+                foreach (var p in GetClrProperties(o, i))
+                {
+                    yield return p;
+                }
+            }
+        }
+
+        private IEnumerable<PropertyViewModel> GetClrProperties(object o, Type t)
+        {
+            return t.GetProperties()
+                .Where(x => x.GetIndexParameters().Length == 0)
+                .Select(x => new ClrPropertyViewModel(o, x));
+        }
+
+        private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+        {
+            if (_propertyIndex.TryGetValue(e.Property, out var properties))
+            {
+                foreach (var property in properties)
+                {
+                    property.Update();
+                }
+            }
+        }
+
+        private void ControlPropertyChanged(object sender, PropertyChangedEventArgs e)
+        {
+            if (_propertyIndex.TryGetValue(e.PropertyName, out var properties))
+            {
+                foreach (var property in properties)
+                {
+                    property.Update();
+                }
+            }
+        }
+
+        private bool FilterProperty(object arg)
+        {
+            if (!string.IsNullOrWhiteSpace(PropertyFilter) && arg is PropertyViewModel property)
+            {
+                return property.Name.IndexOf(PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1;
+            }
+
+            return true;
+        }
+
+        private class PropertyComparer : IComparer<PropertyViewModel>
+        {
+            public static PropertyComparer Instance { get; } = new PropertyComparer();
+
+            public int Compare(PropertyViewModel x, PropertyViewModel y)
+            {
+                var groupX = GroupIndex(x.Group);
+                var groupY = GroupIndex(y.Group);
+
+                if (groupX != groupY)
+                {
+                    return groupX - groupY;
+                }
+                else
+                {
+                    return string.CompareOrdinal(x.Name, y.Name);
+                }
+            }
+
+            private int GroupIndex(string group)
+            {
+                switch (group)
+                {
+                    case "Properties": return 0;
+                    case "Attached Properties": return 1;
+                    case "CLR Properties": return 2;
+                    default: return 3;
+                }
+            }
+        }
+    }
+}

+ 1 - 1
src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Diagnostics.ViewModels
             InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent
         };
 
-        public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsViewModel vm)
+        public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsPageViewModel vm)
             : base(null, type.Name)
         {
             Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name)

+ 19 - 3
src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs

@@ -3,6 +3,7 @@
 
 using System;
 using Avalonia.Diagnostics.Models;
+using Avalonia.Diagnostics.Views;
 using Avalonia.Interactivity;
 using Avalonia.Threading;
 using Avalonia.VisualTree;
@@ -12,11 +13,11 @@ namespace Avalonia.Diagnostics.ViewModels
     internal class EventTreeNode : EventTreeNodeBase
     {
         private readonly RoutedEvent _event;
-        private readonly EventsViewModel _parentViewModel;
+        private readonly EventsPageViewModel _parentViewModel;
         private bool _isRegistered;
         private FiredEvent _currentEvent;
 
-        public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsViewModel vm)
+        public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsPageViewModel vm)
             : base(parent, @event.Name)
         {
             Contract.Requires<ArgumentNullException>(@event != null);
@@ -65,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels
         {
             if (!_isRegistered || IsEnabled == false)
                 return;
-            if (sender is IVisual v && DevTools.BelongsToDevTool(v))
+            if (sender is IVisual v && BelongsToDevTool(v))
                 return;
 
             var s = sender;
@@ -94,5 +95,20 @@ namespace Avalonia.Diagnostics.ViewModels
             else
                 handler();
         }
+
+        private static bool BelongsToDevTool(IVisual v)
+        {
+            while (v != null)
+            {
+                if (v is MainView || v is MainWindow)
+                {
+                    return true;
+                }
+
+                v = v.VisualParent;
+            }
+
+            return false;
+        }
     }
 }

+ 0 - 0
src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs


+ 2 - 15
src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs

@@ -12,12 +12,12 @@ using Avalonia.Media;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class EventsViewModel : ViewModelBase, IDevToolViewModel
+    internal class EventsPageViewModel : ViewModelBase
     {
         private readonly IControl _root;
         private FiredEvent _selectedEvent;
 
-        public EventsViewModel(IControl root)
+        public EventsPageViewModel(IControl root)
         {
             _root = root;
 
@@ -45,17 +45,4 @@ namespace Avalonia.Diagnostics.ViewModels
             RecordedEvents.Clear();
         }
     }
-
-    internal class BoolToBrushConverter : IValueConverter
-    {
-        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
-        {
-            return (bool)value ? Brushes.Green : Brushes.Transparent;
-        }
-
-        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
-        {
-            throw new NotImplementedException();
-        }
-    }
 }

+ 0 - 0
src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs


+ 2 - 4
src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs

@@ -1,6 +1,3 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
 using Avalonia.Collections;
 using Avalonia.Controls;
 using Avalonia.LogicalTree;
@@ -17,7 +14,8 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public static LogicalTreeNode[] Create(object control)
         {
-            return control is ILogical logical ? new[] { new LogicalTreeNode(logical, null) } : null;
+            var logical = control as ILogical;
+            return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null;
         }
     }
 }

+ 126 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@@ -0,0 +1,126 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Diagnostics.Models;
+using Avalonia.Input;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class MainViewModel : ViewModelBase, IDisposable
+    {
+        private readonly IControl _root;
+        private readonly TreePageViewModel _logicalTree;
+        private readonly TreePageViewModel _visualTree;
+        private readonly EventsPageViewModel _events;
+        private ViewModelBase _content;
+        private int _selectedTab;
+        private string _focusedControl;
+        private string _pointerOverElement;
+
+        public MainViewModel(IControl root)
+        {
+            _root = root;
+            _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
+            _visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
+            _events = new EventsPageViewModel(root);
+
+            UpdateFocusedControl();
+            KeyboardDevice.Instance.PropertyChanged += (s, e) =>
+            {
+                if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
+                {
+                    UpdateFocusedControl();
+                }
+            };
+
+            SelectedTab = 0;
+            root.GetObservable(TopLevel.PointerOverElementProperty)
+                .Subscribe(x => PointerOverElement = x?.GetType().Name);
+            Console = new ConsoleViewModel(UpdateConsoleContext);
+        }
+
+        public ConsoleViewModel Console { get; }
+
+        public ViewModelBase Content
+        {
+            get { return _content; }
+            private set
+            {
+                if (_content is TreePageViewModel oldTree &&
+                    value is TreePageViewModel newTree &&
+                    oldTree?.SelectedNode?.Visual is IControl control)
+                {
+                    newTree.SelectControl(control);
+                }
+
+                RaiseAndSetIfChanged(ref _content, value);
+            }
+        }
+
+        public int SelectedTab
+        {
+            get { return _selectedTab; }
+            set
+            {
+                _selectedTab = value;
+
+                switch (value)
+                {
+                    case 0:
+                        Content = _logicalTree;
+                        break;
+                    case 1:
+                        Content = _visualTree;
+                        break;
+                    case 2:
+                        Content = _events;
+                        break;
+                }
+
+                RaisePropertyChanged();
+            }
+        }
+
+        public string FocusedControl
+        {
+            get { return _focusedControl; }
+            private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
+        }
+
+        public string PointerOverElement
+        {
+            get { return _pointerOverElement; }
+            private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
+        }
+
+        private void UpdateConsoleContext(ConsoleContext context)
+        {
+            context.root = _root;
+
+            if (Content is TreePageViewModel tree)
+            {
+                context.e = tree.SelectedNode?.Visual;
+            }
+        }
+
+        public void SelectControl(IControl control)
+        {
+            var tree = Content as TreePageViewModel;
+
+            if (tree != null)
+            {
+                tree.SelectControl(control);
+            }
+        }
+
+        public void Dispose()
+        {
+            _logicalTree.Dispose();
+            _visualTree.Dispose();
+        }
+
+        private void UpdateFocusedControl()
+        {
+            FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
+        }
+    }
+}

+ 60 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs

@@ -0,0 +1,60 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Reflection;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal abstract class PropertyViewModel : ViewModelBase
+    {
+        private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
+        private static readonly Type[] StringParameter = new[] { typeof(string) };
+        private static readonly Type[] StringIFormatProviderParameters = new[] { typeof(string), typeof(IFormatProvider) };
+
+        public abstract object Key { get; }
+        public abstract string Name { get; }
+        public abstract string Group { get; }
+        public abstract string Type { get; }
+        public abstract string Value { get; set; }
+        public abstract void Update();
+
+        protected static string ConvertToString(object value)
+        {
+            if (value is null)
+            {
+                return "(null)";
+            }
+
+            var converter = TypeDescriptor.GetConverter(value);
+            return converter?.ConvertToString(value) ?? value.ToString();
+        }
+
+        protected static object ConvertFromString(string s, Type targetType)
+        {
+            var converter = TypeDescriptor.GetConverter(targetType);
+            
+            if (converter != null && converter.CanConvertFrom(typeof(string)))
+            {
+                return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s);
+            }
+            else
+            {
+                var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
+
+                if (method != null)
+                {
+                    return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture });
+                }
+
+                method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null);
+
+                if (method != null)
+                {
+                    return method.Invoke(null, new object[] { s });
+                }
+            }
+
+            throw new InvalidCastException("Unable to convert value.");
+        }
+    }
+}

+ 8 - 7
src/Avalonia.Diagnostics/ViewModels/TreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@@ -27,9 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels
                 var classesChanged = Observable.FromEventPattern<
                         NotifyCollectionChangedEventHandler,
                         NotifyCollectionChangedEventArgs>(
-                        x => styleable.Classes.CollectionChanged += x,
-                        x => styleable.Classes.CollectionChanged -= x)
-                    .TakeUntil(styleable.StyleDetach);
+                    x => styleable.Classes.CollectionChanged += x,
+                    x => styleable.Classes.CollectionChanged -= x)
+                    .TakeUntil(((IStyleable)styleable).StyleDetach);
 
                 classesChanged.Select(_ => Unit.Default)
                     .StartWith(Unit.Default)
@@ -55,8 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public string Classes
         {
-            get => _classes;
-            private set => RaiseAndSetIfChanged(ref _classes, value);
+            get { return _classes; }
+            private set { RaiseAndSetIfChanged(ref _classes, value); }
         }
 
         public IVisual Visual
@@ -66,8 +66,8 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public bool IsExpanded
         {
-            get => _isExpanded;
-            set => RaiseAndSetIfChanged(ref _isExpanded, value);
+            get { return _isExpanded; }
+            set { RaiseAndSetIfChanged(ref _isExpanded, value); }
         }
 
         public TreeNode Parent
@@ -78,6 +78,7 @@ namespace Avalonia.Diagnostics.ViewModels
         public string Type
         {
             get;
+            private set;
         }
     }
 }

+ 31 - 16
src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@@ -1,24 +1,20 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
+using System;
 using Avalonia.Controls;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class TreePageViewModel : ViewModelBase, IDevToolViewModel
+    internal class TreePageViewModel : ViewModelBase, IDisposable
     {
         private TreeNode _selected;
         private ControlDetailsViewModel _details;
+        private string _propertyFilter;
 
-        public TreePageViewModel(TreeNode[] nodes, string name)
+        public TreePageViewModel(TreeNode[] nodes)
         {
             Nodes = nodes;
-            Name = name;
         }
 
-        public string Name { get; }
-
         public TreeNode[] Nodes { get; protected set; }
 
         public TreeNode SelectedNode
@@ -26,9 +22,16 @@ namespace Avalonia.Diagnostics.ViewModels
             get => _selected;
             set
             {
+                if (Details != null)
+                {
+                    _propertyFilter = Details.PropertyFilter;
+                }
+
                 if (RaiseAndSetIfChanged(ref _selected, value))
                 {
-                    Details = value != null ? new ControlDetailsViewModel(value.Visual) : null;
+                    Details = value != null ?
+                        new ControlDetailsViewModel(value.Visual, _propertyFilter) :
+                        null;
                 }
             }
         }
@@ -36,9 +39,19 @@ namespace Avalonia.Diagnostics.ViewModels
         public ControlDetailsViewModel Details
         {
             get => _details;
-            private set => RaiseAndSetIfChanged(ref _details, value);
+            private set
+            {
+                var oldValue = _details;
+
+                if (RaiseAndSetIfChanged(ref _details, value))
+                {
+                    oldValue?.Dispose();
+                }
+            }
         }
 
+        public void Dispose() => _details?.Dispose();
+
         public TreeNode FindNode(IControl control)
         {
             foreach (var node in Nodes)
@@ -90,14 +103,16 @@ namespace Avalonia.Diagnostics.ViewModels
             {
                 return node;
             }
-
-            foreach (var child in node.Children)
+            else
             {
-                var result = FindNode(child, control);
-
-                if (result != null)
+                foreach (var child in node.Children)
                 {
-                    return result;
+                    var result = FindNode(child, control);
+
+                    if (result != null)
+                    {
+                        return result;
+                    }
                 }
             }
 

+ 17 - 9
src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs

@@ -1,34 +1,42 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
+using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Runtime.CompilerServices;
-using JetBrains.Annotations;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
     internal class ViewModelBase : INotifyPropertyChanged
     {
-        public event PropertyChangedEventHandler PropertyChanged;
+        private PropertyChangedEventHandler _propertyChanged;
+        private List<string> events = new List<string>();
+
+        public event PropertyChangedEventHandler PropertyChanged
+        {
+            add { _propertyChanged += value; events.Add("added"); }
+            remove { _propertyChanged -= value; events.Add("removed"); }
+        }
+
+        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
+        {
+        }
 
-        [NotifyPropertyChangedInvocator]
         protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
         {
             if (!EqualityComparer<T>.Default.Equals(field, value))
             {
                 field = value;
-                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+                RaisePropertyChanged(propertyName);
                 return true;
             }
 
             return false;
         }
 
-        [NotifyPropertyChangedInvocator]
         protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
         {
-            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+            var e = new PropertyChangedEventArgs(propertyName);
+            OnPropertyChanged(e);
+            _propertyChanged?.Invoke(this, e);
         }
     }
 }

+ 3 - 2
src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@@ -29,11 +29,12 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        public bool IsInTemplate { get; }
+        public bool IsInTemplate { get; private set; }
 
         public static VisualTreeNode[] Create(object control)
         {
-            return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : null;
+            var visual = control as IVisual;
+            return visual != null ? new[] { new VisualTreeNode(visual, null) } : null;
         }
     }
 }

+ 56 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml

@@ -0,0 +1,56 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Class="Avalonia.Diagnostics.Views.ConsoleView">
+  <UserControl.Styles>
+    <Style Selector="TextBox.console">
+      <Setter Property="FontFamily" Value="/Assets/Fonts/SourceSansPro-Regular.ttf"/>
+      <Setter Property="Template">
+        <ControlTemplate>
+          <Border Name="border"
+                  Background="{TemplateBinding Background}"
+                  BorderBrush="{TemplateBinding BorderBrush}"
+                  BorderThickness="{TemplateBinding BorderThickness}">
+            <DockPanel Margin="{TemplateBinding Padding}">
+              <TextBlock DockPanel.Dock="Left" Margin="0,0,4,0">></TextBlock>
+              <TextPresenter Name="PART_TextPresenter"
+                             Text="{TemplateBinding Text, Mode=TwoWay}"
+                             CaretIndex="{TemplateBinding CaretIndex}"
+                             SelectionStart="{TemplateBinding SelectionStart}"
+                             SelectionEnd="{TemplateBinding SelectionEnd}"
+                             TextAlignment="{TemplateBinding TextAlignment}"
+                             TextWrapping="{TemplateBinding TextWrapping}"
+                             PasswordChar="{TemplateBinding PasswordChar}"/>
+            </DockPanel>
+          </Border>
+        </ControlTemplate>
+      </Setter>
+    </Style>
+  </UserControl.Styles>
+  
+  <DockPanel>
+    <TextBox Name="input"
+             Classes="console"
+             DockPanel.Dock="Bottom"
+             BorderThickness="0"
+             Text="{Binding Input}"/>
+    
+    <ListBox Name="historyList"
+             BorderBrush="{DynamicResource ThemeControlMidBrush}"
+             BorderThickness="0,0,0,1"
+             FontFamily="/Assets/Fonts/SourceSansPro-Regular.ttf"
+             Items="{Binding History}"
+             VirtualizationMode="None">
+      <ListBox.ItemTemplate>
+        <DataTemplate>
+          <StackPanel Orientation="Vertical">
+            <DockPanel>
+              <TextBlock DockPanel.Dock="Left" Margin="0,0,4,0">></TextBlock>
+              <TextBlock Text="{Binding Input}"/>
+            </DockPanel>
+            <TextBlock Foreground="{Binding Foreground}" Text="{Binding Output}"/>
+          </StackPanel>
+        </DataTemplate>
+      </ListBox.ItemTemplate>
+    </ListBox>
+  </DockPanel>
+</UserControl>

+ 64 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs

@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Specialized;
+using Avalonia.Controls;
+using Avalonia.Diagnostics.ViewModels;
+using Avalonia.Input;
+using Avalonia.LogicalTree;
+using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
+
+namespace Avalonia.Diagnostics.Views
+{
+    internal class ConsoleView : UserControl
+    {
+        private readonly ListBox _historyList;
+        private readonly TextBox _input;
+
+        public ConsoleView()
+        {
+            this.InitializeComponent();
+            _historyList = this.FindControl<ListBox>("historyList");
+            ((ILogical)_historyList).LogicalChildren.CollectionChanged += HistoryChanged;
+            _input = this.FindControl<TextBox>("input");
+            _input.KeyDown += InputKeyDown;
+        }
+
+        public void FocusInput() => _input.Focus();
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private void HistoryChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems[0] is IControl control)
+            {
+                DispatcherTimer.RunOnce(control.BringIntoView, TimeSpan.Zero);
+            }
+        }
+
+        private void InputKeyDown(object sender, KeyEventArgs e)
+        {
+            var vm = (ConsoleViewModel)DataContext;
+
+            switch (e.Key)
+            {
+                case Key.Enter:
+                    vm.Execute();
+                    e.Handled = true;
+                    break;
+                case Key.Up:
+                    vm.HistoryUp();
+                    _input.CaretIndex = _input.Text.Length;
+                    e.Handled = true;
+                    break;
+                case Key.Down:
+                    vm.HistoryDown();
+                    _input.CaretIndex = _input.Text.Length;
+                    e.Handled = true;
+                    break;
+            }
+        }
+    }
+}

+ 25 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@@ -0,0 +1,25 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
+             x:Class="Avalonia.Diagnostics.Views.ControlDetailsView">
+  <Grid ColumnDefinitions="*">
+    <DockPanel Grid.Column="0">
+      <TextBox DockPanel.Dock="Top"
+               BorderThickness="0"
+               Text="{Binding PropertyFilter}"
+               Watermark="Filter properties"/>
+      <DataGrid Items="{Binding PropertiesView}"
+                BorderThickness="0"
+                RowBackground="Transparent"
+                SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
+                CanUserResizeColumns="true">
+        <DataGrid.Columns>
+          <DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True"/>
+          <DataGridTextColumn Header="Value" Binding="{Binding Value}"/>
+          <DataGridTextColumn Header="Type" Binding="{Binding Type}"/>
+          <DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True"/>
+        </DataGrid.Columns>
+      </DataGrid>
+    </DockPanel>
+  </Grid>
+</UserControl>

+ 18 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs

@@ -0,0 +1,18 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Avalonia.Diagnostics.Views
+{
+    internal class ControlDetailsView : UserControl
+    {
+        public ControlDetailsView()
+        {
+            InitializeComponent();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 3 - 2
src/Avalonia.Diagnostics/Views/EventsView.xaml → src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml

@@ -1,9 +1,10 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
-             x:Class="Avalonia.Diagnostics.Views.EventsView">
+             xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
+             x:Class="Avalonia.Diagnostics.Views.EventsPageView">
   <UserControl.Resources>
-    <vm:BoolToBrushConverter x:Key="boolToBrush" />
+    <conv:BoolToBrushConverter x:Key="boolToBrush" Brush="#d9ffdc"/>
   </UserControl.Resources>
   <Grid ColumnDefinitions="*,4,3*">
     <TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}"

+ 2 - 2
src/Avalonia.Diagnostics/Views/EventsView.xaml.cs → src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs

@@ -8,11 +8,11 @@ using Avalonia.Markup.Xaml;
 
 namespace Avalonia.Diagnostics.Views
 {
-    public class EventsView : UserControl
+    internal class EventsPageView : UserControl
     {
         private readonly ListBox _events;
 
-        public EventsView()
+        public EventsPageView()
         {
             InitializeComponent();
             _events = this.FindControl<ListBox>("events");

+ 55 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@@ -0,0 +1,55 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:views="clr-namespace:Avalonia.Diagnostics.Views"
+             x:Class="Avalonia.Diagnostics.Views.MainView">
+  <Grid Name="rootGrid" RowDefinitions="Auto,Auto,*,Auto,0,Auto">
+    <Menu>
+      <MenuItem Header="_File">
+        <MenuItem Header="E_xit" Command="{Binding $parent[Window].Close}"/>
+      </MenuItem>
+      <MenuItem Header="_View">
+        <MenuItem Header="_Console" Command="{Binding $parent[UserControl].ToggleConsole}">
+          <MenuItem.Icon>
+            <CheckBox BorderThickness="0"
+                      IsChecked="{Binding Console.IsVisible}"
+                      IsEnabled="False"/>
+          </MenuItem.Icon>
+        </MenuItem>
+      </MenuItem>
+    </Menu>
+    
+    <TabStrip Grid.Row="1" SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
+      <TabStripItem Content="Logical Tree"/>
+      <TabStripItem Content="Visual Tree"/>
+      <TabStripItem Content="Events"/>
+    </TabStrip>
+
+    <ContentControl Grid.Row="2"
+                    BorderBrush="{DynamicResource ThemeControlMidBrush}"
+                    BorderThickness="0,1,0,0"
+                    Content="{Binding Content}"/>
+
+    <GridSplitter Name="consoleSplitter" Grid.Row="3" Height="1"
+                  Background="{DynamicResource ThemeControlMidBrush}"
+                  IsVisible="False"/>
+    
+    <views:ConsoleView Name="console"
+                       Grid.Row="4"
+                       DataContext="{Binding Console}"
+                       IsVisible="{Binding IsVisible}"/>
+
+    <Border Grid.Row="5"
+            BorderBrush="{DynamicResource ThemeControlMidBrush}"
+            BorderThickness="0,1,0,0">
+      <StackPanel Spacing="4" Orientation="Horizontal">
+        <TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
+        <Separator Width="8"/>
+        <TextBlock>Focused:</TextBlock>
+        <TextBlock Text="{Binding FocusedControl}"/>
+        <Separator Width="8"/>
+        <TextBlock>Pointer Over:</TextBlock>
+        <TextBlock Text="{Binding PointerOverElement}"/>
+      </StackPanel>
+    </Border>
+  </Grid>
+</UserControl>

+ 66 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs

@@ -0,0 +1,66 @@
+using Avalonia.Controls;
+using Avalonia.Diagnostics.ViewModels;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
+
+namespace Avalonia.Diagnostics.Views
+{
+    internal class MainView : UserControl
+    {
+        private readonly ConsoleView _console;
+        private readonly GridSplitter _consoleSplitter;
+        private readonly Grid _rootGrid;
+        private readonly int _consoleRow;
+        private double _consoleHeight = -1;
+
+        public MainView()
+        {
+            InitializeComponent();
+            AddHandler(KeyDownEvent, PreviewKeyDown, RoutingStrategies.Tunnel);
+            _console = this.FindControl<ConsoleView>("console");
+            _consoleSplitter = this.FindControl<GridSplitter>("consoleSplitter");
+            _rootGrid = this.FindControl<Grid>("rootGrid");
+            _consoleRow = Grid.GetRow(_console);
+        }
+
+        public void ToggleConsole()
+        {
+            var vm = (MainViewModel)DataContext;
+
+            if (_consoleHeight == -1)
+            {
+                _consoleHeight = Bounds.Height / 3;
+            }
+
+            vm.Console.ToggleVisibility();
+            _consoleSplitter.IsVisible = vm.Console.IsVisible;
+
+            if (vm.Console.IsVisible)
+            {
+                _rootGrid.RowDefinitions[_consoleRow].Height = new GridLength(_consoleHeight, GridUnitType.Pixel);
+                Dispatcher.UIThread.Post(() => _console.FocusInput(), DispatcherPriority.Background);
+            }
+            else
+            {
+                _consoleHeight = _rootGrid.RowDefinitions[_consoleRow].Height.Value;
+                _rootGrid.RowDefinitions[_consoleRow].Height = new GridLength(0, GridUnitType.Pixel);
+            }
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private void PreviewKeyDown(object sender, KeyEventArgs e)
+        {
+            if (e.Key == Key.Escape)
+            {
+                ToggleConsole();
+                e.Handled = true;
+            }
+        }
+    }
+}

+ 18 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml

@@ -0,0 +1,18 @@
+<Window xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:views="clr-namespace:Avalonia.Diagnostics.Views"
+        xmlns:diag="clr-namespace:Avalonia.Diagnostics"
+        Title="Avalonia DevTools"
+        x:Class="Avalonia.Diagnostics.Views.MainWindow">
+  <Window.DataTemplates>
+    <diag:ViewLocator/>
+  </Window.DataTemplates>
+  
+  <Window.Styles>
+    <StyleInclude Source="resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"/>
+    <StyleInclude Source="resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"/>
+    <StyleInclude Source="resm:Avalonia.Controls.DataGrid.Themes.Default.xaml?assembly=Avalonia.Controls.DataGrid"/>
+  </Window.Styles>
+  
+  <views:MainView/>
+</Window>

+ 74 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@@ -0,0 +1,74 @@
+using System;
+using System.Linq;
+using System.Reactive.Linq;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Diagnostics.ViewModels;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Markup.Xaml;
+using Avalonia.Styling;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Diagnostics.Views
+{
+    internal class MainWindow : Window, IStyleHost
+    {
+        private TopLevel _root;
+        private IDisposable _keySubscription;
+
+        public MainWindow()
+        {
+            InitializeComponent();
+
+            _keySubscription = InputManager.Instance.Process
+                .OfType<RawKeyEventArgs>()
+                .Subscribe(RawKeyDown);
+        }
+
+        public TopLevel Root
+        {
+            get => _root;
+            set
+            {
+                if (_root != value)
+                {
+                    _root = value;
+                    DataContext = new MainViewModel(value);
+                }
+            }
+        }
+
+        IStyleHost IStyleHost.StylingParent => null;
+
+        protected override void OnClosed(EventArgs e)
+        {
+            base.OnClosed(e);
+            _keySubscription.Dispose();
+            ((MainViewModel)DataContext)?.Dispose();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private void RawKeyDown(RawKeyEventArgs e)
+        {
+            const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift;
+
+            if (e.Modifiers == modifiers)
+            {
+                var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default;
+                var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible))
+                    .FirstOrDefault();
+
+                if (control != null)
+                {
+                    var vm = (MainViewModel)DataContext;
+                    vm.SelectControl((IControl)control);
+                }
+            }
+        }
+    }
+}

+ 11 - 8
src/Avalonia.Diagnostics/Views/TreePageView.xaml → src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml

@@ -1,26 +1,29 @@
 <UserControl xmlns="https://github.com/avaloniaui"
-             xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
              x:Class="Avalonia.Diagnostics.Views.TreePageView">
-  <Grid ColumnDefinitions="*,Auto,3*">
-    <TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
+  <Grid ColumnDefinitions="*,4,3*">    
+    <TreeView Name="tree"
+              BorderThickness="0"
+              Items="{Binding Nodes}"
+              SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
       <TreeView.DataTemplates>
         <TreeDataTemplate DataType="vm:TreeNode"
                           ItemsSource="{Binding Children}">
           <StackPanel Orientation="Horizontal" Spacing="8">
-            <TextBlock Text="{Binding Type}" />
-            <TextBlock Text="{Binding Classes}" />
+            <TextBlock Text="{Binding Type}"/>
+            <TextBlock Text="{Binding Classes}"/>
           </StackPanel>
         </TreeDataTemplate>
       </TreeView.DataTemplates>
       <TreeView.Styles>
         <Style Selector="TreeViewItem">
-          <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
+          <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
         </Style>
       </TreeView.Styles>
     </TreeView>
 
-    <GridSplitter Grid.Column="1" />
-    <ContentControl Content="{Binding Details}" Grid.Column="2" />
+    <GridSplitter Background="{DynamicResource ThemeControlMidBrush}" Width="1" Grid.Column="1"/>
+    <ContentControl Content="{Binding Details}" Grid.Column="2"/>
   </Grid>
 </UserControl>

+ 3 - 6
src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs → src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@@ -1,6 +1,3 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
 using Avalonia.Controls;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Primitives;
@@ -12,14 +9,14 @@ using Avalonia.Media;
 
 namespace Avalonia.Diagnostics.Views
 {
-    public class TreePageView : UserControl
+    internal class TreePageView : UserControl
     {
         private Control _adorner;
         private TreeView _tree;
 
         public TreePageView()
         {
-            InitializeComponent();
+            this.InitializeComponent();
             _tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
         }
 
@@ -39,7 +36,7 @@ namespace Avalonia.Diagnostics.Views
                 _adorner = new Rectangle
                 {
                     Fill = new SolidColorBrush(0x80a0c5e8),
-                    [AdornerLayer.AdornedElementProperty] = node.Visual
+                    [AdornerLayer.AdornedElementProperty] = node.Visual,
                 };
 
                 layer.Children.Add(_adorner);

+ 0 - 0
src/Avalonia.Diagnostics/VisualTreeDebug.cs → src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs


+ 0 - 25
src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs

@@ -1,25 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System.Collections.Generic;
-using System.Linq;
-using Avalonia.VisualTree;
-
-namespace Avalonia.Diagnostics.ViewModels
-{
-    internal class ControlDetailsViewModel : ViewModelBase
-    {
-        public ControlDetailsViewModel(IVisual control)
-        {
-            if (control is AvaloniaObject avaloniaObject)
-            {
-                Properties = AvaloniaPropertyRegistry.Instance.GetRegistered(avaloniaObject)
-                    .Select(x => new PropertyDetails(avaloniaObject, x))
-                    .OrderBy(x => x.IsAttached)
-                    .ThenBy(x => x.Name);
-            }
-        }
-
-        public IEnumerable<PropertyDetails> Properties { get; }
-    }
-}

+ 0 - 76
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@@ -1,76 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections.ObjectModel;
-using System.Linq;
-using Avalonia.Controls;
-using Avalonia.Input;
-
-namespace Avalonia.Diagnostics.ViewModels
-{
-    internal class DevToolsViewModel : ViewModelBase
-    {
-        private IDevToolViewModel _selectedTool;
-        private string _focusedControl;
-        private string _pointerOverElement;
-
-        public DevToolsViewModel(IControl root)
-        {
-            Tools = new ObservableCollection<IDevToolViewModel>
-            {
-                new TreePageViewModel(LogicalTreeNode.Create(root), "Logical Tree"),
-                new TreePageViewModel(VisualTreeNode.Create(root), "Visual Tree"),
-                new EventsViewModel(root)
-            };
-
-            SelectedTool = Tools.First();
-
-            UpdateFocusedControl();
-
-            KeyboardDevice.Instance.PropertyChanged += (s, e) =>
-            {
-                if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
-                {
-                    UpdateFocusedControl();
-                }
-            };
-
-            root.GetObservable(TopLevel.PointerOverElementProperty)
-                .Subscribe(x => PointerOverElement = x?.GetType().Name);
-        }
-
-        public IDevToolViewModel SelectedTool
-        {
-            get => _selectedTool;
-            set => RaiseAndSetIfChanged(ref _selectedTool, value);
-        }
-
-        public ObservableCollection<IDevToolViewModel> Tools { get; }
-
-        public string FocusedControl
-        {
-            get => _focusedControl;
-            private set => RaiseAndSetIfChanged(ref _focusedControl, value);
-        }
-
-        public string PointerOverElement
-        {
-            get => _pointerOverElement;
-            private set => RaiseAndSetIfChanged(ref _pointerOverElement, value);
-        }
-
-        public void SelectControl(IControl control)
-        {
-            if (SelectedTool is TreePageViewModel tree)
-            {
-                tree.SelectControl(control);
-            }
-        }
-
-        private void UpdateFocusedControl()
-        {
-            FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
-        }
-    }
-}

+ 0 - 16
src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs

@@ -1,16 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-namespace Avalonia.Diagnostics.ViewModels
-{
-    /// <summary>
-    /// View model interface for tool showing up in DevTools
-    /// </summary>
-    public interface IDevToolViewModel
-    {
-        /// <summary>
-        /// Name of a tool.
-        /// </summary>
-        string Name { get; }
-    }
-}

+ 0 - 58
src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs

@@ -1,58 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using Avalonia.Data;
-
-namespace Avalonia.Diagnostics.ViewModels
-{
-    internal class PropertyDetails : ViewModelBase
-    {
-        private object _value;
-        private string _priority;
-        private string _diagnostic;
-
-        public PropertyDetails(AvaloniaObject o, AvaloniaProperty property)
-        {
-            Name = property.IsAttached ?
-                $"[{property.OwnerType.Name}.{property.Name}]" :
-                property.Name;
-            IsAttached = property.IsAttached;
-
-            // TODO: Unsubscribe when view model is deactivated.
-            o.GetObservable(property).Subscribe(x =>
-            {
-                var diagnostic = o.GetDiagnostic(property);
-                Value = diagnostic.Value ?? "(null)";
-                Priority = (diagnostic.Priority != BindingPriority.Unset) ?
-                    diagnostic.Priority.ToString() :
-                    diagnostic.Property.Inherits ?
-                        "Inherited" :
-                        "Unset";
-                Diagnostic = diagnostic.Diagnostic;
-            });
-        }
-
-        public string Name { get; }
-
-        public bool IsAttached { get; }
-
-        public string Priority
-        {
-            get => _priority;
-            private set => RaiseAndSetIfChanged(ref _priority, value);
-        }
-
-        public string Diagnostic
-        {
-            get => _diagnostic;
-            private set => RaiseAndSetIfChanged(ref _diagnostic, value);
-        }
-
-        public object Value
-        {
-            get => _value;
-            private set => RaiseAndSetIfChanged(ref _value, value);
-        }
-    }
-}

+ 0 - 75
src/Avalonia.Diagnostics/Views/ControlDetailsView.cs

@@ -1,75 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections.Generic;
-using System.Reactive.Linq;
-using Avalonia.Controls;
-using Avalonia.Diagnostics.ViewModels;
-using Avalonia.Media;
-
-namespace Avalonia.Diagnostics.Views
-{
-    internal class ControlDetailsView : UserControl
-    {
-        private static readonly StyledProperty<ControlDetailsViewModel> ViewModelProperty =
-            AvaloniaProperty.Register<ControlDetailsView, ControlDetailsViewModel>(nameof(ViewModel));
-
-        private SimpleGrid _grid;
-
-        public ControlDetailsView()
-        {
-            InitializeComponent();
-            this.GetObservable(DataContextProperty)
-                .Subscribe(x => ViewModel = (ControlDetailsViewModel)x);
-        }
-
-        public ControlDetailsViewModel ViewModel
-        {
-            get => GetValue(ViewModelProperty);
-            private set
-            {
-                SetValue(ViewModelProperty, value);
-                _grid[GridRepeater.ItemsProperty] = value?.Properties;
-            }
-        }
-
-        private void InitializeComponent()
-        {
-            Func<object, IEnumerable<Control>> pt = PropertyTemplate;
-
-            Content = new ScrollViewer { Content = _grid = new SimpleGrid { [GridRepeater.TemplateProperty] = pt } };
-        }
-
-        private IEnumerable<Control> PropertyTemplate(object i)
-        {
-            var property = (PropertyDetails)i;
-
-            var margin = new Thickness(2);
-
-            yield return new TextBlock
-            {
-                Margin = margin,
-                Text = property.Name,
-                TextWrapping = TextWrapping.NoWrap,
-                [!ToolTip.TipProperty] = property.GetObservable<string>(nameof(property.Diagnostic)).ToBinding()
-            };
-
-            yield return new TextBlock
-            {
-                Margin = margin,
-                TextWrapping = TextWrapping.NoWrap,
-                [!TextBlock.TextProperty] = property.GetObservable<object>(nameof(property.Value))
-                    .Select(v => v?.ToString())
-                    .ToBinding()
-            };
-
-            yield return new TextBlock
-            {
-                Margin = margin,
-                TextWrapping = TextWrapping.NoWrap,
-                [!TextBlock.TextProperty] = property.GetObservable<string>((nameof(property.Priority))).ToBinding()
-            };
-        }
-    }
-}

+ 0 - 51
src/Avalonia.Diagnostics/Views/GridRepeater.cs

@@ -1,51 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using Avalonia.Controls;
-
-namespace Avalonia.Diagnostics.Views
-{
-    internal static class GridRepeater
-    {
-        public static readonly AttachedProperty<IEnumerable> ItemsProperty =
-            AvaloniaProperty.RegisterAttached<SimpleGrid, IEnumerable>("Items", typeof(GridRepeater));
-
-        public static readonly AttachedProperty<Func<object, IEnumerable<Control>>> TemplateProperty =
-            AvaloniaProperty.RegisterAttached<SimpleGrid, Func<object, IEnumerable<Control>>>("Template",
-                typeof(GridRepeater));
-
-        static GridRepeater()
-        {
-            ItemsProperty.Changed.Subscribe(ItemsChanged);
-        }
-
-        private static void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
-        {
-            var grid = (SimpleGrid)e.Sender;
-            var items = (IEnumerable)e.NewValue;
-            var template = grid.GetValue(TemplateProperty);
-
-            grid.Children.Clear();
-
-            if (items != null)
-            {
-                int count = 0;
-                int cols = 3;
-
-                foreach (var item in items)
-                {
-                    foreach (var control in template(item))
-                    {
-                        grid.Children.Add(control);
-                        SimpleGrid.SetColumn(control, count % cols);
-                        SimpleGrid.SetRow(control, count / cols);
-                        ++count;
-                    }
-                }
-            }
-        }
-    }
-}

+ 0 - 33
src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs

@@ -1,33 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.ComponentModel;
-using System.Reactive.Linq;
-using System.Reflection;
-
-namespace Avalonia.Diagnostics.Views
-{
-    internal static class PropertyChangedExtensions
-    {
-        public static IObservable<T> GetObservable<T>(this INotifyPropertyChanged source, string propertyName)
-        {
-            Contract.Requires<ArgumentNullException>(source != null);
-            Contract.Requires<ArgumentNullException>(propertyName != null);
-
-            var property = source.GetType().GetTypeInfo().GetDeclaredProperty(propertyName);
-
-            if (property == null)
-            {
-                throw new ArgumentException($"Property '{propertyName}' not found on '{source}.");
-            }
-
-            return Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
-                    e => source.PropertyChanged += e,
-                    e => source.PropertyChanged -= e)
-                .Where(e => e.EventArgs.PropertyName == propertyName)
-                .Select(_ => (T)property.GetValue(source))
-                .StartWith((T)property.GetValue(source));
-        }
-    }
-}

+ 0 - 146
src/Avalonia.Diagnostics/Views/SimpleGrid.cs

@@ -1,146 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System.Collections.Generic;
-using Avalonia.Controls;
-
-namespace Avalonia.Diagnostics.Views
-{
-    /// <summary>
-    /// A simple grid control that lays out columns with a equal width and rows to their desired
-    /// size.
-    /// </summary>
-    /// <remarks>
-    /// This is used in the devtools because our <see cref="Grid"/> performance sucks.
-    /// </remarks>
-    public class SimpleGrid : Panel
-    {
-        private readonly List<double> _columnWidths = new List<double>();
-        private readonly List<double> _rowHeights = new List<double>();
-        private double _totalWidth;
-        private double _totalHeight;
-
-        /// <summary>
-        /// Defines the Column attached property.
-        /// </summary>
-        public static readonly AttachedProperty<int> ColumnProperty =
-            AvaloniaProperty.RegisterAttached<SimpleGrid, Control, int>("Column");
-
-        /// <summary>
-        /// Defines the Row attached property.
-        /// </summary>
-        public static readonly AttachedProperty<int> RowProperty =
-            AvaloniaProperty.RegisterAttached<SimpleGrid, Control, int>("Row");
-
-        /// <summary>
-        /// Gets the value of the Column attached property for a control.
-        /// </summary>
-        /// <param name="control">The control.</param>
-        /// <returns>The control's column.</returns>
-        public static int GetColumn(IControl control)
-        {
-            return control.GetValue(ColumnProperty);
-        }
-
-        /// <summary>
-        /// Gets the value of the Row attached property for a control.
-        /// </summary>
-        /// <param name="control">The control.</param>
-        /// <returns>The control's row.</returns>
-        public static int GetRow(IControl control)
-        {
-            return control.GetValue(RowProperty);
-        }
-
-        /// <summary>
-        /// Sets the value of the Column attached property for a control.
-        /// </summary>
-        /// <param name="control">The control.</param>
-        /// <param name="value">The column value.</param>
-        public static void SetColumn(IControl control, int value)
-        {
-            control.SetValue(ColumnProperty, value);
-        }
-
-
-        /// <summary>
-        /// Sets the value of the Row attached property for a control.
-        /// </summary>
-        /// <param name="control">The control.</param>
-        /// <param name="value">The row value.</param>
-        public static void SetRow(IControl control, int value)
-        {
-            control.SetValue(RowProperty, value);
-        }
-
-        protected override Size MeasureOverride(Size availableSize)
-        {
-            _columnWidths.Clear();
-            _rowHeights.Clear();
-            _totalWidth = 0;
-            _totalHeight = 0;
-
-            foreach (var child in Children)
-            {
-                var column = GetColumn(child);
-                var row = GetRow(child);
-
-                child.Measure(availableSize);
-
-                var desired = child.DesiredSize;
-                UpdateCell(_columnWidths, column, desired.Width, ref _totalWidth);
-                UpdateCell(_rowHeights, row, desired.Height, ref _totalHeight);
-            }
-
-            return new Size(_totalWidth, _totalHeight);
-        }
-
-        protected override Size ArrangeOverride(Size finalSize)
-        {
-            var columnWidth = finalSize.Width / _columnWidths.Count;
-
-            foreach (var child in Children)
-            {
-                var column = GetColumn(child);
-                var row = GetRow(child);
-                var rect = new Rect(column * columnWidth, GetRowTop(row), columnWidth, _rowHeights[row]);
-                child.Arrange(rect);
-            }
-
-            return new Size(finalSize.Width, _totalHeight);
-        }
-
-        private double UpdateCell(IList<double> cells, int cell, double value, ref double total)
-        {
-            while (cells.Count < cell + 1)
-            {
-                cells.Add(0);
-            }
-
-            var existing = cells[cell];
-
-            if (value > existing)
-            {
-                cells[cell] = value;
-                total += value - existing;
-                return value;
-            }
-            else
-            {
-                return existing;
-            }
-        }
-
-        private double GetRowTop(int row)
-        {
-            var result = 0.0;
-
-            for (var i = 0; i < row; ++i)
-            {
-                result += _rowHeights[i];
-            }
-
-            return result;
-        }
-    }
-}

+ 1 - 1
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs

@@ -29,7 +29,7 @@ namespace Avalonia.Dialogs
                 using (Process process = Process.Start(new ProcessStartInfo
                 {
                     FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
-                    Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"-e {url}" : "",
+                    Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "",
                     CreateNoWindow = true,
                     UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
                 }));

+ 0 - 1
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@@ -13,7 +13,6 @@
   <Import Project="..\..\build\BuildTargets.targets" />
 
   <ItemGroup>
-    <ProjectReference Include="..\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
     <ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
   </ItemGroup>
 </Project>

+ 6 - 3
src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs

@@ -111,9 +111,7 @@ namespace Avalonia.Input.GestureRecognizers
             _pointerGrabs.Remove(e.Pointer);
             foreach (var r in _recognizers)
             {
-                if(e.Handled)
-                    break;
-                r.PointerCaptureLost(e);
+                r.PointerCaptureLost(e.Pointer);
             }
         }
 
@@ -121,6 +119,11 @@ namespace Avalonia.Input.GestureRecognizers
         {
             pointer.Capture(_inputElement);
             _pointerGrabs[pointer] = recognizer;
+            foreach (var r in _recognizers)
+            {
+                if (r != recognizer)
+                    r.PointerCaptureLost(pointer);
+            }
         }
 
     }

+ 1 - 1
src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs

@@ -6,7 +6,7 @@ namespace Avalonia.Input.GestureRecognizers
         void PointerPressed(PointerPressedEventArgs e);
         void PointerReleased(PointerReleasedEventArgs e);
         void PointerMoved(PointerEventArgs e);
-        void PointerCaptureLost(PointerCaptureLostEventArgs e);
+        void PointerCaptureLost(IPointer pointer);
     }
     
     public interface IGestureRecognizerActionsDispatcher

+ 3 - 2
src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs

@@ -116,9 +116,9 @@ namespace Avalonia.Input.GestureRecognizers
             }
         }
 
-        public void PointerCaptureLost(PointerCaptureLostEventArgs e)
+        public void PointerCaptureLost(IPointer pointer)
         {
-            if (e.Pointer == _tracking) EndGesture();
+            if (pointer == _tracking) EndGesture();
         }
 
         void EndGesture()
@@ -148,6 +148,7 @@ namespace Avalonia.Input.GestureRecognizers
                     EndGesture();
                 else
                 {
+                    _tracking = null;
                     var savedGestureId = _gestureId;
                     var st = Stopwatch.StartNew();
                     var lastTime = TimeSpan.Zero;

+ 2 - 2
src/Avalonia.Input/NavigationDirection.cs

@@ -100,12 +100,12 @@ namespace Avalonia.Input
         /// </returns>
         public static NavigationDirection? ToNavigationDirection(
             this Key key,
-            InputModifiers modifiers = InputModifiers.None)
+            KeyModifiers modifiers = KeyModifiers.None)
         {
             switch (key)
             {
                 case Key.Tab:
-                    return (modifiers & InputModifiers.Shift) != 0 ?
+                    return (modifiers & KeyModifiers.Shift) != 0 ?
                         NavigationDirection.Next : NavigationDirection.Previous;
                 case Key.Up:
                     return NavigationDirection.Up;

+ 37 - 25
src/Avalonia.Styling/StyledElement.cs

@@ -67,6 +67,7 @@ namespace Avalonia
         private Subject<IStyleable> _styleDetach = new Subject<IStyleable>();
         private ITemplatedControl _templatedParent;
         private bool _dataContextUpdating;
+        private bool _notifyingResourcesChanged;
 
         /// <summary>
         /// Initializes static members of the <see cref="StyledElement"/> class.
@@ -214,28 +215,15 @@ namespace Avalonia
         /// </remarks>
         public Styles Styles
         {
-            get { return _styles ?? (Styles = new Styles()); }
-            set
+            get
             {
-                Contract.Requires<ArgumentNullException>(value != null);
-
-                if (_styles != value)
+                if (_styles == null)
                 {
-                    if (_styles != null)
-                    {
-                        (_styles as ISetResourceParent)?.SetParent(null);
-                        _styles.ResourcesChanged -= ThisResourcesChanged;
-                    }
-
-                    _styles = value;
-
-                    if (value is ISetResourceParent setParent && setParent.ResourceParent == null)
-                    {
-                        setParent.SetParent(this);
-                    }
-
+                    _styles = new Styles(this);
                     _styles.ResourcesChanged += ThisResourcesChanged;
                 }
+
+                return _styles;
             }
         }
 
@@ -253,6 +241,7 @@ namespace Avalonia
 
                 if (_resources != null)
                 {
+                    (_resources as ISetResourceParent)?.SetParent(null);
                     hadResources = _resources.Count > 0;
                     _resources.ResourcesChanged -= ThisResourcesChanged;
                 }
@@ -260,9 +249,14 @@ namespace Avalonia
                 _resources = value;
                 _resources.ResourcesChanged += ThisResourcesChanged;
 
+                if (value is ISetResourceParent setParent && setParent.ResourceParent == null)
+                {
+                    setParent.SetParent(this);
+                }
+
                 if (hadResources || _resources.Count > 0)
                 {
-                    ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                    NotifyResourcesChanged(new ResourcesChangedEventArgs());
                 }
             }
         }
@@ -407,10 +401,7 @@ namespace Avalonia
         }
 
         /// <inheritdoc/>
-        void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e)
-        {
-            ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
-        }
+        void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e);
 
         /// <inheritdoc/>
         bool IResourceProvider.TryGetResource(object key, out object value)
@@ -456,7 +447,8 @@ namespace Avalonia
                 {
                     Parent.ResourcesChanged += ThisResourcesChanged;
                 }
-                ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                
+                NotifyResourcesChanged(new ResourcesChangedEventArgs());
 
                 if (Parent is ILogicalRoot || Parent?.IsAttachedToLogicalTree == true || this is ILogicalRoot)
                 {
@@ -721,9 +713,29 @@ namespace Avalonia
             }
         }
 
+        private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
+        {
+            if (_notifyingResourcesChanged)
+            {
+                return;
+            }
+
+            try
+            {
+                _notifyingResourcesChanged = true;
+                (_resources as ISetResourceParent)?.ParentResourcesChanged(e);
+                (_styles as ISetResourceParent)?.ParentResourcesChanged(e);
+                ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
+            }
+            finally
+            {
+                _notifyingResourcesChanged = false;
+            }
+        }
+
         private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e)
         {
-            ((ILogical)this).NotifyResourcesChanged(e);
+            NotifyResourcesChanged(e);
         }
     }
 }

+ 27 - 19
src/Avalonia.Styling/Styling/Styles.cs

@@ -20,6 +20,7 @@ namespace Avalonia.Styling
         private IResourceDictionary _resources;
         private AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>();
         private Dictionary<Type, List<IStyle>> _cache;
+        private bool _notifyingResourcesChanged;
 
         public Styles()
         {
@@ -38,7 +39,7 @@ namespace Avalonia.Styling
                         ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
                     }
 
-                    x.ResourcesChanged += SubResourceChanged;
+                    x.ResourcesChanged += NotifyResourcesChanged;
                     _cache = null;
                 },
                 x =>
@@ -54,12 +55,18 @@ namespace Avalonia.Styling
                         ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
                     }
 
-                    x.ResourcesChanged -= SubResourceChanged;
+                    x.ResourcesChanged -= NotifyResourcesChanged;
                     _cache = null;
                 },
                 () => { });
         }
 
+        public Styles(IResourceNode parent)
+            : this()
+        {
+            _parent = parent;
+        }
+
         public event NotifyCollectionChangedEventHandler CollectionChanged
         {
             add => _styles.CollectionChanged += value;
@@ -90,11 +97,11 @@ namespace Avalonia.Styling
                 if (_resources != null)
                 {
                     hadResources = _resources.Count > 0;
-                    _resources.ResourcesChanged -= ResourceDictionaryChanged;
+                    _resources.ResourcesChanged -= NotifyResourcesChanged;
                 }
 
                 _resources = value;
-                _resources.ResourcesChanged += ResourceDictionaryChanged;
+                _resources.ResourcesChanged += NotifyResourcesChanged;
 
                 if (hadResources || _resources.Count > 0)
                 {
@@ -261,34 +268,35 @@ namespace Avalonia.Styling
         /// <inheritdoc/>
         void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
         {
-            ResourcesChanged?.Invoke(this, e);
+            NotifyResourcesChanged(e);
         }
 
-        private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e)
+        private void NotifyResourcesChanged(object sender, ResourcesChangedEventArgs e)
         {
-            foreach (var child in this)
-            {
-                (child as ISetResourceParent)?.ParentResourcesChanged(e);
-            }
-
-            ResourcesChanged?.Invoke(this, e);
+            NotifyResourcesChanged(e);
         }
 
-        private void SubResourceChanged(object sender, ResourcesChangedEventArgs e)
+        private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
         {
-            var foundSource = false;
+            if (_notifyingResourcesChanged)
+            {
+                return;
+            }
 
-            foreach (var child in this)
+            try
             {
-                if (foundSource)
+                _notifyingResourcesChanged = true;
+                foreach (var child in this)
                 {
                     (child as ISetResourceParent)?.ParentResourcesChanged(e);
                 }
 
-                foundSource |= child == sender;
+                ResourcesChanged?.Invoke(this, e);
+            }
+            finally
+            {
+                _notifyingResourcesChanged = false;
             }
-
-            ResourcesChanged?.Invoke(this, e);
         }
     }
 }

+ 0 - 1
src/Avalonia.Themes.Default/ComboBox.xaml

@@ -37,7 +37,6 @@
                    MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
                    MaxHeight="{TemplateBinding MaxDropDownHeight}"
                    PlacementTarget="{TemplateBinding}"
-                   ObeyScreenEdges="True"
                    StaysOpen="False">
               <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                       BorderThickness="1">

+ 2 - 4
src/Avalonia.Themes.Default/MenuItem.xaml

@@ -45,8 +45,7 @@
             <Popup Name="PART_Popup"
                    PlacementMode="Right"
                    StaysOpen="True"
-                   IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
-                   ObeyScreenEdges="True">
+                   IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
               <Border Background="{TemplateBinding Background}"
                       BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                       BorderThickness="{TemplateBinding BorderThickness}">
@@ -95,8 +94,7 @@
             </ContentPresenter>
             <Popup Name="PART_Popup"
                    IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
-                   StaysOpen="True" 
-                   ObeyScreenEdges="True">
+                   StaysOpen="True">
               <Border Background="{TemplateBinding Background}"
                       BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                       BorderThickness="{TemplateBinding BorderThickness}">

BIN
src/Avalonia.Visuals/Assets/GraphemeBreak.trie


BIN
src/Avalonia.Visuals/Assets/UnicodeData.trie


+ 4 - 0
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@@ -2,7 +2,11 @@
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
     <RootNamespace>Avalonia</RootNamespace>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Assets\*.trie" />
+  </ItemGroup>
   <ItemGroup> 
     <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
     <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />

+ 4 - 8
src/Avalonia.Visuals/Media/FontManager.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Globalization;
@@ -19,7 +20,7 @@ namespace Avalonia.Media
             new ConcurrentDictionary<FontKey, Typeface>();
         private readonly FontFamily _defaultFontFamily;
 
-        private FontManager(IFontManagerImpl platformImpl)
+        public FontManager(IFontManagerImpl platformImpl)
         {
             PlatformImpl = platformImpl;
 
@@ -39,14 +40,9 @@ namespace Avalonia.Media
                     return current;
                 }
 
-                var renderInterface = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
+                var fontManagerImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>();
 
-                var fontManagerImpl = renderInterface?.CreateFontManager();
-
-                if (fontManagerImpl == null)
-                {
-                    return null;
-                }
+                if (fontManagerImpl == null) throw new InvalidOperationException("No font manager implementation was registered.");
 
                 current = new FontManager(fontManagerImpl);
 

+ 162 - 65
src/Avalonia.Visuals/Media/GlyphRun.cs

@@ -13,13 +13,14 @@ namespace Avalonia.Media
     /// </summary>
     public sealed class GlyphRun : IDisposable
     {
-        private static readonly IPlatformRenderInterface s_platformRenderInterface =
-            AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
+        private static readonly IComparer<ushort> s_ascendingComparer = Comparer<ushort>.Default;
+        private static readonly IComparer<ushort> s_descendingComparer = new ReverseComparer<ushort>();
 
         private IGlyphRunImpl _glyphRunImpl;
         private GlyphTypeface _glyphTypeface;
         private double _fontRenderingEmSize;
         private Rect? _bounds;
+        private int _biDiLevel;
 
         private ReadOnlySlice<ushort> _glyphIndices;
         private ReadOnlySlice<double> _glyphAdvances;
@@ -45,7 +46,7 @@ namespace Avalonia.Media
         /// <param name="glyphOffsets">The glyph offsets.</param>
         /// <param name="characters">The characters.</param>
         /// <param name="glyphClusters">The glyph clusters.</param>
-        /// <param name="bidiLevel">The bidi level.</param>
+        /// <param name="biDiLevel">The bidi level.</param>
         /// <param name="bounds">The bound.</param>
         public GlyphRun(
             GlyphTypeface glyphTypeface,
@@ -55,7 +56,7 @@ namespace Avalonia.Media
             ReadOnlySlice<Vector> glyphOffsets = default,
             ReadOnlySlice<char> characters = default,
             ReadOnlySlice<ushort> glyphClusters = default,
-            int bidiLevel = 0,
+            int biDiLevel = 0,
             Rect? bounds = null)
         {
             GlyphTypeface = glyphTypeface;
@@ -72,7 +73,7 @@ namespace Avalonia.Media
 
             GlyphClusters = glyphClusters;
 
-            BidiLevel = bidiLevel;
+            BiDiLevel = biDiLevel;
 
             Initialize(bounds);
         }
@@ -143,21 +144,21 @@ namespace Avalonia.Media
         /// <summary>
         ///     Gets or sets the bidirectional nesting level of the <see cref="GlyphRun"/>.
         /// </summary>
-        public int BidiLevel
+        public int BiDiLevel
         {
-            get;
-            set;
+            get => _biDiLevel;
+            set => Set(ref _biDiLevel, value);
         }
 
         /// <summary>
-        /// 
+        /// Gets the scale of the current <see cref="Media.GlyphTypeface"/>
         /// </summary>
         internal double Scale => FontRenderingEmSize / GlyphTypeface.DesignEmHeight;
 
         /// <summary>
-        ///     
+        /// Returns <c>true</c> if the text direction is left-to-right. Otherwise, returns <c>false</c>.
         /// </summary>
-        internal bool IsLeftToRight => ((BidiLevel & 1) == 0);
+        public bool IsLeftToRight => ((BiDiLevel & 1) == 0);
 
         /// <summary>
         ///     Gets or sets the conservative bounding box of the <see cref="GlyphRun"/>.
@@ -173,9 +174,11 @@ namespace Avalonia.Media
 
                 return _bounds.Value;
             }
-            set => _bounds = value;
         }
 
+        /// <summary>
+        /// The platform implementation of the <see cref="GlyphRun"/>.
+        /// </summary>
         public IGlyphRunImpl GlyphRunImpl
         {
             get
@@ -189,19 +192,38 @@ namespace Avalonia.Media
             }
         }
 
+        /// <summary>
+        /// Retrieves the offset from the leading edge of the <see cref="GlyphRun"/>
+        /// to the leading or trailing edge of a caret stop containing the specified character hit.
+        /// </summary>
+        /// <param name="characterHit">The <see cref="CharacterHit"/> to use for computing the offset.</param>
+        /// <returns>
+        /// A <see cref="double"/> that represents the offset from the leading edge of the <see cref="GlyphRun"/>
+        /// to the leading or trailing edge of a caret stop containing the character hit.
+        /// </returns>
         public double GetDistanceFromCharacterHit(CharacterHit characterHit)
         {
             var distance = 0.0;
 
-            var end = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+            if (characterHit.FirstCharacterIndex + characterHit.TrailingLength > Characters.End)
+            {
+                return Bounds.Width;
+            }
+
+            var glyphIndex = FindGlyphIndex(characterHit.FirstCharacterIndex);
+
+            var currentCluster = _glyphClusters[glyphIndex];
 
-            for (var i = 0; i < _glyphClusters.Length; i++)
+            if (characterHit.TrailingLength > 0)
             {
-                if (_glyphClusters[i] >= end)
+                while (glyphIndex < _glyphClusters.Length && _glyphClusters[glyphIndex] == currentCluster)
                 {
-                    break;
+                    glyphIndex++;
                 }
+            }
 
+            for (var i = 0; i < glyphIndex; i++)
+            {
                 if (GlyphAdvances.IsEmpty)
                 {
                     var glyph = GlyphIndices[i];
@@ -217,6 +239,15 @@ namespace Avalonia.Media
             return distance;
         }
 
+        /// <summary>
+        /// Retrieves the <see cref="CharacterHit"/> value that represents the character hit of the caret of the <see cref="GlyphRun"/>.
+        /// </summary>
+        /// <param name="distance">Offset to use for computing the caret character hit.</param>
+        /// <param name="isInside">Determines whether the character hit is inside the <see cref="GlyphRun"/>.</param>
+        /// <returns>
+        /// A <see cref="CharacterHit"/> value that represents the character hit that is closest to the distance value.
+        /// The out parameter <c>isInside</c> returns <c>true</c> if the character hit is inside the <see cref="GlyphRun"/>; otherwise, <c>false</c>.
+        /// </returns>
         public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
         {
             // Before
@@ -245,37 +276,46 @@ namespace Avalonia.Media
 
             for (; index < GlyphIndices.Length; index++)
             {
+                double advance;
+
                 if (GlyphAdvances.IsEmpty)
                 {
                     var glyph = GlyphIndices[index];
 
-                    currentX += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+                    advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
                 }
                 else
                 {
-                    currentX += GlyphAdvances[index];
+                    advance = GlyphAdvances[index];
                 }
 
-                if (currentX > distance)
+                if (currentX + advance >= distance)
                 {
                     break;
                 }
-            }
 
-            if (index == GlyphIndices.Length)
-            {
-                index--;
+                currentX += advance;
             }
 
             var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width);
 
-            isInside = distance < currentX && width > 0;
+            var offset = GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex));
+
+            isInside = true;
 
-            var isTrailing = distance > currentX - width / 2;
+            var isTrailing = distance > offset + width / 2;
 
             return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex);
         }
 
+        /// <summary>
+        /// Retrieves the next valid caret character hit in the logical direction in the <see cref="GlyphRun"/>.
+        /// </summary>
+        /// <param name="characterHit">The <see cref="CharacterHit"/> to use for computing the next hit value.</param>
+        /// <returns>
+        /// A <see cref="CharacterHit"/> that represents the next valid caret character hit in the logical direction.
+        /// If the return value is equal to <c>characterHit</c>, no further navigation is possible in the <see cref="GlyphRun"/>.
+        /// </returns>
         public CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
         {
             if (characterHit.TrailingLength == 0)
@@ -288,11 +328,24 @@ namespace Avalonia.Media
             return new CharacterHit(nextCharacterHit.FirstCharacterIndex);
         }
 
+        /// <summary>
+        /// Retrieves the previous valid caret character hit in the logical direction in the <see cref="GlyphRun"/>.
+        /// </summary>
+        /// <param name="characterHit">The <see cref="CharacterHit"/> to use for computing the previous hit value.</param>
+        /// <returns>
+        /// A cref="CharacterHit"/> that represents the previous valid caret character hit in the logical direction.
+        /// If the return value is equal to <c>characterHit</c>, no further navigation is possible in the <see cref="GlyphRun"/>.
+        /// </returns>
         public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
         {
-            return characterHit.TrailingLength == 0 ?
-                FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _) :
-                new CharacterHit(characterHit.FirstCharacterIndex);
+            if (characterHit.TrailingLength != 0)
+            {
+                return new CharacterHit(characterHit.FirstCharacterIndex);
+            }
+
+            return characterHit.FirstCharacterIndex == Characters.Start ?
+                new CharacterHit(Characters.Start) :
+                FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
         }
 
         private class ReverseComparer<T> : IComparer<T>
@@ -303,83 +356,121 @@ namespace Avalonia.Media
             }
         }
 
-        private static readonly IComparer<ushort> s_ascendingComparer = Comparer<ushort>.Default;
-        private static readonly IComparer<ushort> s_descendingComparer = new ReverseComparer<ushort>();
-
-        internal CharacterHit FindNearestCharacterHit(int index, out double width)
+        /// <summary>
+        /// Finds a glyph index for given character index.
+        /// </summary>
+        /// <param name="characterIndex">The character index.</param>
+        /// <returns>
+        /// The glyph index.
+        /// </returns>
+        public int FindGlyphIndex(int characterIndex)
         {
-            width = 0.0;
+            if (IsLeftToRight)
+            {
+                if (characterIndex < _glyphClusters[0])
+                {
+                    return 0;
+                }
 
-            if (index < 0)
+                if (characterIndex > _glyphClusters[_glyphClusters.Length - 1])
+                {
+                    return _glyphClusters.End;
+                }
+            }
+            else
             {
-                return default;
+                if (characterIndex < _glyphClusters[_glyphClusters.Length - 1])
+                {
+                    return _glyphClusters.End;
+                }
+
+                if (characterIndex > _glyphClusters[0])
+                {
+                    return 0;
+                }
             }
 
             var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
 
-            var clusters = _glyphClusters.AsSpan();
+            var clusters = _glyphClusters.Buffer.Span;
 
-            int start;
-
-            if (index == 0 && clusters[0] == 0)
-            {
-                start = 0;
-            }
-            else
-            {
-                // Find the start of the cluster at the character index.
-                start = clusters.BinarySearch((ushort)index, comparer);
-            }
+            // Find the start of the cluster at the character index.
+            var start = clusters.BinarySearch((ushort)characterIndex, comparer);
 
             // No cluster found.
             if (start < 0)
             {
-                while (index > 0 && start < 0)
+                while (characterIndex > 0 && start < 0)
                 {
-                    index--;
+                    characterIndex--;
 
-                    start = clusters.BinarySearch((ushort)index, comparer);
+                    start = clusters.BinarySearch((ushort)characterIndex, comparer);
                 }
 
                 if (start < 0)
                 {
-                    return default;
+                    return -1;
                 }
             }
 
-            var trailingLength = 0;
-
-            var currentCluster = clusters[start];
-
-            while (start > 0 && clusters[start - 1] == currentCluster)
+            while (start > 0 && clusters[start - 1] == clusters[start])
             {
                 start--;
             }
 
-            for (var lastIndex = start; lastIndex < _glyphClusters.Length; ++lastIndex)
-            {
-                if (_glyphClusters[lastIndex] != currentCluster)
-                {
-                    break;
-                }
+            return start;
+        }
+
+        /// <summary>
+        /// Finds the nearest <see cref="CharacterHit"/> at given index.
+        /// </summary>
+        /// <param name="index">The index.</param>
+        /// <param name="width">The width of found cluster.</param>
+        /// <returns>
+        /// The nearest <see cref="CharacterHit"/>.
+        /// </returns>
+        public CharacterHit FindNearestCharacterHit(int index, out double width)
+        {
+            width = 0.0;
+
+            var start = FindGlyphIndex(index);
+
+            var currentCluster = _glyphClusters[start];
+
+            var trailingLength = 0;
 
+            while (start < _glyphClusters.Length && _glyphClusters[start] == currentCluster)
+            {
                 if (GlyphAdvances.IsEmpty)
                 {
-                    var glyph = GlyphIndices[lastIndex];
+                    var glyph = GlyphIndices[start];
 
                     width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
                 }
                 else
                 {
-                    width += GlyphAdvances[lastIndex];
+                    width += GlyphAdvances[start];
                 }
 
                 trailingLength++;
+                start++;
+            }
+
+            if (start == _glyphClusters.Length &&
+                currentCluster + trailingLength != Characters.Start + Characters.Length)
+            {
+                trailingLength = Characters.Start + Characters.Length - currentCluster;
             }
 
             return new CharacterHit(currentCluster, trailingLength);
         }
 
+        /// <summary>
+        /// Calculates the bounds of the <see cref="GlyphRun"/>.
+        /// </summary>
+        /// <returns>
+        /// The calculated bounds.
+        /// </returns>
         private Rect CalculateBounds()
         {
             var scale = FontRenderingEmSize / GlyphTypeface.DesignEmHeight;
@@ -416,6 +507,10 @@ namespace Avalonia.Media
             field = value;
         }
 
+        /// <summary>
+        /// Initializes the <see cref="GlyphRun"/>.
+        /// </summary>
+        /// <param name="bounds">Optional pre computed bounds.</param>
         private void Initialize(Rect? bounds)
         {
             if (GlyphIndices.Length == 0)
@@ -435,7 +530,9 @@ namespace Avalonia.Media
                 throw new InvalidOperationException();
             }
 
-            _glyphRunImpl = s_platformRenderInterface.CreateGlyphRun(this, out var width);
+            var platformRenderInterface = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
+
+            _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width);
 
             if (bounds.HasValue)
             {

+ 19 - 5
src/Avalonia.Visuals/Media/GlyphTypeface.cs

@@ -2,16 +2,15 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-
 using Avalonia.Platform;
 
 namespace Avalonia.Media
 {
     public sealed class GlyphTypeface : IDisposable
     {
-        public GlyphTypeface(Typeface typeface)
-        {
-            PlatformImpl = FontManager.Current?.PlatformImpl.CreateGlyphTypeface(typeface);
+        public GlyphTypeface(Typeface typeface) 
+            : this(FontManager.Current?.PlatformImpl.CreateGlyphTypeface(typeface))
+        { 
         }
 
         public GlyphTypeface(IGlyphTypefaceImpl platformImpl)
@@ -75,7 +74,7 @@ namespace Avalonia.Media
         ///     Returns an glyph index for the specified codepoint.
         /// </summary>
         /// <remarks>
-        ///     Returns <c>0</c> if a glyph isn't found.
+        ///     Returns a replacement glyph if a glyph isn't found.
         /// </remarks>
         /// <param name="codepoint">The codepoint.</param>
         /// <returns>
@@ -83,6 +82,21 @@ namespace Avalonia.Media
         /// </returns>
         public ushort GetGlyph(uint codepoint) => PlatformImpl.GetGlyph(codepoint);
 
+        /// <summary>
+        ///     Tries to get an glyph index for specified codepoint.
+        /// </summary>
+        /// <param name="codepoint">The codepoint.</param>
+        /// <param name="glyph">A glyph index.</param>
+        /// <returns>
+        ///     <c>true</c> if an glyph index was found, <c>false</c> otherwise.
+        /// </returns>
+        public bool TryGetGlyph(uint codepoint, out ushort glyph)
+        {
+            glyph = PlatformImpl.GetGlyph(codepoint);
+
+            return glyph != 0;
+        }
+
         /// <summary>
         ///     Returns an array of glyph indices. Codepoints that are not represented by the font are returned as <code>0</code>.
         /// </summary>

+ 56 - 0
src/Avalonia.Visuals/Media/Immutable/ImmutableTextDecoration.cs

@@ -0,0 +1,56 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Media.Immutable
+{
+    /// <summary>
+    /// An immutable representation of a <see cref="TextDecoration"/>.
+    /// </summary>
+    public class ImmutableTextDecoration
+    {
+        public ImmutableTextDecoration(TextDecorationLocation location, ImmutablePen pen,
+            TextDecorationUnit penThicknessUnit,
+            double penOffset, TextDecorationUnit penOffsetUnit)
+        {
+            Location = location;
+            Pen = pen;
+            PenThicknessUnit = penThicknessUnit;
+            PenOffset = penOffset;
+            PenOffsetUnit = penOffsetUnit;
+        }
+
+        /// <summary>
+        /// Gets or sets the location.
+        /// </summary>
+        /// <value>
+        /// The location.
+        /// </value>
+        public TextDecorationLocation Location { get; }
+
+        /// <summary>
+        /// Gets or sets the pen.
+        /// </summary>
+        /// <value>
+        /// The pen.
+        /// </value>
+        public ImmutablePen Pen { get; }
+
+        /// <summary>
+        /// Gets the units in which the Thickness of the text decoration's <see cref="Pen"/> is expressed.
+        /// </summary>
+        public TextDecorationUnit PenThicknessUnit { get; }
+
+        /// <summary>
+        /// Gets or sets the pen offset.
+        /// </summary>
+        /// <value>
+        /// The pen offset.
+        /// </value>
+        public double PenOffset { get; }
+
+        /// <summary>
+        /// Gets the units in which the <see cref="PenOffset"/> value is expressed.
+        /// </summary>
+        public TextDecorationUnit PenOffsetUnit { get; }
+    }
+}

+ 106 - 0
src/Avalonia.Visuals/Media/TextDecoration.cs

@@ -0,0 +1,106 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Media.Immutable;
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Represents a text decoration, which is a visual ornamentation that is added to text (such as an underline).
+    /// </summary>
+    public class TextDecoration : AvaloniaObject
+    {
+        /// <summary>
+        /// Defines the <see cref="Location"/> property.
+        /// </summary>
+        public static readonly StyledProperty<TextDecorationLocation> LocationProperty =
+            AvaloniaProperty.Register<TextDecoration, TextDecorationLocation>(nameof(Location));
+
+        /// <summary>
+        /// Defines the <see cref="Pen"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IPen> PenProperty =
+            AvaloniaProperty.Register<TextDecoration, IPen>(nameof(Pen));
+
+        /// <summary>
+        /// Defines the <see cref="PenThicknessUnit"/> property.
+        /// </summary>
+        public static readonly StyledProperty<TextDecorationUnit> PenThicknessUnitProperty =
+            AvaloniaProperty.Register<TextDecoration, TextDecorationUnit>(nameof(PenThicknessUnit));
+
+        /// <summary>
+        /// Defines the <see cref="PenOffset"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> PenOffsetProperty =
+            AvaloniaProperty.Register<TextDecoration, double>(nameof(PenOffset));
+
+        /// <summary>
+        /// Defines the <see cref="PenOffsetUnit"/> property.
+        /// </summary>
+        public static readonly StyledProperty<TextDecorationUnit> PenOffsetUnitProperty =
+            AvaloniaProperty.Register<TextDecoration, TextDecorationUnit>(nameof(PenOffsetUnit));
+
+        /// <summary>
+        /// Gets or sets the location.
+        /// </summary>
+        /// <value>
+        /// The location.
+        /// </value>
+        public TextDecorationLocation Location
+        {
+            get => GetValue(LocationProperty);
+            set => SetValue(LocationProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the pen.
+        /// </summary>
+        /// <value>
+        ///     The pen.
+        /// </value>
+        public IPen Pen
+        {
+            get => GetValue(PenProperty);
+            set => SetValue(PenProperty, value);
+        }
+
+        /// <summary>
+        /// Gets the units in which the Thickness of the text decoration's <see cref="Pen"/> is expressed.
+        /// </summary>
+        public TextDecorationUnit PenThicknessUnit
+        {
+            get => GetValue(PenThicknessUnitProperty);
+            set => SetValue(PenThicknessUnitProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the pen offset.
+        /// </summary>
+        /// <value>
+        /// The pen offset.
+        /// </value>
+        public double PenOffset
+        {
+            get => GetValue(PenOffsetProperty);
+            set => SetValue(PenOffsetProperty, value);
+        }
+
+        /// <summary>
+        /// Gets the units in which the <see cref="PenOffset"/> value is expressed.
+        /// </summary>
+        public TextDecorationUnit PenOffsetUnit
+        {
+            get => GetValue(PenOffsetUnitProperty);
+            set => SetValue(PenOffsetUnitProperty, value);
+        }
+
+        /// <summary>
+        /// Creates an immutable clone of the <see cref="TextDecoration"/>.
+        /// </summary>
+        /// <returns>The immutable clone.</returns>
+        public ImmutableTextDecoration ToImmutable()
+        {
+            return new ImmutableTextDecoration(Location, Pen?.ToImmutable(), PenThicknessUnit, PenOffset, PenOffsetUnit);
+        }
+    }
+}

+ 82 - 0
src/Avalonia.Visuals/Media/TextDecorationCollection.cs

@@ -0,0 +1,82 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using Avalonia.Collections;
+using Avalonia.Media.Immutable;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// A collection that holds <see cref="TextDecoration"/> objects.
+    /// </summary>
+    public class TextDecorationCollection : AvaloniaList<TextDecoration>
+    {
+        /// <summary>
+        /// Creates an immutable clone of the <see cref="TextDecorationCollection"/>.
+        /// </summary>
+        /// <returns>The immutable clone.</returns>
+        public ImmutableTextDecoration[] ToImmutable()
+        {
+            var immutable = new ImmutableTextDecoration[Count];
+
+            for (var i = 0; i < Count; i++)
+            {
+                immutable[i] = this[i].ToImmutable();
+            }
+
+            return immutable;
+        }
+
+        /// <summary>
+        /// Parses a <see cref="TextDecorationCollection"/> string.
+        /// </summary>
+        /// <param name="s">The string.</param>
+        /// <returns>The <see cref="TextDecorationCollection"/>.</returns>
+        public static TextDecorationCollection Parse(string s)
+        {
+            var locations = new List<TextDecorationLocation>();
+
+            using (var tokenizer = new StringTokenizer(s, ',', "Invalid text decoration."))
+            {
+                while (tokenizer.TryReadString(out var name))
+                {
+                    var location = GetTextDecorationLocation(name);
+
+                    if (locations.Contains(location))
+                    {
+                        throw new ArgumentException("Text decoration already specified.", nameof(s));
+                    }
+
+                    locations.Add(location);
+                }
+            }
+
+            var textDecorations = new TextDecorationCollection();
+
+            foreach (var textDecorationLocation in locations)
+            {
+                textDecorations.Add(new TextDecoration { Location = textDecorationLocation });
+            }
+
+            return textDecorations;
+        }
+
+        /// <summary>
+        /// Parses a <see cref="TextDecorationLocation"/> string.
+        /// </summary>
+        /// <param name="s">The string.</param>
+        /// <returns>The <see cref="TextDecorationLocation"/>.</returns>
+        private static TextDecorationLocation GetTextDecorationLocation(string s)
+        {
+            if (Enum.TryParse<TextDecorationLocation>(s,true, out var location))
+            {
+                return location;
+            }
+
+            throw new ArgumentException("Could not parse text decoration.", nameof(s));
+        }
+    }
+}

+ 31 - 0
src/Avalonia.Visuals/Media/TextDecorationLocation.cs

@@ -0,0 +1,31 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Specifies the vertical position of a <see cref="TextDecoration"/> object.
+    /// </summary>
+    public enum TextDecorationLocation
+    {
+        /// <summary>
+        /// The underline position.
+        /// </summary>
+        Underline = 0,
+
+        /// <summary>
+        /// The over line position.
+        /// </summary>
+        Overline = 1,
+
+        /// <summary>
+        /// The strikethrough position.
+        /// </summary>
+        Strikethrough = 2,
+
+        /// <summary>
+        /// The baseline position.
+        /// </summary>
+        Baseline = 3,
+    }
+}

+ 29 - 0
src/Avalonia.Visuals/Media/TextDecorationUnit.cs

@@ -0,0 +1,29 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Specifies the unit type of either a <see cref="TextDecoration.PenOffset"/> or a <see cref="Pen"/> thickness value.
+    /// </summary>
+    public enum TextDecorationUnit
+    {
+        /// <summary>
+        /// A unit value that is relative to the font used for the <see cref="TextDecoration"/>.
+        /// If the decoration spans multiple fonts, an average recommended value is calculated.
+        /// This is the default value.
+        /// </summary>
+        FontRecommended,
+
+        /// <summary>
+        /// A unit value that is relative to the em size of the font.
+        /// The value of the offset or thickness is equal to the offset or thickness value multiplied by the font em size.
+        /// </summary>
+        FontRenderingEmSize,
+
+        /// <summary>
+        /// A unit value that is expressed in pixels.
+        /// </summary>
+        Pixel
+    }
+}

+ 66 - 0
src/Avalonia.Visuals/Media/TextDecorations.cs

@@ -0,0 +1,66 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Defines a set of commonly used text decorations.
+    /// </summary>
+    public static class TextDecorations
+    {
+        static TextDecorations()
+        {
+            Underline = new TextDecorationCollection
+                        {
+                            new TextDecoration
+                            {
+                                Location = TextDecorationLocation.Underline
+                            }
+                        };
+
+            Strikethrough = new TextDecorationCollection
+                            {
+                                new TextDecoration
+                                {
+                                    Location = TextDecorationLocation.Strikethrough
+                                }
+                            };
+
+            Overline = new TextDecorationCollection
+                       {
+                           new TextDecoration
+                           {
+                               Location = TextDecorationLocation.Overline
+                           }
+                       };
+
+            Baseline = new TextDecorationCollection
+                       {
+                           new TextDecoration
+                           {
+                               Location = TextDecorationLocation.Baseline
+                           }
+                       };
+        }
+
+        /// <summary>
+        /// Gets a <see cref="TextDecorationCollection"/> containing an underline.
+        /// </summary>
+        public static TextDecorationCollection Underline { get; }
+
+        /// <summary>
+        /// Gets a <see cref="TextDecorationCollection"/> containing a strikethrough.
+        /// </summary>
+        public static TextDecorationCollection Strikethrough { get; }
+
+        /// <summary>
+        /// Gets a <see cref="TextDecorationCollection"/> containing an overline.
+        /// </summary>
+        public static TextDecorationCollection Overline { get; }
+
+        /// <summary>
+        /// Gets a <see cref="TextDecorationCollection"/> containing a baseline.
+        /// </summary>
+        public static TextDecorationCollection Baseline { get; }
+    }
+}

+ 22 - 0
src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs

@@ -0,0 +1,22 @@
+using Avalonia.Platform;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// A text run that supports drawing content.
+    /// </summary>
+    public abstract class DrawableTextRun : TextRun
+    {
+        /// <summary>
+        /// Gets the bounds.
+        /// </summary>
+        public abstract Rect Bounds { get; }
+
+        /// <summary>
+        /// Draws the <see cref="DrawableTextRun"/> at the given origin.
+        /// </summary>
+        /// <param name="drawingContext">The drawing context.</param>
+        /// <param name="origin">The origin.</param>
+        public abstract void Draw(IDrawingContextImpl drawingContext, Point origin);
+    }
+}

+ 74 - 0
src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs

@@ -0,0 +1,74 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// A metric that holds information about font specific measurements.
+    /// </summary>
+    public readonly struct FontMetrics
+    {
+        public FontMetrics(Typeface typeface, double fontSize)
+        {
+            var glyphTypeface = typeface.GlyphTypeface;
+
+            var scale = fontSize / glyphTypeface.DesignEmHeight;
+
+            Ascent = glyphTypeface.Ascent * scale;
+
+            Descent = glyphTypeface.Descent * scale;
+
+            LineGap = glyphTypeface.LineGap * scale;
+
+            LineHeight = Descent - Ascent + LineGap;
+
+            UnderlineThickness = glyphTypeface.UnderlineThickness * scale;
+
+            UnderlinePosition = glyphTypeface.UnderlinePosition * scale;
+
+            StrikethroughThickness = glyphTypeface.StrikethroughThickness * scale;
+
+            StrikethroughPosition = glyphTypeface.StrikethroughPosition * scale;
+        }
+
+        /// <summary>
+        /// Gets the recommended distance above the baseline.
+        /// </summary>
+        public double Ascent { get; }
+
+        /// <summary>
+        /// Gets the recommended distance under the baseline.
+        /// </summary>
+        public double Descent { get; }
+
+        /// <summary>
+        /// Gets the recommended additional space between two lines of text.
+        /// </summary>
+        public double LineGap { get; }
+
+        /// <summary>
+        /// Gets the estimated line height.
+        /// </summary>
+        public double LineHeight { get; }
+
+        /// <summary>
+        /// Gets a value that indicates the thickness of the underline.
+        /// </summary>
+        public double UnderlineThickness { get; }
+
+        /// <summary>
+        /// Gets a value that indicates the distance of the underline from the baseline.
+        /// </summary>
+        public double UnderlinePosition { get; }
+
+        /// <summary>
+        /// Gets a value that indicates the thickness of the underline.
+        /// </summary>
+        public double StrikethroughThickness { get; }
+
+        /// <summary>
+        /// Gets a value that indicates the distance of the strikethrough from the baseline.
+        /// </summary>
+        public double StrikethroughPosition { get; }
+    }
+}

+ 15 - 0
src/Avalonia.Visuals/Media/TextFormatting/ITextSource.cs

@@ -0,0 +1,15 @@
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// Produces <see cref="TextRun"/> objects that are used by the <see cref="TextFormatter"/>.
+    /// </summary>
+    public interface ITextSource
+    {
+        /// <summary>
+        /// Gets a <see cref="TextRun"/> for specified text source index.
+        /// </summary>
+        /// <param name="textSourceIndex">The text source index.</param>
+        /// <returns>The text run.</returns>
+        TextRun GetTextRun(int textSourceIndex);
+    }
+}

+ 212 - 0
src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs

@@ -0,0 +1,212 @@
+using Avalonia.Media.Immutable;
+using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Platform;
+using Avalonia.Utility;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// A text run that holds a shaped glyph run.
+    /// </summary>
+    public sealed class ShapedTextRun : DrawableTextRun
+    {
+        public ShapedTextRun(ReadOnlySlice<char> text, TextStyle style) : this(
+            TextShaper.Current.ShapeText(text, style.TextFormat), style)
+        {
+        }
+
+        public ShapedTextRun(GlyphRun glyphRun, TextStyle style)
+        {
+            Text = glyphRun.Characters;
+            Style = style;
+            GlyphRun = glyphRun;
+        }
+
+        /// <inheritdoc/>
+        public override Rect Bounds => GlyphRun.Bounds;
+
+        /// <summary>
+        /// Gets the glyph run.
+        /// </summary>
+        /// <value>
+        /// The glyphs.
+        /// </value>
+        public GlyphRun GlyphRun { get; }
+
+        /// <inheritdoc/>
+        public override void Draw(IDrawingContextImpl drawingContext, Point origin)
+        {
+            if (GlyphRun.GlyphIndices.Length == 0)
+            {
+                return;
+            }
+
+            if (Style.TextFormat.Typeface == null)
+            {
+                return;
+            }
+
+            if (Style.Foreground == null)
+            {
+                return;
+            }
+
+            drawingContext.DrawGlyphRun(Style.Foreground, GlyphRun, origin);
+
+            if (Style.TextDecorations == null)
+            {
+                return;
+            }
+
+            foreach (var textDecoration in Style.TextDecorations)
+            {
+                DrawTextDecoration(drawingContext, textDecoration, origin);
+            }
+        }
+
+        /// <summary>
+        /// Draws the <see cref="TextDecoration"/> at given origin.
+        /// </summary>
+        /// <param name="drawingContext">The drawing context.</param>
+        /// <param name="textDecoration">The text decoration.</param>
+        /// <param name="origin">The origin.</param>
+        private void DrawTextDecoration(IDrawingContextImpl drawingContext, ImmutableTextDecoration textDecoration, Point origin)
+        {
+            var textFormat = Style.TextFormat;
+
+            var fontMetrics = Style.TextFormat.FontMetrics;
+
+            var thickness = textDecoration.Pen?.Thickness ?? 1.0;
+
+            switch (textDecoration.PenThicknessUnit)
+            {
+                case TextDecorationUnit.FontRecommended:
+                    switch (textDecoration.Location)
+                    {
+                        case TextDecorationLocation.Underline:
+                            thickness = fontMetrics.UnderlineThickness;
+                            break;
+                        case TextDecorationLocation.Strikethrough:
+                            thickness = fontMetrics.StrikethroughThickness;
+                            break;
+                    }
+                    break;
+                case TextDecorationUnit.FontRenderingEmSize:
+                    thickness = textFormat.FontRenderingEmSize * thickness;
+                    break;
+            }
+
+            switch (textDecoration.Location)
+            {
+                case TextDecorationLocation.Overline:
+                    origin += new Point(0, textFormat.FontMetrics.Ascent);
+                    break;
+                case TextDecorationLocation.Strikethrough:
+                    origin += new Point(0, -textFormat.FontMetrics.StrikethroughPosition);
+                    break;
+                case TextDecorationLocation.Underline:
+                    origin += new Point(0, -textFormat.FontMetrics.UnderlinePosition);
+                    break;
+            }
+
+            switch (textDecoration.PenOffsetUnit)
+            {
+                case TextDecorationUnit.FontRenderingEmSize:
+                    origin += new Point(0, textDecoration.PenOffset * textFormat.FontRenderingEmSize);
+                    break;
+                case TextDecorationUnit.Pixel:
+                    origin += new Point(0, textDecoration.PenOffset);
+                    break;
+            }
+
+            var pen = new ImmutablePen(
+                textDecoration.Pen?.Brush ?? Style.Foreground.ToImmutable(),
+                thickness,
+                textDecoration.Pen?.DashStyle?.ToImmutable(),
+                textDecoration.Pen?.LineCap ?? default,
+                textDecoration.Pen?.LineJoin ?? PenLineJoin.Miter,
+                textDecoration.Pen?.MiterLimit ?? 10.0);
+
+            drawingContext.DrawLine(pen, origin, origin + new Point(GlyphRun.Bounds.Width, 0));
+        }
+
+        /// <summary>
+        /// Splits the <see cref="TextRun"/> at specified length.
+        /// </summary>
+        /// <param name="length">The length.</param>
+        /// <returns>The split result.</returns>
+        public SplitTextCharactersResult Split(int length)
+        {
+            var glyphCount = 0;
+
+            var firstCharacters = GlyphRun.Characters.Take(length);
+
+            var codepointEnumerator = new CodepointEnumerator(firstCharacters);
+
+            while (codepointEnumerator.MoveNext())
+            {
+                glyphCount++;
+            }
+
+            if (GlyphRun.Characters.Length == length)
+            {
+                return new SplitTextCharactersResult(this, null);
+            }
+
+            if (GlyphRun.GlyphIndices.Length == glyphCount)
+            {
+                return new SplitTextCharactersResult(this, null);
+            }
+
+            var firstGlyphRun = new GlyphRun(
+                Style.TextFormat.Typeface.GlyphTypeface,
+                Style.TextFormat.FontRenderingEmSize,
+                GlyphRun.GlyphIndices.Take(glyphCount),
+                GlyphRun.GlyphAdvances.Take(glyphCount),
+                GlyphRun.GlyphOffsets.Take(glyphCount),
+                GlyphRun.Characters.Take(length),
+                GlyphRun.GlyphClusters.Take(length));
+
+            var firstTextRun = new ShapedTextRun(firstGlyphRun, Style);
+
+            var secondGlyphRun = new GlyphRun(
+                Style.TextFormat.Typeface.GlyphTypeface,
+                Style.TextFormat.FontRenderingEmSize,
+                GlyphRun.GlyphIndices.Skip(glyphCount),
+                GlyphRun.GlyphAdvances.Skip(glyphCount),
+                GlyphRun.GlyphOffsets.Skip(glyphCount),
+                GlyphRun.Characters.Skip(length),
+                GlyphRun.GlyphClusters.Skip(length));
+
+            var secondTextRun = new ShapedTextRun(secondGlyphRun, Style);
+
+            return new SplitTextCharactersResult(firstTextRun, secondTextRun);
+        }
+
+        public readonly struct SplitTextCharactersResult
+        {
+            public SplitTextCharactersResult(ShapedTextRun first, ShapedTextRun second)
+            {
+                First = first;
+
+                Second = second;
+            }
+
+            /// <summary>
+            /// Gets the first text run.
+            /// </summary>
+            /// <value>
+            /// The first text run.
+            /// </value>
+            public ShapedTextRun First { get; }
+
+            /// <summary>
+            /// Gets the second text run.
+            /// </summary>
+            /// <value>
+            /// The second text run.
+            /// </value>
+            public ShapedTextRun Second { get; }
+        }
+    }
+}

+ 417 - 0
src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs

@@ -0,0 +1,417 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Platform;
+using Avalonia.Utility;
+
+namespace Avalonia.Media.TextFormatting
+{
+    internal class SimpleTextFormatter : TextFormatter
+    {
+        private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
+
+        /// <summary>
+        /// Formats a text line.
+        /// </summary>
+        /// <param name="textSource">The text source.</param>
+        /// <param name="firstTextSourceIndex">The first character index to start the text line from.</param>
+        /// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
+        /// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
+        /// such as TextWrapping, TextAlignment, or TextStyle.</param>
+        /// <returns>The formatted line.</returns>
+        public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
+            TextParagraphProperties paragraphProperties)
+        {
+            var textTrimming = paragraphProperties.TextTrimming;
+            var textWrapping = paragraphProperties.TextWrapping;
+            TextLine textLine;
+
+            var textRuns = FormatTextRuns(textSource, firstTextSourceIndex, out var textPointer);
+
+            if (textTrimming != TextTrimming.None)
+            {
+                textLine = PerformTextTrimming(textPointer, textRuns, paragraphWidth, paragraphProperties);
+            }
+            else
+            {
+                if (textWrapping == TextWrapping.Wrap)
+                {
+                    textLine = PerformTextWrapping(textPointer, textRuns, paragraphWidth, paragraphProperties);
+                }
+                else
+                {
+                    var textLineMetrics =
+                        TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment);
+
+                    textLine = new SimpleTextLine(textPointer, textRuns, textLineMetrics);
+                }
+            }
+
+            return textLine;
+        }
+
+        /// <summary>
+        /// Formats text runs with optional text style overrides.
+        /// </summary>
+        /// <param name="textSource">The text source.</param>
+        /// <param name="firstTextSourceIndex">The first text source index.</param>
+        /// <param name="textPointer">The text pointer that covers the formatted text runs.</param>
+        /// <returns>
+        /// The formatted text runs.
+        /// </returns>
+        private List<ShapedTextRun> FormatTextRuns(ITextSource textSource, int firstTextSourceIndex, out TextPointer textPointer)
+        {
+            var start = firstTextSourceIndex;
+
+            var textRuns = new List<ShapedTextRun>();
+
+            while (true)
+            {
+                var textRun = textSource.GetTextRun(firstTextSourceIndex);
+
+                if (textRun.Text.IsEmpty)
+                {
+                    break;
+                }
+
+                if (textRun is TextEndOfLine)
+                {
+                    break;
+                }
+
+                if (!(textRun is TextCharacters))
+                {
+                    throw new NotSupportedException("Run type not supported by the formatter.");
+                }
+
+                var runText = textRun.Text;
+
+                while (!runText.IsEmpty)
+                {
+                    var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style);
+
+                    var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length),
+                        shapableTextStyleRun.Style);
+
+                    textRuns.Add(shapedRun);
+
+                    runText = runText.Skip(shapedRun.Text.Length);
+                }
+
+                firstTextSourceIndex += textRun.Text.Length;
+            }
+
+            textPointer = new TextPointer(start, firstTextSourceIndex - start);
+
+            return textRuns;
+        }
+
+        /// <summary>
+        /// Performs text trimming and returns a trimmed line.
+        /// </summary>
+        /// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
+        /// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
+        /// such as TextWrapping, TextAlignment, or TextStyle.</param>
+        /// <param name="textRuns">The text runs to perform the trimming on.</param>
+        /// <param name="text">The text that was used to construct the text runs.</param>
+        /// <returns></returns>
+        private TextLine PerformTextTrimming(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
+            double paragraphWidth, TextParagraphProperties paragraphProperties)
+        {
+            var textTrimming = paragraphProperties.TextTrimming;
+            var availableWidth = paragraphWidth;
+            var currentWidth = 0.0;
+            var runIndex = 0;
+
+            while (runIndex < textRuns.Count)
+            {
+                var currentRun = textRuns[runIndex];
+
+                currentWidth += currentRun.GlyphRun.Bounds.Width;
+
+                if (currentWidth > availableWidth)
+                {
+                    var ellipsisRun = CreateEllipsisRun(currentRun.Style);
+
+                    var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width);
+
+                    if (textTrimming == TextTrimming.WordEllipsis)
+                    {
+                        if (measuredLength < text.End)
+                        {
+                            var currentBreakPosition = 0;
+
+                            var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+
+                            while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
+                            {
+                                var nextBreakPosition = lineBreaker.Current.PositionWrap;
+
+                                if (nextBreakPosition == 0)
+                                {
+                                    break;
+                                }
+
+                                if (nextBreakPosition > measuredLength)
+                                {
+                                    break;
+                                }
+
+                                currentBreakPosition = nextBreakPosition;
+                            }
+
+                            measuredLength = currentBreakPosition;
+                        }
+                    }
+
+                    var splitResult = SplitTextRuns(textRuns, measuredLength);
+
+                    var trimmedRuns = new List<ShapedTextRun>(splitResult.First.Count + 1);
+
+                    trimmedRuns.AddRange(splitResult.First);
+
+                    trimmedRuns.Add(ellipsisRun);
+
+                    var textLineMetrics =
+                        TextLineMetrics.Create(trimmedRuns, paragraphWidth, paragraphProperties.TextAlignment);
+
+                    return new SimpleTextLine(text.Take(measuredLength), trimmedRuns, textLineMetrics);
+                }
+
+                availableWidth -= currentRun.GlyphRun.Bounds.Width;
+
+                runIndex++;
+            }
+
+            return new SimpleTextLine(text, textRuns,
+                TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment));
+        }
+
+        /// <summary>
+        /// Performs text wrapping returns a list of text lines.
+        /// </summary>
+        /// <param name="paragraphProperties">The text paragraph properties.</param>
+        /// <param name="textRuns">The text run'S.</param>
+        /// <param name="text">The text to analyze for break opportunities.</param>
+        /// <param name="paragraphWidth"></param>
+        /// <returns></returns>
+        private TextLine PerformTextWrapping(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
+            double paragraphWidth, TextParagraphProperties paragraphProperties)
+        {
+            var availableWidth = paragraphWidth;
+            var currentWidth = 0.0;
+            var runIndex = 0;
+
+            while (runIndex < textRuns.Count)
+            {
+                var currentRun = textRuns[runIndex];
+
+                currentWidth += currentRun.GlyphRun.Bounds.Width;
+
+                if (currentWidth > availableWidth)
+                {
+                    var measuredLength = MeasureText(currentRun, paragraphWidth);
+
+                    if (measuredLength < text.End)
+                    {
+                        var currentBreakPosition = -1;
+
+                        var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+
+                        while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
+                        {
+                            var nextBreakPosition = lineBreaker.Current.PositionWrap;
+
+                            if (nextBreakPosition == 0)
+                            {
+                                break;
+                            }
+
+                            if (nextBreakPosition > measuredLength)
+                            {
+                                break;
+                            }
+
+                            currentBreakPosition = nextBreakPosition;
+                        }
+
+                        if (currentBreakPosition != -1)
+                        {
+                            measuredLength = currentBreakPosition;
+                        }
+                    }
+
+                    var splitResult = SplitTextRuns(textRuns, measuredLength);
+
+                    var textLineMetrics =
+                        TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment);
+
+                    return new SimpleTextLine(text.Take(measuredLength), splitResult.First, textLineMetrics);
+                }
+
+                availableWidth -= currentRun.GlyphRun.Bounds.Width;
+
+                runIndex++;
+            }
+
+            return new SimpleTextLine(text, textRuns,
+                TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment));
+        }
+
+        /// <summary>
+        /// Measures the number of characters that fits into available width.
+        /// </summary>
+        /// <param name="textRun">The text run.</param>
+        /// <param name="availableWidth">The available width.</param>
+        /// <returns></returns>
+        private int MeasureText(ShapedTextRun textRun, double availableWidth)
+        {
+            if (textRun.GlyphRun.Bounds.Width < availableWidth)
+            {
+                return textRun.Text.Length;
+            }
+
+            var measuredWidth = 0.0;
+
+            var index = 0;
+
+            for (; index < textRun.GlyphRun.GlyphAdvances.Length; index++)
+            {
+                var advance = textRun.GlyphRun.GlyphAdvances[index];
+
+                if (measuredWidth + advance > availableWidth)
+                {
+                    break;
+                }
+
+                measuredWidth += advance;
+            }
+
+            var cluster = textRun.GlyphRun.GlyphClusters[index];
+
+            var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _);
+
+            return characterHit.FirstCharacterIndex - textRun.GlyphRun.Characters.Start +
+                   (textRun.GlyphRun.IsLeftToRight ? characterHit.TrailingLength : 0);
+        }
+
+        /// <summary>
+        /// Creates an ellipsis.
+        /// </summary>
+        /// <param name="textStyle">The text style.</param>
+        /// <returns></returns>
+        private static ShapedTextRun CreateEllipsisRun(TextStyle textStyle)
+        {
+            var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
+
+            var glyphRun = formatterImpl.ShapeText(s_ellipsis, textStyle.TextFormat);
+
+            return new ShapedTextRun(glyphRun, textStyle);
+        }
+
+        private readonly struct SplitTextRunsResult
+        {
+            public SplitTextRunsResult(IReadOnlyList<ShapedTextRun> first, IReadOnlyList<ShapedTextRun> second)
+            {
+                First = first;
+
+                Second = second;
+            }
+
+            /// <summary>
+            /// Gets the first text runs.
+            /// </summary>
+            /// <value>
+            /// The first text runs.
+            /// </value>
+            public IReadOnlyList<ShapedTextRun> First { get; }
+
+            /// <summary>
+            /// Gets the second text runs.
+            /// </summary>
+            /// <value>
+            /// The second text runs.
+            /// </value>
+            public IReadOnlyList<ShapedTextRun> Second { get; }
+        }
+
+        /// <summary>
+        /// Split a sequence of runs into two segments at specified length.
+        /// </summary>
+        /// <param name="textRuns">The text run's.</param>
+        /// <param name="length">The length to split at.</param>
+        /// <returns></returns>
+        private static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextRun> textRuns, int length)
+        {
+            var currentLength = 0;
+
+            for (var i = 0; i < textRuns.Count; i++)
+            {
+                var currentRun = textRuns[i];
+
+                if (currentLength + currentRun.GlyphRun.Characters.Length < length)
+                {
+                    currentLength += currentRun.GlyphRun.Characters.Length;
+                    continue;
+                }
+
+                var firstCount = currentRun.GlyphRun.Characters.Length > 1 ? i + 1 : i;
+
+                var first = new ShapedTextRun[firstCount];
+
+                if (firstCount > 1)
+                {
+                    for (var j = 0; j < i; j++)
+                    {
+                        first[j] = textRuns[j];
+                    }
+                }
+
+                var secondCount = textRuns.Count - firstCount;
+
+                if (currentLength + currentRun.GlyphRun.Characters.Length == length)
+                {
+                    var second = new ShapedTextRun[secondCount];
+
+                    var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0;
+
+                    if (secondCount > 0)
+                    {
+                        for (var j = 0; j < secondCount; j++)
+                        {
+                            second[j] = textRuns[i + j + offset];
+                        }
+                    }
+
+                    first[i] = currentRun;
+
+                    return new SplitTextRunsResult(first, second);
+                }
+                else
+                {
+                    secondCount++;
+
+                    var second = new ShapedTextRun[secondCount];
+
+                    if (secondCount > 0)
+                    {
+                        for (var j = 1; j < secondCount; j++)
+                        {
+                            second[j] = textRuns[i + j];
+                        }
+                    }
+
+                    var split = currentRun.Split(length - currentLength);
+
+                    first[i] = split.First;
+
+                    second[0] = split.Second;
+
+                    return new SplitTextRunsResult(first, second);
+                }
+            }
+
+            return new SplitTextRunsResult(textRuns, null);
+        }
+    }
+}

+ 259 - 0
src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs

@@ -0,0 +1,259 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Platform;
+
+namespace Avalonia.Media.TextFormatting
+{
+    internal class SimpleTextLine : TextLine
+    {
+        private readonly IReadOnlyList<ShapedTextRun> _textRuns;
+
+        public SimpleTextLine(TextPointer textPointer, IReadOnlyList<ShapedTextRun> textRuns, TextLineMetrics lineMetrics)
+        {
+            Text = textPointer;
+            _textRuns = textRuns;
+            LineMetrics = lineMetrics;
+        }
+
+        /// <inheritdoc/>
+        public override TextPointer Text { get; }
+
+        /// <inheritdoc/>
+        public override IReadOnlyList<TextRun> TextRuns => _textRuns;
+
+        /// <inheritdoc/>
+        public override TextLineMetrics LineMetrics { get; }
+
+        /// <inheritdoc/>
+        public override void Draw(IDrawingContextImpl drawingContext, Point origin)
+        {
+            var currentX = origin.X;
+
+            foreach (var textRun in _textRuns)
+            {
+                var baselineOrigin = new Point(currentX + LineMetrics.BaselineOrigin.X,
+                    origin.Y + LineMetrics.BaselineOrigin.Y);
+
+                textRun.Draw(drawingContext, baselineOrigin);
+
+                currentX += textRun.Bounds.Width;
+            }
+        }
+
+        /// <inheritdoc/>
+        public override CharacterHit GetCharacterHitFromDistance(double distance)
+        {
+            if (distance < 0)
+            {
+                // hit happens before the line, return the first position
+                return new CharacterHit(Text.Start);
+            }
+
+            // process hit that happens within the line
+            var characterHit = new CharacterHit();
+
+            foreach (var run in _textRuns)
+            {
+                characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+
+                if (distance <= run.Bounds.Width)
+                {
+                    break;
+                }
+
+                distance -= run.Bounds.Width;
+            }
+
+            return characterHit;
+        }
+
+        /// <inheritdoc/>
+        public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
+        {
+            return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0));
+        }
+
+        /// <inheritdoc/>
+        public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
+        {
+            int nextVisibleCp;
+            bool navigableCpFound;
+
+            if (characterHit.TrailingLength == 0)
+            {
+                navigableCpFound = FindNextCodepointIndex(characterHit.FirstCharacterIndex, out nextVisibleCp);
+
+                if (navigableCpFound)
+                {
+                    // Move from leading to trailing edge
+                    return new CharacterHit(nextVisibleCp, 1);
+                }
+            }
+
+            navigableCpFound = FindNextCodepointIndex(characterHit.FirstCharacterIndex + 1, out nextVisibleCp);
+
+            if (navigableCpFound)
+            {
+                // Move from trailing edge of current character to trailing edge of next
+                return new CharacterHit(nextVisibleCp, 1);
+            }
+
+            // Can't move, we're after the last character
+            return characterHit;
+        }
+
+        /// <inheritdoc/>
+        public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
+        {
+            int previousCodepointIndex;
+            bool codepointIndexFound;
+
+            var cpHit = characterHit.FirstCharacterIndex;
+            var trailingHit = characterHit.TrailingLength != 0;
+
+            // Input can be right after the end of the current line. Snap it to be at the end of the line.
+            if (cpHit >= Text.Start + Text.Length)
+            {
+                cpHit = Text.Start + Text.Length - 1;
+
+                trailingHit = true;
+            }
+
+            if (trailingHit)
+            {
+                codepointIndexFound = FindPreviousCodepointIndex(cpHit, out previousCodepointIndex);
+
+                if (codepointIndexFound)
+                {
+                    // Move from trailing to leading edge
+                    return new CharacterHit(previousCodepointIndex, 0);
+                }
+            }
+
+            codepointIndexFound = FindPreviousCodepointIndex(cpHit - 1, out previousCodepointIndex);
+
+            if (codepointIndexFound)
+            {
+                // Move from leading edge of current character to leading edge of previous
+                return new CharacterHit(previousCodepointIndex, 0);
+            }
+
+            // Can't move, we're before the first character
+            return characterHit;
+        }
+
+        /// <inheritdoc/>
+        public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit)
+        {
+            // same operation as move-to-previous
+            return GetPreviousCaretCharacterHit(characterHit);
+        }
+
+        /// <summary>
+        /// Get distance from line start to the specified codepoint index
+        /// </summary>
+        private double DistanceFromCodepointIndex(int codepointIndex)
+        {
+            var currentDistance = 0.0;
+
+            foreach (var textRun in _textRuns)
+            {
+                if (codepointIndex > textRun.Text.End)
+                {
+                    currentDistance += textRun.Bounds.Width;
+
+                    continue;
+                }
+
+                return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(codepointIndex));
+            }
+
+            return currentDistance;
+        }
+
+        /// <summary>
+        /// Search forward from the given codepoint index (inclusive) to find the next navigable codepoint index.
+        /// Return true if one such codepoint index is found, false otherwise.
+        /// </summary>
+        private bool FindNextCodepointIndex(int codepointIndex, out int nextCodepointIndex)
+        {
+            nextCodepointIndex = codepointIndex;
+
+            if (codepointIndex >= Text.Start + Text.Length)
+            {
+                return false; // Cannot go forward anymore
+            }
+
+            GetRunIndexAtCodepointIndex(codepointIndex, out var runIndex, out var cpRunStart);
+
+            while (runIndex < TextRuns.Count)
+            {
+                // When navigating forward, only the trailing edge of visible content is
+                // navigable.
+                if (runIndex < TextRuns.Count)
+                {
+                    nextCodepointIndex = Math.Max(cpRunStart, codepointIndex);
+                    return true;
+                }
+
+                cpRunStart += TextRuns[runIndex++].Text.Length;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Search backward from the given codepoint index (inclusive) to find the previous navigable codepoint index.
+        /// Return true if one such codepoint is found, false otherwise.
+        /// </summary>
+        private bool FindPreviousCodepointIndex(int codepointIndex, out int previousCodepointIndex)
+        {
+            previousCodepointIndex = codepointIndex;
+
+            if (codepointIndex < Text.Start)
+            {
+                return false; // Cannot go backward anymore.
+            }
+
+            // Position the cpRunEnd at the end of the span that contains the given cp
+            GetRunIndexAtCodepointIndex(codepointIndex, out var runIndex, out var codepointIndexAtRunEnd);
+
+            codepointIndexAtRunEnd += TextRuns[runIndex].Text.End;
+
+            while (runIndex >= 0)
+            {
+                // Visible content has caret stops at its leading edge.
+                if (runIndex + 1 < TextRuns.Count)
+                {
+                    previousCodepointIndex = Math.Min(codepointIndexAtRunEnd, codepointIndex);
+                    return true;
+                }
+
+                // Newline sequence has caret stops at its leading edge.
+                if (runIndex == TextRuns.Count)
+                {
+                    // Get the cp index at the beginning of the newline sequence.
+                    previousCodepointIndex = codepointIndexAtRunEnd - TextRuns[runIndex].Text.Length + 1;
+                    return true;
+                }
+
+                codepointIndexAtRunEnd -= TextRuns[runIndex--].Text.Length;
+            }
+
+            return false;
+        }
+
+        private void GetRunIndexAtCodepointIndex(int codepointIndex, out int runIndex, out int codepointIndexAtRunStart)
+        {
+            codepointIndexAtRunStart = Text.Start;
+            runIndex = 0;
+
+            // Find the span that contains the given cp
+            while (runIndex < TextRuns.Count &&
+                   codepointIndexAtRunStart + TextRuns[runIndex].Text.Length <= codepointIndex)
+            {
+                codepointIndexAtRunStart += TextRuns[runIndex++].Text.Length;
+            }
+        }
+    }
+}

+ 21 - 0
src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs

@@ -0,0 +1,21 @@
+using Avalonia.Utility;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// A text run that holds text characters.
+    /// </summary>
+    public class TextCharacters : TextRun
+    {
+        protected TextCharacters()
+        {
+            
+        }
+
+        public TextCharacters(ReadOnlySlice<char> text, TextStyle style)
+        {
+            Text = text;
+            Style = style;
+        }
+    }
+}

+ 9 - 0
src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs

@@ -0,0 +1,9 @@
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// A text run that indicates the end of a line.
+    /// </summary>
+    public class TextEndOfLine : TextRun
+    {
+    }
+}

+ 9 - 0
src/Avalonia.Visuals/Media/TextFormatting/TextEndOfParagraph.cs

@@ -0,0 +1,9 @@
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    ///  A text run that indicates the end of a paragraph.
+    /// </summary>
+    public class TextEndOfParagraph : TextEndOfLine
+    {
+    }
+}

+ 74 - 0
src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs

@@ -0,0 +1,74 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// Unique text formatting properties that are used by the <see cref="TextFormatter"/>.
+    /// </summary>
+    public readonly struct TextFormat : IEquatable<TextFormat>
+    {
+        public TextFormat(Typeface typeface, double fontRenderingEmSize)
+        {
+            Typeface = typeface;
+            FontRenderingEmSize = fontRenderingEmSize;
+            FontMetrics = new FontMetrics(typeface, fontRenderingEmSize);
+        }
+
+        /// <summary>
+        /// Gets the typeface.
+        /// </summary>
+        /// <value>
+        /// The typeface.
+        /// </value>
+        public Typeface Typeface { get; }
+
+        /// <summary>
+        /// Gets the font rendering em size.
+        /// </summary>
+        /// <value>
+        /// The em rendering size of the font.
+        /// </value>
+        public double FontRenderingEmSize { get; }
+
+        /// <summary>
+        /// Gets the font metrics.
+        /// </summary>
+        /// <value>
+        /// The metrics of the font.
+        /// </value> 
+        public FontMetrics FontMetrics { get; }
+
+        public static bool operator ==(TextFormat self, TextFormat other)
+        {
+            return self.Equals(other);
+        }
+
+        public static bool operator !=(TextFormat self, TextFormat other)
+        {
+            return !(self == other);
+        }
+
+        public bool Equals(TextFormat other)
+        {
+            return Typeface.Equals(other.Typeface) && FontRenderingEmSize.Equals(other.FontRenderingEmSize);
+        }
+
+        public override bool Equals(object obj)
+        {
+            return obj is TextFormat other && Equals(other);
+        }
+
+        public override int GetHashCode()
+        {
+            unchecked
+            {
+                var hashCode = (Typeface != null ? Typeface.GetHashCode() : 0);
+                hashCode = (hashCode * 397) ^ FontRenderingEmSize.GetHashCode();
+                return hashCode;
+            }
+        }
+    }
+}

+ 186 - 0
src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs

@@ -0,0 +1,186 @@
+using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Utility;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// Represents a base class for text formatting.
+    /// </summary>
+    public abstract class TextFormatter
+    {
+        /// <summary>
+        /// Gets the current <see cref="TextFormatter"/> that is used for non complex text formatting.
+        /// </summary>
+        public static TextFormatter Current
+        {
+            get
+            {
+                var current = AvaloniaLocator.Current.GetService<TextFormatter>();
+
+                if (current != null)
+                {
+                    return current;
+                }
+
+                current = new SimpleTextFormatter();
+
+                AvaloniaLocator.CurrentMutable.Bind<TextFormatter>().ToConstant(current);
+
+                return current;
+            }
+        }
+
+        /// <summary>
+        /// Formats a text line.
+        /// </summary>
+        /// <param name="textSource">The text source.</param>
+        /// <param name="firstTextSourceIndex">The first character index to start the text line from.</param>
+        /// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
+        /// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
+        /// such as TextWrapping, TextAlignment, or TextStyle.</param>
+        /// <returns>The formatted line.</returns>
+        public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
+            TextParagraphProperties paragraphProperties);
+
+        /// <summary>
+        /// Creates a text style run with unique properties.
+        /// </summary>
+        /// <param name="text">The text to create text runs from.</param>
+        /// <param name="defaultStyle"></param>
+        /// <returns>A list of text runs.</returns>
+        protected TextStyleRun CreateShapableTextStyleRun(ReadOnlySlice<char> text, TextStyle defaultStyle)
+        {
+            var defaultTypeface = defaultStyle.TextFormat.Typeface;
+
+            var currentTypeface = defaultTypeface;
+
+            if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count))
+            {
+                return new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface,
+                    defaultStyle.TextFormat.FontRenderingEmSize,
+                    defaultStyle.Foreground, defaultStyle.TextDecorations));
+
+            }
+
+            var codepoint = Codepoint.ReadAt(text, count, out _);
+
+            //ToDo: Fix FontFamily fallback
+            currentTypeface =
+                FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style);
+
+            if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
+            {
+                //Fallback found
+                return new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface,
+                    defaultStyle.TextFormat.FontRenderingEmSize,
+                    defaultStyle.Foreground, defaultStyle.TextDecorations));
+
+            }
+
+            // no fallback found
+            currentTypeface = defaultTypeface;
+
+            var glyphTypeface = currentTypeface.GlyphTypeface;
+
+            var enumerator = new GraphemeEnumerator(text);
+
+            while (enumerator.MoveNext())
+            {
+                var grapheme = enumerator.Current;
+
+                if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
+                {
+                    break;
+                }
+
+                count += grapheme.Text.Length;
+            }
+
+            return new TextStyleRun(new TextPointer(text.Start, count),
+                new TextStyle(currentTypeface, defaultStyle.TextFormat.FontRenderingEmSize,
+                    defaultStyle.Foreground, defaultStyle.TextDecorations));
+        }
+
+        /// <summary>
+        /// Tries to get run properties.
+        /// </summary>
+        /// <param name="defaultTypeface"></param>
+        /// <param name="text"></param>
+        /// <param name="typeface">The typeface that is used to find matching characters.</param>
+        /// <param name="count"></param>
+        /// <returns></returns>
+        protected bool TryGetRunProperties(ReadOnlySlice<char> text, Typeface typeface, Typeface defaultTypeface,
+            out int count)
+        {
+            if (text.Length == 0)
+            {
+                count = 0;
+                return false;
+            }
+
+            var isFallback = typeface != defaultTypeface;
+
+            count = 0;
+            var script = Script.Common;
+            //var direction = BiDiClass.LeftToRight;
+
+            var font = typeface.GlyphTypeface;
+            var defaultFont = defaultTypeface.GlyphTypeface;
+
+            var enumerator = new GraphemeEnumerator(text);
+
+            while (enumerator.MoveNext())
+            {
+                var grapheme = enumerator.Current;
+
+                var currentScript = grapheme.FirstCodepoint.Script;
+
+                //var currentDirection = grapheme.FirstCodepoint.BiDiClass;
+
+                //// ToDo: Implement BiDi algorithm
+                //if (currentScript.HorizontalDirection != direction)
+                //{
+                //    if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint))
+                //    {
+                //        break;
+                //    }
+                //}
+
+                if (currentScript != script)
+                {
+                    if (currentScript != Script.Inherited && currentScript != Script.Common)
+                    {
+                        if (script == Script.Inherited || script == Script.Common)
+                        {
+                            script = currentScript;
+                        }
+                        else
+                        {
+                            break;
+                        }
+                    }
+                }
+
+                if (isFallback)
+                {
+                    if (defaultFont.TryGetGlyph(grapheme.FirstCodepoint, out _))
+                    {
+                        break;
+                    }
+                }
+
+                if (!font.TryGetGlyph(grapheme.FirstCodepoint, out _))
+                {
+                    if (!grapheme.FirstCodepoint.IsWhiteSpace)
+                    {
+                        break;
+                    }
+                }
+
+                count += grapheme.Text.Length;
+            }
+
+            return count > 0;
+        }
+    }
+}

+ 387 - 0
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@@ -0,0 +1,387 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Media.Immutable;
+using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Platform;
+using Avalonia.Utility;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// Represents a multi line text layout.
+    /// </summary>
+    public class TextLayout
+    {
+        private static readonly ReadOnlySlice<char> s_empty = new ReadOnlySlice<char>(new[] { '\u200B' });
+
+        private readonly ReadOnlySlice<char> _text;
+        private readonly TextParagraphProperties _paragraphProperties;
+        private readonly IReadOnlyList<TextStyleRun> _textStyleOverrides;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TextLayout" /> class.
+        /// </summary>
+        /// <param name="text">The text.</param>
+        /// <param name="typeface">The typeface.</param>
+        /// <param name="fontSize">Size of the font.</param>
+        /// <param name="foreground">The foreground.</param>
+        /// <param name="textAlignment">The text alignment.</param>
+        /// <param name="textWrapping">The text wrapping.</param>
+        /// <param name="textTrimming">The text trimming.</param>
+        /// <param name="textDecorations">The text decorations.</param>
+        /// <param name="maxWidth">The maximum width.</param>
+        /// <param name="maxHeight">The maximum height.</param>
+        /// <param name="textStyleOverrides">The text style overrides.</param>
+        public TextLayout(
+            string text,
+            Typeface typeface,
+            double fontSize,
+            IBrush foreground,
+            TextAlignment textAlignment = TextAlignment.Left,
+            TextWrapping textWrapping = TextWrapping.NoWrap,
+            TextTrimming textTrimming = TextTrimming.None,
+            TextDecorationCollection textDecorations = null,
+            double maxWidth = double.PositiveInfinity,
+            double maxHeight = double.PositiveInfinity,
+            IReadOnlyList<TextStyleRun> textStyleOverrides = null)
+        {
+            _text = string.IsNullOrEmpty(text) ?
+                new ReadOnlySlice<char>() :
+                new ReadOnlySlice<char>(text.AsMemory());
+
+            _paragraphProperties =
+                CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textTrimming, textDecorations?.ToImmutable());
+
+            _textStyleOverrides = textStyleOverrides;
+
+            MaxWidth = maxWidth;
+
+            MaxHeight = maxHeight;
+
+            UpdateLayout();
+        }
+
+        /// <summary>
+        /// Gets the maximum width.
+        /// </summary>
+        public double MaxWidth { get; }
+
+
+        /// <summary>
+        /// Gets the maximum height.
+        /// </summary>
+        public double MaxHeight { get; }
+
+        /// <summary>
+        /// Gets the text lines.
+        /// </summary>
+        /// <value>
+        /// The text lines.
+        /// </value>
+        public IReadOnlyList<TextLine> TextLines { get; private set; }
+
+        /// <summary>
+        /// Gets the bounds of the layout.
+        /// </summary>
+        /// <value>
+        /// The bounds.
+        /// </value>
+        public Rect Bounds { get; private set; }
+
+        /// <summary>
+        /// Draws the text layout.
+        /// </summary>
+        /// <param name="context">The drawing context.</param>
+        /// <param name="origin">The origin.</param>
+        public void Draw(IDrawingContextImpl context, Point origin)
+        {
+            if (!TextLines.Any())
+            {
+                return;
+            }
+
+            var currentY = origin.Y;
+
+            foreach (var textLine in TextLines)
+            {
+                textLine.Draw(context, new Point(origin.X, currentY));
+
+                currentY += textLine.LineMetrics.Size.Height;
+            }
+        }
+
+        /// <summary>
+        /// Creates the default <see cref="TextParagraphProperties"/> that are used by the <see cref="TextFormatter"/>.
+        /// </summary>
+        /// <param name="typeface">The typeface.</param>
+        /// <param name="fontSize">The font size.</param>
+        /// <param name="foreground">The foreground.</param>
+        /// <param name="textAlignment">The text alignment.</param>
+        /// <param name="textWrapping">The text wrapping.</param>
+        /// <param name="textTrimming">The text trimming.</param>
+        /// <param name="textDecorations">The text decorations.</param>
+        /// <returns></returns>
+        private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
+            IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextTrimming textTrimming,
+            ImmutableTextDecoration[] textDecorations)
+        {
+            var textRunStyle = new TextStyle(typeface, fontSize, foreground, textDecorations);
+
+            return new TextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming);
+        }
+
+        /// <summary>
+        /// Updates the current bounds.
+        /// </summary>
+        /// <param name="textLine">The text line.</param>
+        /// <param name="left">The left.</param>
+        /// <param name="right">The right.</param>
+        /// <param name="bottom">The bottom.</param>
+        private static void UpdateBounds(TextLine textLine, ref double left, ref double right, ref double bottom)
+        {
+            if (right < textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width)
+            {
+                right = textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width;
+            }
+
+            if (left < textLine.LineMetrics.BaselineOrigin.X)
+            {
+                left = textLine.LineMetrics.BaselineOrigin.X;
+            }
+
+            bottom += textLine.LineMetrics.Size.Height;
+        }
+
+        /// <summary>
+        /// Creates an empty text line.
+        /// </summary>
+        /// <returns>The empty text line.</returns>
+        private TextLine CreateEmptyTextLine(int startingIndex)
+        {
+            var textFormat = _paragraphProperties.DefaultTextStyle.TextFormat;
+
+            var glyphRun = TextShaper.Current.ShapeText(s_empty, textFormat);
+
+            var textRuns = new[] { new ShapedTextRun(glyphRun, _paragraphProperties.DefaultTextStyle) };
+
+            return new SimpleTextLine(new TextPointer(startingIndex, 0), textRuns,
+                TextLineMetrics.Create(textRuns, MaxWidth, _paragraphProperties.TextAlignment));
+        }
+
+        /// <summary>
+        /// Updates the layout and applies specified text style overrides.
+        /// </summary>
+        private void UpdateLayout()
+        {
+            if (_text.IsEmpty || Math.Abs(MaxWidth) < double.Epsilon || Math.Abs(MaxHeight) < double.Epsilon)
+            {
+                var textLine = CreateEmptyTextLine(0);
+
+                TextLines = new List<TextLine> { textLine };
+
+                Bounds = new Rect(textLine.LineMetrics.BaselineOrigin.X, 0, 0, textLine.LineMetrics.Size.Height);
+            }
+            else
+            {
+                var textLines = new List<TextLine>();
+
+                double left = 0.0, right = 0.0, bottom = 0.0;
+
+                var lineBreaker = new LineBreakEnumerator(_text);
+
+                var currentPosition = 0;
+
+                while (currentPosition < _text.Length)
+                {
+                    int length;
+
+                    if (lineBreaker.MoveNext())
+                    {
+                        if (!lineBreaker.Current.Required)
+                        {
+                            continue;
+                        }
+
+                        length = lineBreaker.Current.PositionWrap - currentPosition;
+
+                        if (currentPosition + length < _text.Length)
+                        {
+                            //The line breaker isn't treating \n\r as a pair so we have to fix that here.
+                            if (_text[lineBreaker.Current.PositionMeasure] == '\n'
+                             && _text[lineBreaker.Current.PositionWrap] == '\r')
+                            {
+                                length++;
+                            }
+                        }
+                    }
+                    else
+                    {
+                        length = _text.Length - currentPosition;
+                    }
+
+                    var remainingLength = length;
+
+                    while (remainingLength > 0)
+                    {
+                        var textSlice = _text.AsSlice(currentPosition, remainingLength);
+
+                        var textSource = new FormattedTextSource(textSlice, _paragraphProperties.DefaultTextStyle, _textStyleOverrides);
+
+                        var textLine = TextFormatter.Current.FormatLine(textSource, 0, MaxWidth, _paragraphProperties);
+
+                        UpdateBounds(textLine, ref left, ref right, ref bottom);
+
+                        textLines.Add(textLine);
+
+                        if (_paragraphProperties.TextTrimming != TextTrimming.None)
+                        {
+                            currentPosition += remainingLength;
+
+                            break;
+                        }
+
+                        remainingLength -= textLine.Text.Length;
+
+                        currentPosition += textLine.Text.Length;
+                    }
+
+                    if (lineBreaker.Current.Required && currentPosition == _text.Length)
+                    {
+                        var emptyTextLine = CreateEmptyTextLine(currentPosition);
+
+                        UpdateBounds(emptyTextLine, ref left, ref right, ref bottom);
+
+                        textLines.Add(emptyTextLine);
+
+                        break;
+                    }
+
+                    if (!double.IsPositiveInfinity(MaxHeight) && MaxHeight < Bounds.Height)
+                    {
+                        break;
+                    }
+                }
+
+                Bounds = new Rect(left, 0, right, bottom);
+
+                TextLines = textLines;
+            }
+        }
+
+        private struct FormattedTextSource : ITextSource
+        {
+            private readonly ReadOnlySlice<char> _text;
+            private readonly TextStyle _defaultStyle;
+            private readonly IReadOnlyList<TextStyleRun> _textStyleOverrides;
+
+            public FormattedTextSource(ReadOnlySlice<char> text, TextStyle defaultStyle,
+                IReadOnlyList<TextStyleRun> textStyleOverrides)
+            {
+                _text = text;
+                _defaultStyle = defaultStyle;
+                _textStyleOverrides = textStyleOverrides;
+            }
+
+            public TextRun GetTextRun(int textSourceIndex)
+            {
+                var runText = _text.Skip(textSourceIndex);
+
+                if (runText.IsEmpty)
+                {
+                    return new TextEndOfLine();
+                }
+
+                var textStyleRun = CreateTextStyleRunWithOverride(runText, _defaultStyle, _textStyleOverrides);
+
+                return new TextCharacters(runText.Take(textStyleRun.TextPointer.Length), textStyleRun.Style);
+            }
+
+            /// <summary>
+            /// Creates a text style run that has overrides applied. Only overrides with equal TextStyle.
+            /// If optimizeForShaping is <c>true</c> Foreground is ignored.
+            /// </summary>
+            /// <param name="text">The text to create the run for.</param>
+            /// <param name="defaultTextStyle">The default text style for segments that don't have an override.</param>
+            /// <param name="textStyleOverrides">The text style overrides.</param>
+            /// <returns>
+            /// The created text style run.
+            /// </returns>
+            private static TextStyleRun CreateTextStyleRunWithOverride(ReadOnlySlice<char> text,
+                TextStyle defaultTextStyle, IReadOnlyList<TextStyleRun> textStyleOverrides)
+            {
+                if(textStyleOverrides == null || textStyleOverrides.Count == 0)
+                {
+                    return new TextStyleRun(new TextPointer(text.Start, text.Length), defaultTextStyle);
+                }
+
+                var currentTextStyle = defaultTextStyle;
+
+                var hasOverride = false;
+
+                var i = 0;
+
+                var length = 0;
+
+                for (; i < textStyleOverrides.Count; i++)
+                {
+                    var styleOverride = textStyleOverrides[i];
+
+                    var textPointer = styleOverride.TextPointer;
+
+                    if (textPointer.End < text.Start)
+                    {
+                        continue;
+                    }
+
+                    if (textPointer.Start > text.End)
+                    {
+                        length = text.Length;
+                        break;
+                    }
+
+                    if (textPointer.Start > text.Start)
+                    {
+                        if (styleOverride.Style.TextFormat != currentTextStyle.TextFormat ||
+                            !currentTextStyle.Foreground.Equals(styleOverride.Style.Foreground))
+                        {
+                            length = Math.Min(Math.Abs(textPointer.Start - text.Start), text.Length);
+
+                            break;
+                        }
+                    }
+
+                    length += Math.Min(text.Length - length, textPointer.Length);
+
+                    if (hasOverride)
+                    {
+                        continue;
+                    }
+
+                    hasOverride = true;
+
+                    currentTextStyle = styleOverride.Style;
+                }
+
+                if (length < text.Length && i == textStyleOverrides.Count)
+                {
+                    if (currentTextStyle.Foreground.Equals(defaultTextStyle.Foreground) &&
+                        currentTextStyle.TextFormat == defaultTextStyle.TextFormat)
+                    {
+                        length = text.Length;
+                    }
+                }
+
+                if (length != text.Length)
+                {
+                    text = text.Take(length);
+                }
+
+                return new TextStyleRun(new TextPointer(text.Start, length), currentTextStyle);
+            }
+        }
+    }
+}

+ 109 - 0
src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs

@@ -0,0 +1,109 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Collections.Generic;
+using Avalonia.Platform;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// Represents a line of text that is used for text rendering.
+    /// </summary>
+    public abstract class TextLine
+    {
+        /// <summary>
+        /// Gets the text.
+        /// </summary>
+        /// <value>
+        /// The text pointer.
+        /// </value>
+        public abstract TextPointer Text { get; }
+
+        /// <summary>
+        /// Gets the text runs.
+        /// </summary>
+        /// <value>
+        /// The text runs.
+        /// </value>
+        public abstract IReadOnlyList<TextRun> TextRuns { get; }
+
+        /// <summary>
+        /// Gets the line metrics.
+        /// </summary>
+        /// <value>
+        /// The line metrics.
+        /// </value>
+        public abstract TextLineMetrics LineMetrics { get; }
+
+        /// <summary>
+        /// Draws the <see cref="TextLine"/> at the given origin.
+        /// </summary>
+        /// <param name="drawingContext">The drawing context.</param>
+        /// <param name="origin">The origin.</param>
+        public abstract void Draw(IDrawingContextImpl drawingContext, Point origin);
+
+        /// <summary>
+        /// Client to get the character hit corresponding to the specified 
+        /// distance from the beginning of the line.
+        /// </summary>
+        /// <param name="distance">distance in text flow direction from the beginning of the line</param>
+        /// <returns>The <see cref="CharacterHit"/></returns>
+        public abstract CharacterHit GetCharacterHitFromDistance(double distance);
+
+        /// <summary>
+        /// Client to get the distance from the beginning of the line from the specified 
+        /// <see cref="CharacterHit"/>.
+        /// </summary>
+        /// <param name="characterHit"><see cref="CharacterHit"/> of the character to query the distance.</param>
+        /// <returns>Distance in text flow direction from the beginning of the line.</returns>
+        public abstract double GetDistanceFromCharacterHit(CharacterHit characterHit);
+
+        /// <summary>
+        /// Client to get the next <see cref="CharacterHit"/> for caret navigation.
+        /// </summary>
+        /// <param name="characterHit">The current <see cref="CharacterHit"/>.</param>
+        /// <returns>The next <see cref="CharacterHit"/>.</returns>
+        public abstract CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit);
+
+        /// <summary>
+        /// Client to get the previous character hit for caret navigation
+        /// </summary>
+        /// <param name="characterHit">the current character hit</param>
+        /// <returns>The previous <see cref="CharacterHit"/></returns>
+        public abstract CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit);
+
+        /// <summary>
+        /// Client to get the previous character hit after backspacing
+        /// </summary>
+        /// <param name="characterHit">the current character hit</param>
+        /// <returns>The <see cref="CharacterHit"/> after backspacing</returns>
+        public abstract CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit);
+
+        /// <summary>
+        /// Gets the text line offset x.
+        /// </summary>
+        /// <param name="lineWidth">The line width.</param>
+        /// <param name="paragraphWidth">The paragraph width.</param>
+        /// <param name="textAlignment">The text alignment.</param>
+        /// <returns>The paragraph offset.</returns>
+        internal static double GetParagraphOffsetX(double lineWidth, double paragraphWidth, TextAlignment textAlignment)
+        {
+            if (double.IsPositiveInfinity(paragraphWidth))
+            {
+                return 0;
+            }
+
+            switch (textAlignment)
+            {
+                case TextAlignment.Center:
+                    return (paragraphWidth - lineWidth) / 2;
+
+                case TextAlignment.Right:
+                    return paragraphWidth - lineWidth;
+
+                default:
+                    return 0.0f;
+            }
+        }
+    }
+}

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