diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 4e742b3b7b..2506fd0624 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -369,6 +369,14 @@ namespace Avalonia.Controls get { return _newLine; } set { SetAndRaise(NewLineProperty, ref _newLine, value); } } + + /// + /// Clears the current selection, maintaining the + /// + public void ClearSelection() + { + SelectionStart = SelectionEnd = CaretIndex; + } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -413,8 +421,7 @@ namespace Avalonia.Controls if (ContextMenu == null || !ContextMenu.IsOpen) { - SelectionStart = 0; - SelectionEnd = 0; + ClearSelection(); RevealPassword = false; } @@ -444,7 +451,7 @@ namespace Avalonia.Controls text = Text ?? string.Empty; SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); CaretIndex += input.Length; - SelectionStart = SelectionEnd = CaretIndex; + ClearSelection(); _undoRedoHelper.DiscardRedo(); } } @@ -662,7 +669,7 @@ namespace Avalonia.Controls SetTextInternal(text.Substring(0, caretIndex - removedCharacters) + text.Substring(caretIndex)); CaretIndex -= removedCharacters; - SelectionStart = SelectionEnd = CaretIndex; + ClearSelection(); } _undoRedoHelper.Snapshot(); @@ -735,7 +742,7 @@ namespace Avalonia.Controls } else if (movement) { - SelectionStart = SelectionEnd = CaretIndex; + ClearSelection(); } if (handled || movement) @@ -1042,7 +1049,8 @@ namespace Avalonia.Controls var end = Math.Max(selectionStart, selectionEnd); var text = Text; SetTextInternal(text.Substring(0, start) + text.Substring(end)); - SelectionStart = SelectionEnd = CaretIndex = start; + CaretIndex = start; + ClearSelection(); return true; } else @@ -1131,7 +1139,8 @@ namespace Avalonia.Controls set { Text = value.Text; - SelectionStart = SelectionEnd = CaretIndex = value.CaretPosition; + CaretIndex = value.CaretPosition; + ClearSelection(); } } } diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 438cbc8b27..3128753781 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -141,7 +141,7 @@ namespace Avalonia.Controls.Utils var radiusY = keypoints.RightTop.Y - boundRect.TopRight.Y; if (radiusX != 0 || radiusY != 0) { - context.ArcTo(keypoints.RightTop, new Size(radiusY, radiusY), 0, false, SweepDirection.Clockwise); + context.ArcTo(keypoints.RightTop, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); } // Right diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index acc3ef16c2..1c49b24f52 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -18,12 +18,13 @@ namespace Avalonia.Diagnostics.ViewModels private int _selectedTab; private string _focusedControl; private string _pointerOverElement; + private bool _shouldVisualizeMarginPadding = true; public MainViewModel(IControl root) { _root = root; - _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root)); - _visualTree = new TreePageViewModel(VisualTreeNode.Create(root)); + _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); + _visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root)); _events = new EventsPageViewModel(root); UpdateFocusedControl(); @@ -34,6 +35,17 @@ namespace Avalonia.Diagnostics.ViewModels Console = new ConsoleViewModel(UpdateConsoleContext); } + public bool ShouldVisualizeMarginPadding + { + get => _shouldVisualizeMarginPadding; + set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); + } + + public void ToggleVisualizeMarginPadding() + { + ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding; + } + public ConsoleViewModel Console { get; } public ViewModelBase Content diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index ec48cff399..748f67523b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -10,8 +10,9 @@ namespace Avalonia.Diagnostics.ViewModels private ControlDetailsViewModel _details; private string _propertyFilter; - public TreePageViewModel(TreeNode[] nodes) + public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes) { + MainView = mainView; Nodes = nodes; Selection = new SelectionModel { @@ -23,7 +24,9 @@ namespace Avalonia.Diagnostics.ViewModels { SelectedNode = (TreeNode)Selection.SelectedItem; }; - } + } + + public MainViewModel MainView { get; } public TreeNode[] Nodes { get; protected set; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 663722acba..0165398718 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -16,6 +16,15 @@ + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 633d18ddd8..1b61986ce6 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -1,7 +1,7 @@ +using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Shapes; using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; using Avalonia.Markup.Xaml; @@ -11,45 +11,78 @@ namespace Avalonia.Diagnostics.Views { internal class TreePageView : UserControl { - private Control _adorner; + private readonly Panel _adorner; + private AdornerLayer _currentLayer; private TreeView _tree; public TreePageView() { - this.InitializeComponent(); + InitializeComponent(); _tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized; + + _adorner = new Panel + { + ClipToBounds = false, + Children = + { + //Padding frame + new Border { BorderBrush = new SolidColorBrush(Colors.Green, 0.5) }, + //Content frame + new Border { Background = new SolidColorBrush(Color.FromRgb(160, 197, 232), 0.5) }, + //Margin frame + new Border { BorderBrush = new SolidColorBrush(Colors.Yellow, 0.5) } + }, + }; } protected void AddAdorner(object sender, PointerEventArgs e) { var node = (TreeNode)((Control)sender).DataContext; - var layer = AdornerLayer.GetAdornerLayer(node.Visual); + var visual = (Visual)node.Visual; + + _currentLayer = AdornerLayer.GetAdornerLayer(visual); - if (layer != null) + if (_currentLayer == null || + _currentLayer.Children.Contains(_adorner)) { - if (_adorner != null) - { - ((Panel)_adorner.Parent).Children.Remove(_adorner); - _adorner = null; - } + return; + } - _adorner = new Rectangle - { - Fill = new SolidColorBrush(0x80a0c5e8), - [AdornerLayer.AdornedElementProperty] = node.Visual, - }; + _currentLayer.Children.Add(_adorner); + AdornerLayer.SetAdornedElement(_adorner, visual); + + var vm = (TreePageViewModel) DataContext; - layer.Children.Add(_adorner); + if (vm.MainView.ShouldVisualizeMarginPadding) + { + var paddingBorder = (Border)_adorner.Children[0]; + paddingBorder.BorderThickness = visual.GetValue(PaddingProperty); + + var contentBorder = (Border)_adorner.Children[1]; + contentBorder.Margin = visual.GetValue(PaddingProperty); + + var marginBorder = (Border)_adorner.Children[2]; + marginBorder.BorderThickness = visual.GetValue(MarginProperty); + marginBorder.Margin = InvertThickness(visual.GetValue(MarginProperty)); } } + private static Thickness InvertThickness(Thickness input) + { + return new Thickness(-input.Left, -input.Top, -input.Right, -input.Bottom); + } + protected void RemoveAdorner(object sender, PointerEventArgs e) { - if (_adorner != null) + foreach (var border in _adorner.Children.OfType()) { - ((Panel)_adorner.Parent).Children.Remove(_adorner); - _adorner = null; + border.Margin = default; + border.Padding = default; + border.BorderThickness = default; } + + _currentLayer?.Children.Remove(_adorner); + _currentLayer = null; } private void InitializeComponent() diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index cfd47d48de..804cf7f8ac 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -110,11 +110,20 @@ namespace Avalonia.Native .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta)) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) - .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)) - ; + .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)); + if (_options.UseGpu) - AvaloniaLocator.CurrentMutable.Bind() - .ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay())); + { + try + { + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay())); + } + catch (Exception) + { + // ignored + } + } } public IWindowImpl CreateWindow() diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 4b13666edd..08c5d51ea0 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -351,12 +351,12 @@ namespace Avalonia.Native public Point PointToClient(PixelPoint point) { - return _native.PointToClient(point.ToAvnPoint()).ToAvaloniaPoint(); + return _native?.PointToClient(point.ToAvnPoint()).ToAvaloniaPoint() ?? default; } public PixelPoint PointToScreen(Point point) { - return _native.PointToScreen(point.ToAvnPoint()).ToAvaloniaPixelPoint(); + return _native?.PointToScreen(point.ToAvnPoint()).ToAvaloniaPixelPoint() ?? default; } public void Hide() diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index a32a3e1b6c..da3a1f721c 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -555,7 +555,7 @@ namespace Avalonia.Media } } - return new Rect(0, 0, width, height); + return new Rect(0, GlyphTypeface.Ascent * Scale, width, height); } private void Set(ref T field, T value) @@ -595,8 +595,6 @@ namespace Avalonia.Media _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width); var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; - - _bounds = new Rect(0, 0, width, height); } void IDisposable.Dispose() diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index 8b44e32c48..08d9107bb1 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -181,6 +181,17 @@ namespace Avalonia.Media.TextFormatting return nextCharacterHit; } + if (characterHit.FirstCharacterIndex + characterHit.TrailingLength <= TextRange.Start + TextRange.Length) + { + return characterHit; // Can't move, we're after the last character + } + + var runIndex = GetRunIndexAtCodepointIndex(TextRange.End); + + var textRun = _textRuns[runIndex]; + + characterHit = textRun.GlyphRun.GetNextCaretCharacterHit(characterHit); + return characterHit; // Can't move, we're after the last character } @@ -192,6 +203,11 @@ namespace Avalonia.Media.TextFormatting return previousCharacterHit; } + if (characterHit.FirstCharacterIndex < TextRange.Start) + { + characterHit = new CharacterHit(TextRange.Start); + } + return characterHit; // Can't move, we're before the first character } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs index eaf4effdbe..bdf05c4f86 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs @@ -25,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph GlyphRun glyphRun, Point baselineOrigin, IDictionary childScenes = null) - : base(glyphRun.Bounds, transform) + : base(glyphRun.Bounds.Translate(baselineOrigin), transform) { Transform = transform; Foreground = foreground?.ToImmutable(); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs index c4d67deb4c..349143253e 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -24,7 +24,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { AvaloniaXamlIlDataContextTypeMetadataNode inferredDataContextTypeNode = null; AvaloniaXamlIlDataContextTypeMetadataNode directiveDataContextTypeNode = null; - bool isDataTemplate = on.Type.GetClrType().Equals(context.GetAvaloniaTypes().DataTemplate); for (int i = 0; i < on.Children.Count; ++i) { @@ -57,7 +56,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { inferredDataContextTypeNode = ParseDataContext(context, on, obj); } - else if(isDataTemplate + else if(context.GetAvaloniaTypes().DataTemplate.IsAssignableFrom(on.Type.GetClrType()) && pa.Property.Name == "DataType" && pa.Values[0] is XamlTypeExtensionNode dataTypeNode) { @@ -70,7 +69,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers // do more specialized inference if (directiveDataContextTypeNode is null) { - if (isDataTemplate && inferredDataContextTypeNode is null) + if (context.GetAvaloniaTypes().IDataTemplate.IsAssignableFrom(on.Type.GetClrType()) + && inferredDataContextTypeNode is null) { // Infer data type from collection binding on a control that displays items. var parentObject = context.ParentNodes().OfType().FirstOrDefault(); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index f4ca76c21c..3dec96dc43 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -41,6 +41,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType ResolveByNameExtension { get; } public IXamlType DataTemplate { get; } + public IXamlType IDataTemplate { get; } public IXamlType IItemsPresenterHost { get; } public IXamlType ItemsRepeater { get; } public IXamlType ReflectionBindingExtension { get; } @@ -98,6 +99,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers CompiledBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension"); ResolveByNameExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ResolveByNameExtension"); DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate"); + IDataTemplate = cfg.TypeSystem.GetType("Avalonia.Controls.Templates.IDataTemplate"); IItemsPresenterHost = cfg.TypeSystem.GetType("Avalonia.Controls.Presenters.IItemsPresenterHost"); ItemsRepeater = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsRepeater"); ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension"); diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index f41938a9bb..fe25fa7346 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -562,6 +562,41 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void TextBox_CaretIndex_Persists_When_Focus_Lost() + { + using (UnitTestApplication.Start(FocusServices)) + { + var target1 = new TextBox + { + Template = CreateTemplate(), + Text = "1234" + }; + var target2 = new TextBox + { + Template = CreateTemplate(), + Text = "5678" + }; + var sp = new StackPanel(); + sp.Children.Add(target1); + sp.Children.Add(target2); + + target1.ApplyTemplate(); + target2.ApplyTemplate(); + + var root = new TestRoot { Child = sp }; + + target2.Focus(); + target2.CaretIndex = 2; + Assert.False(target1.IsFocused); + Assert.True(target2.IsFocused); + + target1.Focus(); + + Assert.Equal(2, target2.CaretIndex); + } + } + [Fact] public void TextBox_Reveal_Password_Reset_When_Lost_Focus() { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 3655d78c9d..7abfe29f11 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; @@ -10,6 +9,65 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { public class TextLineTests { + private static readonly string s_multiLineText = "012345678\r\r0123456789"; + + [Fact] + public void Should_Get_First_CharacterHit() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(s_multiLineText, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var currentIndex = 0; + + while (currentIndex < s_multiLineText.Length) + { + var textLine = + formatter.FormatLine(textSource, currentIndex, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var firstCharacterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(int.MinValue)); + + Assert.Equal(textLine.TextRange.Start, firstCharacterHit.FirstCharacterIndex); + + currentIndex += textLine.TextRange.Length; + } + } + } + + [Fact] + public void Should_Get_Last_CharacterHit() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(s_multiLineText, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var currentIndex = 0; + + while (currentIndex < s_multiLineText.Length) + { + var textLine = + formatter.FormatLine(textSource, currentIndex, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var lastCharacterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(int.MaxValue)); + + Assert.Equal(textLine.TextRange.Start + textLine.TextRange.Length, + lastCharacterHit.FirstCharacterIndex + lastCharacterHit.TrailingLength); + + currentIndex += textLine.TextRange.Length; + } + } + } + [InlineData("𐐷𐐷𐐷𐐷𐐷")] [InlineData("𐐷1234")] [Theory]