diff --git a/.ncrunch/BindingDemo.v3.ncrunchproject b/.ncrunch/BindingDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/BindingDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/RenderDemo.v3.ncrunchproject b/.ncrunch/RenderDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/RenderDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/VirtualizationDemo.v3.ncrunchproject b/.ncrunch/VirtualizationDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/VirtualizationDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6a9f72039a..b70e0bf77f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -35,16 +35,16 @@ jobs: vmImage: 'macOS-10.14' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.x' + displayName: 'Use .NET Core SDK 3.1.101' inputs: packageType: sdk - version: 3.1.x + version: 3.1.101 - task: UseDotNet@2 - displayName: 'Use .NET Core Runtime 3.1.x' + displayName: 'Use .NET Core Runtime 3.1.1' inputs: packageType: runtime - version: 3.1.x + version: 3.1.1 - task: CmdLine@2 displayName: 'Install Mono 5.18' diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 4a960d47a1..ee57f54e59 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -25,6 +25,12 @@ struct IAvnGlSurfaceRenderingSession; struct IAvnAppMenu; struct IAvnAppMenuItem; +enum SystemDecorations { + SystemDecorationsNone = 0, + SystemDecorationsBorderOnly = 1, + SystemDecorationsFull = 2, +}; + struct AvnSize { double Width, Height; @@ -236,7 +242,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; - virtual HRESULT SetHasDecorations(bool value) = 0; + virtual HRESULT SetHasDecorations(SystemDecorations value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index b6ce172ffa..2c03407732 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -115,7 +115,6 @@ public: [NSApp activateIgnoringOtherApps:YES]; [Window setTitle:_lastTitle]; - [Window setTitleVisibility:NSWindowTitleVisible]; return S_OK; } @@ -411,7 +410,7 @@ class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, pub { private: bool _canResize = true; - bool _hasDecorations = true; + SystemDecorations _hasDecorations = SystemDecorationsFull; CGRect _lastUndecoratedFrame; AvnWindowState _lastWindowState; @@ -476,23 +475,26 @@ private: bool IsZoomed () { - return _hasDecorations ? [Window isZoomed] : UndecoratedIsMaximized(); + return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized(); } void DoZoom() { - if (_hasDecorations) + switch (_hasDecorations) { - [Window performZoom:Window]; - } - else - { - if (!UndecoratedIsMaximized()) - { - _lastUndecoratedFrame = [Window frame]; - } - - [Window zoom:Window]; + case SystemDecorationsNone: + if (!UndecoratedIsMaximized()) + { + _lastUndecoratedFrame = [Window frame]; + } + + [Window zoom:Window]; + break; + + case SystemDecorationsBorderOnly: + case SystemDecorationsFull: + [Window performZoom:Window]; + break; } } @@ -506,13 +508,35 @@ private: } } - virtual HRESULT SetHasDecorations(bool value) override + virtual HRESULT SetHasDecorations(SystemDecorations value) override { @autoreleasepool { _hasDecorations = value; UpdateStyle(); - + + switch (_hasDecorations) + { + case SystemDecorationsNone: + [Window setHasShadow:NO]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + break; + + case SystemDecorationsBorderOnly: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + break; + + case SystemDecorationsFull: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + break; + } + return S_OK; } } @@ -523,7 +547,6 @@ private: { _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; [Window setTitle:_lastTitle]; - [Window setTitleVisibility:NSWindowTitleVisible]; return S_OK; } @@ -645,10 +668,26 @@ protected: virtual NSWindowStyleMask GetStyle() override { unsigned long s = NSWindowStyleMaskBorderless; - if(_hasDecorations) - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; - if(_canResize) - s = s | NSWindowStyleMaskResizable; + + switch (_hasDecorations) + { + case SystemDecorationsNone: + break; + + case SystemDecorationsBorderOnly: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + break; + + case SystemDecorationsFull: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; + if(_canResize) + { + s = s | NSWindowStyleMaskResizable; + } + + break; + } + return s; } }; diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index b95e01c5ae..f3f70719e3 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -59,10 +59,17 @@ - + + + No Decorations + Border Only + Full Decorations + + Light Dark - + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index acb9bc5bc6..5f71b2ecd9 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -56,6 +56,20 @@ namespace ControlCatalog } }; Styles.Add(light); + + var decorations = this.Find("Decorations"); + decorations.SelectionChanged += (sender, e) => + { + Window window = (Window)VisualRoot; + window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex; + }; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + var decorations = this.Find("Decorations"); + decorations.SelectedIndex = (int)((Window)VisualRoot).SystemDecorations; } } } diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index 9b8f8af765..f61931ed72 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -7,7 +7,7 @@ Displays an image - + Bitmap @@ -22,6 +22,23 @@ + Crop + + None + Center + TopLeft + TopRight + BottomLeft + BottomRight + + + + + + + + + Drawing None diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index bbe89d1dfd..d8f4d6d5a2 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -1,6 +1,10 @@ +using System; +using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; namespace ControlCatalog.Pages { @@ -8,12 +12,14 @@ namespace ControlCatalog.Pages { private readonly Image _bitmapImage; private readonly Image _drawingImage; + private readonly Image _croppedImage; public ImagePage() { InitializeComponent(); _bitmapImage = this.FindControl("bitmapImage"); _drawingImage = this.FindControl("drawingImage"); + _croppedImage = this.FindControl("croppedImage"); } private void InitializeComponent() @@ -38,5 +44,32 @@ namespace ControlCatalog.Pages _drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex; } } + + public void BitmapCropChanged(object sender, SelectionChangedEventArgs e) + { + if (_croppedImage != null) + { + var comboxBox = (ComboBox)sender; + var croppedBitmap = _croppedImage.Source as CroppedBitmap; + croppedBitmap.SourceRect = GetCropRect(comboxBox.SelectedIndex); + } + } + + private PixelRect GetCropRect(int index) + { + var bitmapWidth = 640; + var bitmapHeight = 426; + var cropSize = new PixelSize(320, 240); + return index switch + { + 1 => new PixelRect(new PixelPoint((bitmapWidth - cropSize.Width) / 2, (bitmapHeight - cropSize.Width) / 2), cropSize), + 2 => new PixelRect(new PixelPoint(0, 0), cropSize), + 3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize), + 4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize), + 5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize), + _ => PixelRect.Empty + }; + + } } } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 88b99cd99a..51062f1568 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -80,8 +80,12 @@ namespace Avalonia _inheritanceParent?.RemoveInheritanceChild(this); _inheritanceParent = value; - foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType())) + var properties = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()); + var propertiesCount = properties.Count; + + for (var i = 0; i < propertiesCount; i++) { + var property = properties[i]; if (valuestore?.IsSet(property) == true) { // If local value set there can be no change. diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 29ab10278b..7d3d5492b6 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; -using Avalonia.Data; namespace Avalonia { @@ -28,8 +26,6 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _directCache = new Dictionary>(); - private readonly Dictionary> _initializedCache = - new Dictionary>(); private readonly Dictionary> _inheritedCache = new Dictionary>(); @@ -49,7 +45,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegistered(Type type) + public IReadOnlyList GetRegistered(Type type) { Contract.Requires(type != null); @@ -83,7 +79,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegisteredAttached(Type type) + public IReadOnlyList GetRegisteredAttached(Type type) { Contract.Requires(type != null); @@ -114,7 +110,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegisteredDirect(Type type) + public IReadOnlyList GetRegisteredDirect(Type type) { Contract.Requires(type != null); @@ -145,7 +141,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegisteredInherited(Type type) + public IReadOnlyList GetRegisteredInherited(Type type) { Contract.Requires(type != null); @@ -157,16 +153,27 @@ namespace Avalonia result = new List(); var visited = new HashSet(); - foreach (var property in GetRegistered(type)) + var registered = GetRegistered(type); + var registeredCount = registered.Count; + + for (var i = 0; i < registeredCount; i++) { + var property = registered[i]; + if (property.Inherits) { result.Add(property); visited.Add(property); } } - foreach (var property in GetRegisteredAttached(type)) + + var registeredAttached = GetRegisteredAttached(type); + var registeredAttachedCount = registeredAttached.Count; + + for (var i = 0; i < registeredAttachedCount; i++) { + var property = registeredAttached[i]; + if (property.Inherits) { if (!visited.Contains(property)) @@ -185,7 +192,7 @@ namespace Avalonia /// /// The object. /// A collection of definitions. - public IEnumerable GetRegistered(IAvaloniaObject o) + public IReadOnlyList GetRegistered(IAvaloniaObject o) { Contract.Requires(o != null); @@ -229,8 +236,13 @@ namespace Avalonia throw new InvalidOperationException("Attached properties not supported."); } - foreach (AvaloniaProperty x in GetRegistered(type)) + var registered = GetRegistered(type); + var registeredCount = registered.Count; + + for (var i = 0; i < registeredCount; i++) { + AvaloniaProperty x = registered[i]; + if (x.Name == name) { return x; @@ -276,8 +288,13 @@ namespace Avalonia return property; } - foreach (var p in GetRegisteredDirect(o.GetType())) + var registeredDirect = GetRegisteredDirect(o.GetType()); + var registeredDirectCount = registeredDirect.Count; + + for (var i = 0; i < registeredDirectCount; i++) { + var p = registeredDirect[i]; + if (p == property) { return (DirectPropertyBase)p; @@ -308,8 +325,23 @@ namespace Avalonia Contract.Requires(type != null); Contract.Requires(property != null); - return Instance.GetRegistered(type).Any(x => x == property) || - Instance.GetRegisteredAttached(type).Any(x => x == property); + static bool ContainsProperty(IReadOnlyList properties, AvaloniaProperty property) + { + var propertiesCount = properties.Count; + + for (var i = 0; i < propertiesCount; i++) + { + if (properties[i] == property) + { + return true; + } + } + + return false; + } + + return ContainsProperty(Instance.GetRegistered(type), property) || + ContainsProperty(Instance.GetRegisteredAttached(type), property); } /// @@ -374,7 +406,6 @@ namespace Avalonia } _registeredCache.Clear(); - _initializedCache.Clear(); _inheritedCache.Clear(); } @@ -411,32 +442,7 @@ namespace Avalonia } _attachedCache.Clear(); - _initializedCache.Clear(); _inheritedCache.Clear(); } - - private readonly struct PropertyInitializationData - { - public AvaloniaProperty Property { get; } - public object Value { get; } - public bool IsDirect { get; } - public IDirectPropertyAccessor DirectAccessor { get; } - - public PropertyInitializationData(AvaloniaProperty property, IDirectPropertyAccessor directAccessor) - { - Property = property; - Value = null; - IsDirect = true; - DirectAccessor = directAccessor; - } - - public PropertyInitializationData(AvaloniaProperty property, IStyledPropertyAccessor styledAccessor, Type type) - { - Property = property; - Value = styledAccessor.GetDefaultValue(type); - IsDirect = false; - DirectAccessor = null; - } - } } } diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index ea06bdb394..07e42c64e4 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -476,7 +476,7 @@ namespace Avalonia.Controls { _dropDownButton.Click += DropDownButton_Click; _buttonPointerPressedSubscription = - _dropDownButton.AddHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true); + _dropDownButton.AddDisposableHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true); } if (_textBox != null) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 9d471a0fc0..d1f54da23a 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.VisualTree; @@ -265,7 +266,7 @@ namespace Avalonia.Controls var toplevel = this.GetVisualRoot() as TopLevel; if (toplevel != null) { - _subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) => + _subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => { //eat wheel scroll event outside dropdown popup while it's open if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel) diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 91b895f38a..238070bbef 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -36,7 +36,7 @@ namespace Avalonia.Platform /// /// Enables or disables system window decorations (title bar, buttons, etc) /// - void SetSystemDecorations(bool enabled); + void SetSystemDecorations(SystemDecorations enabled); /// /// Sets the icon of this window. diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 9dd481cf40..1a12b53b9d 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -279,7 +279,7 @@ namespace Avalonia.Controls.Primitives } } - DeferCleanup(topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel)); + DeferCleanup(topLevel.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel)); DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick)); diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 1655e22331..0278360ba5 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -20,6 +20,12 @@ namespace Avalonia.Controls public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty PaddingProperty = + Decorator.PaddingProperty.AddOwner(); + // TODO: Define these attached properties elsewhere (e.g. on a Text class) and AddOwner // them into TextBlock. @@ -29,7 +35,7 @@ namespace Avalonia.Controls public static readonly AttachedProperty FontFamilyProperty = AvaloniaProperty.RegisterAttached( nameof(FontFamily), - defaultValue: FontFamily.Default, + defaultValue: FontFamily.Default, inherits: true); /// @@ -110,20 +116,31 @@ namespace Avalonia.Controls static TextBlock() { ClipToBoundsProperty.OverrideDefaultValue(true); + AffectsRender( - BackgroundProperty, - ForegroundProperty, - FontWeightProperty, - FontSizeProperty, - FontStyleProperty); + BackgroundProperty, ForegroundProperty, FontSizeProperty, + FontWeightProperty, FontStyleProperty, TextWrappingProperty, + TextTrimmingProperty, TextAlignmentProperty, FontFamilyProperty, + TextDecorationsProperty, TextProperty, PaddingProperty); + + AffectsMeasure( + FontSizeProperty, FontWeightProperty, FontStyleProperty, + FontFamilyProperty, TextTrimmingProperty, TextProperty, + PaddingProperty); Observable.Merge( TextProperty.Changed, + ForegroundProperty.Changed, TextAlignmentProperty.Changed, + TextWrappingProperty.Changed, + TextTrimmingProperty.Changed, FontSizeProperty.Changed, FontStyleProperty.Changed, - FontWeightProperty.Changed - ).AddClassHandler((x, _) => x.OnTextPropertiesChanged()); + FontWeightProperty.Changed, + FontFamilyProperty.Changed, + TextDecorationsProperty.Changed, + PaddingProperty.Changed + ).AddClassHandler((x, _) => x.InvalidateTextLayout()); } /// @@ -145,6 +162,15 @@ namespace Avalonia.Controls } } + /// + /// Gets or sets the padding to place around the . + /// + public Thickness Padding + { + get { return GetValue(PaddingProperty); } + set { SetValue(PaddingProperty, value); } + } + /// /// Gets or sets a brush used to paint the control's background. /// @@ -363,7 +389,9 @@ namespace Avalonia.Controls context.FillRectangle(background, new Rect(Bounds.Size)); } - TextLayout?.Draw(context.PlatformImpl, new Point()); + var padding = Padding; + + TextLayout?.Draw(context.PlatformImpl, new Point(padding.Left, padding.Top)); } /// @@ -412,6 +440,10 @@ namespace Avalonia.Controls return new Size(); } + var padding = Padding; + + availableSize = availableSize.Deflate(padding); + if (_constraint != availableSize) { InvalidateTextLayout(); @@ -419,19 +451,17 @@ namespace Avalonia.Controls _constraint = availableSize; - return TextLayout?.Bounds.Size ?? Size.Empty; + var measuredSize = TextLayout?.Bounds.Size ?? Size.Empty; + + return measuredSize.Inflate(padding); } protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnAttachedToLogicalTree(e); - InvalidateTextLayout(); - InvalidateMeasure(); - } - private void OnTextPropertiesChanged() - { InvalidateTextLayout(); + InvalidateMeasure(); } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index a438d7380b..4acfb75cb9 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -275,6 +275,22 @@ namespace Avalonia.Controls } } + public string SelectedText + { + get { return GetSelection(); } + set + { + if (value == null) + { + return; + } + + _undoRedoHelper.Snapshot(); + HandleTextInput(value); + _undoRedoHelper.Snapshot(); + } + } + /// /// Gets or sets the horizontal alignment of the content within the control. /// @@ -683,12 +699,12 @@ namespace Avalonia.Controls protected override void OnPointerPressed(PointerPressedEventArgs e) { - var point = e.GetPosition(_presenter); - var index = CaretIndex = _presenter.GetCaretIndex(point); var text = Text; - if (text != null && e.MouseButton == MouseButton.Left) + if (text != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { + var point = e.GetPosition(_presenter); + var index = CaretIndex = _presenter.GetCaretIndex(point); switch (e.ClickCount) { case 1: @@ -714,7 +730,8 @@ namespace Avalonia.Controls protected override void OnPointerMoved(PointerEventArgs e) { - if (_presenter != null && e.Pointer.Captured == _presenter) + // selection should not change during pointer move if the user right clicks + if (_presenter != null && e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { var point = e.GetPosition(_presenter); @@ -727,6 +744,22 @@ namespace Avalonia.Controls { if (_presenter != null && e.Pointer.Captured == _presenter) { + if (e.InitialPressMouseButton == MouseButton.Right) + { + var point = e.GetPosition(_presenter); + var caretIndex = _presenter.GetCaretIndex(point); + + // see if mouse clicked inside current selection + // if it did not, we change the selection to where the user clicked + var firstSelection = Math.Min(SelectionStart, SelectionEnd); + var lastSelection = Math.Max(SelectionStart, SelectionEnd); + var didClickInSelection = SelectionStart != SelectionEnd && + caretIndex >= firstSelection && caretIndex <= lastSelection; + if (!didClickInSelection) + { + CaretIndex = SelectionEnd = SelectionStart = caretIndex; + } + } e.Pointer.Capture(null); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 6554237b3a..f4a3f05f03 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -2,19 +2,19 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.ComponentModel; +using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Controls.Platform; +using Avalonia.Data; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Styling; -using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; -using System.ComponentModel; -using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -45,6 +45,27 @@ namespace Avalonia.Controls WidthAndHeight = 3, } + /// + /// Determines system decorations (title bar, border, etc) for a + /// + public enum SystemDecorations + { + /// + /// No decorations + /// + None = 0, + + /// + /// Window border without titlebar + /// + BorderOnly = 1, + + /// + /// Fully decorated (default) + /// + Full = 2 + } + /// /// A top-level window. /// @@ -59,8 +80,18 @@ namespace Avalonia.Controls /// /// Enables or disables system window decorations (title bar, buttons, etc) /// - public static readonly StyledProperty HasSystemDecorationsProperty = - AvaloniaProperty.Register(nameof(HasSystemDecorations), true); + [Obsolete("Use SystemDecorationsProperty instead")] + public static readonly DirectProperty HasSystemDecorationsProperty = + AvaloniaProperty.RegisterDirect( + nameof(HasSystemDecorations), + o => o.HasSystemDecorations, + (o, v) => o.HasSystemDecorations = v); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SystemDecorationsProperty = + AvaloniaProperty.Register(nameof(SystemDecorations), SystemDecorations.Full); /// /// Enables or disables the taskbar icon @@ -124,9 +155,6 @@ namespace Avalonia.Controls { BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); - HasSystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue)); - ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); IconProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue)?.PlatformImpl)); @@ -135,12 +163,11 @@ namespace Avalonia.Controls WindowStateProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); - + MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); MaxHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); - } /// @@ -191,11 +218,30 @@ namespace Avalonia.Controls /// /// Enables or disables system window decorations (title bar, buttons, etc) /// - /// + [Obsolete("Use SystemDecorations instead")] public bool HasSystemDecorations { - get { return GetValue(HasSystemDecorationsProperty); } - set { SetValue(HasSystemDecorationsProperty, value); } + get => SystemDecorations == SystemDecorations.Full; + set + { + var oldValue = HasSystemDecorations; + + if (oldValue != value) + { + SystemDecorations = value ? SystemDecorations.Full : SystemDecorations.None; + RaisePropertyChanged(HasSystemDecorationsProperty, oldValue, value); + } + } + } + + /// + /// Sets the system decorations (title bar, border, etc) + /// + /// + public SystemDecorations SystemDecorations + { + get { return GetValue(SystemDecorationsProperty); } + set { SetValue(SystemDecorationsProperty, value); } } /// @@ -584,5 +630,27 @@ namespace Avalonia.Controls /// event needs to be raised. /// protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); + + protected override void OnPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) + { + if (property == SystemDecorationsProperty) + { + var typedNewValue = newValue.GetValueOrDefault(); + + PlatformImpl?.SetSystemDecorations(typedNewValue); + + var o = oldValue.GetValueOrDefault() == SystemDecorations.Full; + var n = typedNewValue == SystemDecorations.Full; + + if (o != n) + { + RaisePropertyChanged(HasSystemDecorationsProperty, o, n); + } + } + } } } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 86e34ca6d4..7480b3519c 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -96,7 +96,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 4bba5ef41b..7bf1d236bd 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -110,7 +110,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 0464047273..b3a8f4745e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -22,7 +22,7 @@ namespace Avalonia.Diagnostics } } - return root.AddHandler( + return root.AddDisposableHandler( InputElement.KeyDownEvent, PreviewKeyDown, RoutingStrategies.Tunnel); diff --git a/src/Avalonia.Interactivity/IInteractive.cs b/src/Avalonia.Interactivity/IInteractive.cs index 33baa9453a..51aee78988 100644 --- a/src/Avalonia.Interactivity/IInteractive.cs +++ b/src/Avalonia.Interactivity/IInteractive.cs @@ -23,7 +23,7 @@ namespace Avalonia.Interactivity /// The routing strategies to listen to. /// Whether handled events should also be listened for. /// A disposable that terminates the event subscription. - IDisposable AddHandler( + void AddHandler( RoutedEvent routedEvent, Delegate handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, @@ -38,7 +38,7 @@ namespace Avalonia.Interactivity /// The routing strategies to listen to. /// Whether handled events should also be listened for. /// A disposable that terminates the event subscription. - IDisposable AddHandler( + void AddHandler( RoutedEvent routedEvent, EventHandler handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 9493d86885..321ecbc516 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -27,8 +27,7 @@ namespace Avalonia.Interactivity /// The handler. /// The routing strategies to listen to. /// Whether handled events should also be listened for. - /// A disposable that terminates the event subscription. - public IDisposable AddHandler( + public void AddHandler( RoutedEvent routedEvent, Delegate handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, @@ -38,7 +37,8 @@ namespace Avalonia.Interactivity handler = handler ?? throw new ArgumentNullException(nameof(handler)); var subscription = new EventSubscription(handler, routes, handledEventsToo); - return AddEventSubscription(routedEvent, subscription); + + AddEventSubscription(routedEvent, subscription); } /// @@ -49,8 +49,7 @@ namespace Avalonia.Interactivity /// The handler. /// The routing strategies to listen to. /// Whether handled events should also be listened for. - /// A disposable that terminates the event subscription. - public IDisposable AddHandler( + public void AddHandler( RoutedEvent routedEvent, EventHandler handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, @@ -69,7 +68,7 @@ namespace Avalonia.Interactivity var subscription = new EventSubscription(handler, routes, handledEventsToo, (baseHandler, sender, args) => InvokeAdapter(baseHandler, sender, args)); - return AddEventSubscription(routedEvent, subscription); + AddEventSubscription(routedEvent, subscription); } /// @@ -188,7 +187,7 @@ namespace Avalonia.Interactivity return result; } - private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription) + private void AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription) { _eventHandlers ??= new Dictionary>(); @@ -199,8 +198,6 @@ namespace Avalonia.Interactivity } subscriptions.Add(subscription); - - return new UnsubscribeDisposable(subscriptions, subscription); } private readonly struct EventSubscription @@ -225,22 +222,5 @@ namespace Avalonia.Interactivity public bool HandledEventsToo { get; } } - - private sealed class UnsubscribeDisposable : IDisposable - { - private readonly List _subscriptions; - private readonly EventSubscription _subscription; - - public UnsubscribeDisposable(List subscriptions, EventSubscription subscription) - { - _subscriptions = subscriptions; - _subscription = subscription; - } - - public void Dispose() - { - _subscriptions.Remove(_subscription); - } - } } } diff --git a/src/Avalonia.Interactivity/InteractiveExtensions.cs b/src/Avalonia.Interactivity/InteractiveExtensions.cs index 414c408080..e6c93e26b2 100644 --- a/src/Avalonia.Interactivity/InteractiveExtensions.cs +++ b/src/Avalonia.Interactivity/InteractiveExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Disposables; using System.Reactive.Linq; namespace Avalonia.Interactivity @@ -11,6 +12,28 @@ namespace Avalonia.Interactivity /// public static class InteractiveExtensions { + /// + /// Adds a handler for the specified routed event and returns a disposable that can terminate the event subscription. + /// + /// The type of the event's args. + /// Target for adding given event handler. + /// The routed event. + /// The handler. + /// The routing strategies to listen to. + /// Whether handled events should also be listened for. + /// A disposable that terminates the event subscription. + public static IDisposable AddDisposableHandler(this IInteractive o, RoutedEvent routedEvent, + EventHandler handler, + RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, + bool handledEventsToo = false) where TEventArgs : RoutedEventArgs + { + o.AddHandler(routedEvent, handler, routes, handledEventsToo); + + return Disposable.Create( + (instance: o, handler, routedEvent), + state => state.instance.RemoveHandler(state.routedEvent, state.handler)); + } + /// /// Gets an observable for a . /// @@ -31,7 +54,7 @@ namespace Avalonia.Interactivity o = o ?? throw new ArgumentNullException(nameof(o)); routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); - return Observable.Create(x => o.AddHandler( + return Observable.Create(x => o.AddDisposableHandler( routedEvent, (_, e) => x.OnNext(e), routes, diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index c757576017..73ec81ce57 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -68,9 +68,9 @@ namespace Avalonia.Native _native.CanResize = value; } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(Controls.SystemDecorations enabled) { - _native.HasDecorations = enabled; + _native.HasDecorations = (Interop.SystemDecorations)enabled; } public void SetTitleBarColor (Avalonia.Media.Color color) diff --git a/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs index c884b6ac43..7906a29cb5 100644 --- a/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs +++ b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs @@ -14,6 +14,7 @@ namespace Avalonia.Styling.Activators { private readonly IList _match; private readonly IAvaloniaReadOnlyList _classes; + private NotifyCollectionChangedEventHandler? _classesChangedHandler; public StyleClassActivator(IAvaloniaReadOnlyList classes, IList match) { @@ -21,6 +22,9 @@ namespace Avalonia.Styling.Activators _match = match; } + private NotifyCollectionChangedEventHandler ClassesChangedHandler => + _classesChangedHandler ??= ClassesChanged; + public static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) { int remainingMatches = toMatch.Count; @@ -51,16 +55,15 @@ namespace Avalonia.Styling.Activators return remainingMatches == 0; } - protected override void Initialize() { PublishNext(IsMatching()); - _classes.CollectionChanged += ClassesChanged; + _classes.CollectionChanged += ClassesChangedHandler; } protected override void Deinitialize() { - _classes.CollectionChanged -= ClassesChanged; + _classes.CollectionChanged -= ClassesChangedHandler; } private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) diff --git a/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs new file mode 100644 index 0000000000..70823e114c --- /dev/null +++ b/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs @@ -0,0 +1,96 @@ +using System; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media.Imaging +{ + /// + /// Crops a Bitmap. + /// + public class CroppedBitmap : AvaloniaObject, IImage, IAffectsRender, IDisposable + { + /// + /// Defines the property. + /// + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SourceRectProperty = + AvaloniaProperty.Register(nameof(SourceRect)); + + public event EventHandler Invalidated; + + static CroppedBitmap() + { + SourceRectProperty.Changed.AddClassHandler((x, e) => x.SourceRectChanged(e)); + SourceProperty.Changed.AddClassHandler((x, e) => x.SourceChanged(e)); + } + + /// + /// Gets or sets the source for the bitmap. + /// + public IImage Source + { + get => GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + + /// + /// Gets or sets the rectangular area that the bitmap is cropped to. + /// + public PixelRect SourceRect + { + get => GetValue(SourceRectProperty); + set => SetValue(SourceRectProperty, value); + } + + public CroppedBitmap() + { + Source = null; + SourceRect = default; + } + + public CroppedBitmap(IImage source, PixelRect sourceRect) + { + Source = source; + SourceRect = sourceRect; + } + + private void SourceChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.NewValue == null) + return; + if (!(e.NewValue is IBitmap)) + throw new ArgumentException("Only IBitmap supported as source"); + Invalidated?.Invoke(this, e); + } + + private void SourceRectChanged(AvaloniaPropertyChangedEventArgs e) => Invalidated?.Invoke(this, e); + + public virtual void Dispose() + { + (Source as IBitmap)?.Dispose(); + } + + public Size Size { + get + { + if (Source == null) + return Size.Empty; + if (SourceRect.IsEmpty) + return Source.Size; + return SourceRect.Size.ToSizeWithDpi((Source as IBitmap).Dpi); + } + } + + public void Draw(DrawingContext context, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + { + if (Source == null) + return; + var topLeft = SourceRect.TopLeft.ToPointWithDpi((Source as IBitmap).Dpi); + Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect, bitmapInterpolationMode); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs index e84242c628..30d513386e 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs @@ -201,18 +201,17 @@ namespace Avalonia.Media.TextFormatting var availableWidth = paragraphWidth; var currentWidth = 0.0; var runIndex = 0; + var length = 0; while (runIndex < textRuns.Count) { var currentRun = textRuns[runIndex]; - currentWidth += currentRun.GlyphRun.Bounds.Width; - - if (currentWidth > availableWidth) + if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) { - var measuredLength = MeasureText(currentRun, paragraphWidth); + var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth); - if (measuredLength < text.End) + if (measuredLength < currentRun.Text.Length) { var currentBreakPosition = -1; @@ -241,15 +240,19 @@ namespace Avalonia.Media.TextFormatting } } - var splitResult = SplitTextRuns(textRuns, measuredLength); + length += measuredLength; + + var splitResult = SplitTextRuns(textRuns, length); var textLineMetrics = TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment); - return new SimpleTextLine(text.Take(measuredLength), splitResult.First, textLineMetrics); + return new SimpleTextLine(text.Take(length), splitResult.First, textLineMetrics); } - availableWidth -= currentRun.GlyphRun.Bounds.Width; + currentWidth += currentRun.GlyphRun.Bounds.Width; + + length += currentRun.GlyphRun.Characters.Length; runIndex++; } @@ -281,12 +284,18 @@ namespace Avalonia.Media.TextFormatting if (measuredWidth + advance > availableWidth) { + index--; break; } measuredWidth += advance; } + if(index < 0) + { + return 0; + } + var cluster = textRun.GlyphRun.GlyphClusters[index]; var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _); @@ -355,7 +364,7 @@ namespace Avalonia.Media.TextFormatting continue; } - var firstCount = currentRun.GlyphRun.Characters.Length > 1 ? i + 1 : i; + var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; var first = new ShapedTextRun[firstCount]; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 0c9013e6f7..3f0cf7c680 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -233,6 +233,12 @@ namespace Avalonia.Media.TextFormatting var textLine = TextFormatter.Current.FormatLine(textSource, 0, MaxWidth, _paragraphProperties); + if (!double.IsPositiveInfinity(MaxHeight) && bottom + textLine.LineMetrics.Size.Height > MaxHeight) + { + currentPosition = _text.Length; + break; + } + UpdateBounds(textLine, ref left, ref right, ref bottom); textLines.Add(textLine); @@ -253,17 +259,17 @@ namespace Avalonia.Media.TextFormatting { var emptyTextLine = CreateEmptyTextLine(currentPosition); + if (!double.IsPositiveInfinity(MaxHeight) && bottom + emptyTextLine.LineMetrics.Size.Height > MaxHeight) + { + break; + } + 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); diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs index 10fe74f9b9..29cc365251 100644 --- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs @@ -8,6 +8,7 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Imaging")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")] diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index b065079564..f11bd76a7b 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -355,15 +355,21 @@ namespace Avalonia.Rendering node.BeginRender(context, isLayerRoot); - foreach (var operation in node.DrawOperations) + var drawOperations = node.DrawOperations; + var drawOperationsCount = drawOperations.Count; + for (int i = 0; i < drawOperationsCount; i++) { + var operation = drawOperations[i]; _currentDraw = operation; operation.Item.Render(context); _currentDraw = null; } - foreach (var child in node.Children) + var children = node.Children; + var childrenCount = children.Count; + for (int i = 0; i < childrenCount; i++) { + var child = children[i]; Render(context, (VisualNode)child, layer, clipBounds); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 94f1dbc8d1..2492f5a43a 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -173,6 +173,7 @@ namespace Avalonia.X11 Surfaces = surfaces.ToArray(); UpdateMotifHints(); + UpdateSizeHints(null); _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing, XNames.XNClientWindow, _handle, IntPtr.Zero); XFlush(_x11.Display); @@ -219,12 +220,16 @@ namespace Avalonia.X11 var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border | MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH; - if (_popup || !_systemDecorations) + if (_popup || _systemDecorations == SystemDecorations.None) { decorations = 0; } + else if (_systemDecorations == SystemDecorations.BorderOnly) + { + decorations = MotifDecorations.Border; + } - if (!_canResize) + if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) { functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize); decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH); @@ -247,7 +252,7 @@ namespace Avalonia.X11 var min = _minMaxSize.minSize; var max = _minMaxSize.maxSize; - if (!_canResize) + if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) max = min = _realSize; if (preResize.HasValue) @@ -621,7 +626,7 @@ namespace Avalonia.X11 return rv; } - private bool _systemDecorations = true; + private SystemDecorations _systemDecorations = SystemDecorations.Full; private bool _canResize = true; private const int MaxWindowDimension = 100000; @@ -777,10 +782,11 @@ namespace Avalonia.X11 (int)(point.X * Scaling + Position.X), (int)(point.Y * Scaling + Position.Y)); - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { _systemDecorations = enabled; UpdateMotifHints(); + UpdateSizeHints(null); } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 904e122382..50b568cab2 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1298,7 +1298,17 @@ namespace Avalonia.Win32.Interop [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)] internal static extern void DoDragDrop(IOleDataObject dataObject, IDropSource dropSource, int allowedEffects, out int finalEffect); + [DllImport("dwmapi.dll")] + public static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); + [StructLayout(LayoutKind.Sequential)] + internal struct MARGINS + { + public int cxLeftWidth; + public int cxRightWidth; + public int cyTopHeight; + public int cyBottomHeight; + } public enum MONITOR { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index cea9f88799..6900d73c93 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -34,7 +34,7 @@ namespace Avalonia.Win32 private IInputRoot _owner; private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock(); private bool _trackingMouse; - private bool _decorated = true; + private SystemDecorations _decorated = SystemDecorations.Full; private bool _resizable = true; private bool _topmost = false; private bool _taskbarIcon = true; @@ -97,7 +97,7 @@ namespace Avalonia.Win32 { get { - if (_decorated) + if (_decorated == SystemDecorations.Full) { var style = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); var exStyle = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE); @@ -281,7 +281,7 @@ namespace Avalonia.Win32 UnmanagedMethods.ShowWindow(_hwnd, UnmanagedMethods.ShowWindowCommand.Hide); } - public void SetSystemDecorations(bool value) + public void SetSystemDecorations(SystemDecorations value) { if (value == _decorated) { @@ -464,7 +464,7 @@ namespace Avalonia.Win32 return IntPtr.Zero; case WindowsMessage.WM_NCCALCSIZE: - if (ToInt32(wParam) == 1 && !_decorated) + if (ToInt32(wParam) == 1 && _decorated != SystemDecorations.Full) { return IntPtr.Zero; } @@ -682,14 +682,14 @@ namespace Avalonia.Win32 break; case WindowsMessage.WM_NCPAINT: - if (!_decorated) + if (_decorated != SystemDecorations.Full) { return IntPtr.Zero; } break; case WindowsMessage.WM_NCACTIVATE: - if (!_decorated) + if (_decorated != SystemDecorations.Full) { return new IntPtr(1); } @@ -1001,7 +1001,7 @@ namespace Avalonia.Win32 style |= WindowStyles.WS_OVERLAPPEDWINDOW; - if (!_decorated) + if (_decorated != SystemDecorations.Full) { style ^= (WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU); } @@ -1011,6 +1011,10 @@ namespace Avalonia.Win32 style ^= (WindowStyles.WS_SIZEFRAME); } + MARGINS margins = new MARGINS(); + margins.cyBottomHeight = _decorated == SystemDecorations.BorderOnly ? 1 : 0; + UnmanagedMethods.DwmExtendFrameIntoClientArea(_hwnd, ref margins); + GetClientRect(_hwnd, out var oldClientRect); var oldClientRectOrigin = new UnmanagedMethods.POINT(); ClientToScreen(_hwnd, ref oldClientRectOrigin); @@ -1024,7 +1028,7 @@ namespace Avalonia.Win32 if (oldDecorated != _decorated) { var newRect = oldClientRect; - if (_decorated) + if (_decorated == SystemDecorations.Full) AdjustWindowRectEx(ref newRect, (uint)style, false, GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE)); SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height, diff --git a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs index 65a6c15971..838bf49846 100644 --- a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs +++ b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs @@ -20,7 +20,7 @@ namespace Avalonia.iOS return Disposable.Empty; } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } diff --git a/tests/Avalonia.Benchmarks/NullGlyphRun.cs b/tests/Avalonia.Benchmarks/NullGlyphRun.cs new file mode 100644 index 0000000000..5ba0822649 --- /dev/null +++ b/tests/Avalonia.Benchmarks/NullGlyphRun.cs @@ -0,0 +1,11 @@ +using Avalonia.Platform; + +namespace Avalonia.Benchmarks +{ + internal class NullGlyphRun : IGlyphRunImpl + { + public void Dispose() + { + } + } +} diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 101d40f00a..1f983069c2 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -72,7 +72,9 @@ namespace Avalonia.Benchmarks public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) { - throw new NotImplementedException(); + width = default; + + return new NullGlyphRun(); } } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 225eca17b2..14251454ea 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -385,6 +385,49 @@ namespace Avalonia.Controls.UnitTests Assert.True(target.SelectionEnd <= "123".Length); } } + + [Fact] + public void SelectedText_Changes_OnSelectionChange() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123456789" + }; + + Assert.True(target.SelectedText == ""); + + target.SelectionStart = 2; + target.SelectionEnd = 4; + + Assert.True(target.SelectedText == "23"); + } + } + + [Fact] + public void SelectedText_EditsText() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123" + }; + + target.SelectedText = "AA"; + Assert.True(target.Text == "AA0123"); + + target.SelectionStart = 1; + target.SelectionEnd = 3; + target.SelectedText = "BB"; + + Assert.True(target.Text == "ABB123"); + } + } + [Fact] public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending() {